mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
28 Commits
fix-platfo
...
Expression
Author | SHA1 | Date | |
---|---|---|---|
![]() |
55fc686a5b | ||
![]() |
5580189f88 | ||
![]() |
9a6d1d6d32 | ||
![]() |
6c9739c01d | ||
![]() |
cd3c997b28 | ||
![]() |
8064c4de57 | ||
![]() |
8666851f54 | ||
![]() |
6d568b2f2c | ||
![]() |
180d4318aa | ||
![]() |
65a57f86da | ||
![]() |
6a3e7f9c58 | ||
![]() |
74f1d571ba | ||
![]() |
a7cb3fc5a2 | ||
![]() |
08d3c3323a | ||
![]() |
fec603b811 | ||
![]() |
d68affc117 | ||
![]() |
77cd6c44d6 | ||
![]() |
2b4c8813e4 | ||
![]() |
2ef9266ec4 | ||
![]() |
ceba6cf739 | ||
![]() |
18f2085de7 | ||
![]() |
b586fb87ed | ||
![]() |
8aed02ab17 | ||
![]() |
8c383fc448 | ||
![]() |
af3a2016f2 | ||
![]() |
7a20161794 | ||
![]() |
0feb4ef321 | ||
![]() |
5cbcd16523 |
@@ -670,18 +670,6 @@ gd::String EventsCodeGenerator::GenerateActionsListCode(
|
||||
return outputCode;
|
||||
}
|
||||
|
||||
const gd::String EventsCodeGenerator::GenerateRelationalOperatorCodes(const gd::String &operatorString) {
|
||||
if (operatorString == "=") {
|
||||
return "==";
|
||||
}
|
||||
if (operatorString != "<" && operatorString != ">" &&
|
||||
operatorString != "<=" && operatorString != ">=" && operatorString != "!=") {
|
||||
cout << "Warning: Bad relational operator: Set to == by default." << endl;
|
||||
return "==";
|
||||
}
|
||||
return operatorString;
|
||||
}
|
||||
|
||||
gd::String EventsCodeGenerator::GenerateParameterCodes(
|
||||
const gd::Expression& parameter,
|
||||
const gd::ParameterMetadata& metadata,
|
||||
@@ -706,7 +694,14 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
|
||||
argOutput =
|
||||
GenerateObject(parameter.GetPlainString(), metadata.GetType(), context);
|
||||
} else if (metadata.GetType() == "relationalOperator") {
|
||||
argOutput += GenerateRelationalOperatorCodes(parameter.GetPlainString());
|
||||
auto parameterString = parameter.GetPlainString();
|
||||
argOutput += parameterString == "=" ? "==" : parameterString;
|
||||
if (argOutput != "==" && argOutput != "<" && argOutput != ">" &&
|
||||
argOutput != "<=" && argOutput != ">=" && argOutput != "!=") {
|
||||
cout << "Warning: Bad relational operator: Set to == by default." << endl;
|
||||
argOutput = "==";
|
||||
}
|
||||
|
||||
argOutput = "\"" + argOutput + "\"";
|
||||
} else if (metadata.GetType() == "operator") {
|
||||
argOutput += parameter.GetPlainString();
|
||||
|
@@ -481,9 +481,6 @@ class GD_CORE_API EventsCodeGenerator {
|
||||
*/
|
||||
size_t GenerateSingleUsageUniqueIdForEventsList();
|
||||
|
||||
virtual const gd::String GenerateRelationalOperatorCodes(
|
||||
const gd::String& operatorString);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* \brief Generate the code for a single parameter.
|
||||
|
@@ -81,7 +81,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
|
||||
.AddExpression(
|
||||
"GetArgumentAsNumber",
|
||||
_("Get function parameter value"),
|
||||
_("Get function parameter (also called \"argument\") value."),
|
||||
_("Get function parameter (also called \"argument\") value"),
|
||||
"",
|
||||
"res/function16.png")
|
||||
.AddParameter("functionParameterName", "Parameter name");
|
||||
@@ -90,34 +90,10 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
|
||||
.AddStrExpression(
|
||||
"GetArgumentAsString",
|
||||
_("Get function parameter text"),
|
||||
_("Get function parameter (also called \"argument\") text."),
|
||||
_("Get function parameter (also called \"argument\") text "),
|
||||
"",
|
||||
"res/function16.png")
|
||||
.AddParameter("functionParameterName", "Parameter name");
|
||||
|
||||
extension
|
||||
.AddCondition(
|
||||
"CompareArgumentAsNumber",
|
||||
_("Compare function parameter value"),
|
||||
_("Compare function parameter (also called \"argument\") value."),
|
||||
_("Parameter _PARAM0_"),
|
||||
"",
|
||||
"res/function32.png",
|
||||
"res/function16.png")
|
||||
.AddParameter("functionParameterName", "Parameter name")
|
||||
.UseStandardRelationalOperatorParameters("number");
|
||||
|
||||
extension
|
||||
.AddCondition(
|
||||
"CompareArgumentAsString",
|
||||
_("Compare function parameter text"),
|
||||
_("Compare function parameter (also called \"argument\") text."),
|
||||
_("Parameter _PARAM0_"),
|
||||
"",
|
||||
"res/function32.png",
|
||||
"res/function16.png")
|
||||
.AddParameter("functionParameterName", "Parameter name")
|
||||
.UseStandardRelationalOperatorParameters("string");
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -96,7 +96,7 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
|
||||
"JSONToVariableStructure",
|
||||
_("Convert JSON to a scene variable"),
|
||||
_("Parse a JSON object and store it into a scene variable"),
|
||||
_("Convert JSON string _PARAM0_ and store it into variable _PARAM1_"),
|
||||
_("Parse JSON string _PARAM0_ and store it into variable _PARAM1_"),
|
||||
"",
|
||||
"res/actions/net24.png",
|
||||
"res/actions/net.png")
|
||||
@@ -108,7 +108,7 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
|
||||
.AddAction("JSONToGlobalVariableStructure",
|
||||
_("Convert JSON to global variable"),
|
||||
_("Parse a JSON object and store it into a global variable"),
|
||||
_("Convert JSON string _PARAM0_ and store it into global "
|
||||
_("Parse JSON string _PARAM0_ and store it into global "
|
||||
"variable _PARAM1_"),
|
||||
"",
|
||||
"res/actions/net24.png",
|
||||
|
@@ -324,19 +324,6 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
"res/conditions/animation24.png",
|
||||
"res/conditions/animation.png")
|
||||
|
||||
.AddParameter("object", _("Object"), "Sprite")
|
||||
.MarkAsSimple()
|
||||
.SetHidden();
|
||||
|
||||
obj.AddCondition("AnimationEnded2",
|
||||
_("Animation finished"),
|
||||
_("Check if the animation being played by the Sprite object "
|
||||
"is finished."),
|
||||
_("The animation of _PARAM0_ is finished"),
|
||||
_("Animations and images"),
|
||||
"res/conditions/animation24.png",
|
||||
"res/conditions/animation.png")
|
||||
|
||||
.AddParameter("object", _("Object"), "Sprite")
|
||||
.MarkAsSimple();
|
||||
|
||||
|
@@ -236,6 +236,7 @@ class GD_CORE_API BehaviorMetadata {
|
||||
}
|
||||
|
||||
const gd::String& GetName() const;
|
||||
#if defined(GD_IDE_ONLY)
|
||||
const gd::String& GetFullName() const { return fullname; }
|
||||
const gd::String& GetDefaultName() const { return defaultName; }
|
||||
const gd::String& GetDescription() const { return description; }
|
||||
@@ -256,21 +257,7 @@ class GD_CORE_API BehaviorMetadata {
|
||||
* \note An empty string means the base object, so any object.
|
||||
*/
|
||||
const gd::String& GetObjectType() const { return objectType; }
|
||||
|
||||
/**
|
||||
* Check if the behavior is private - it can't be used outside of its
|
||||
* extension.
|
||||
*/
|
||||
bool IsPrivate() const { return isPrivate; }
|
||||
|
||||
/**
|
||||
* Set that the behavior is private - it can't be used outside of its
|
||||
* extension.
|
||||
*/
|
||||
BehaviorMetadata &SetPrivate() {
|
||||
isPrivate = true;
|
||||
return *this;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Return the associated gd::Behavior, handling behavior contents.
|
||||
@@ -328,7 +315,6 @@ class GD_CORE_API BehaviorMetadata {
|
||||
gd::String group;
|
||||
gd::String iconFilename;
|
||||
gd::String objectType;
|
||||
bool isPrivate = false;
|
||||
|
||||
// TODO: Nitpicking: convert these to std::unique_ptr to clarify ownership.
|
||||
std::shared_ptr<gd::Behavior> instance;
|
||||
|
@@ -15,12 +15,9 @@ ParameterMetadata::ParameterMetadata() : codeOnly(false) {}
|
||||
void ParameterMetadata::SerializeTo(SerializerElement& element) const {
|
||||
valueTypeMetadata.SerializeTo(element);
|
||||
element.SetAttribute("description", description);
|
||||
if (!longDescription.empty()) {
|
||||
element.SetAttribute("longDescription", longDescription);
|
||||
}
|
||||
if (codeOnly) {
|
||||
element.SetAttribute("codeOnly", codeOnly);
|
||||
}
|
||||
element.SetAttribute("longDescription", longDescription);
|
||||
element.SetAttribute("codeOnly", codeOnly);
|
||||
element.SetAttribute("defaultValue", defaultValue);
|
||||
element.SetAttribute("name", name);
|
||||
}
|
||||
|
||||
@@ -29,6 +26,7 @@ void ParameterMetadata::UnserializeFrom(const SerializerElement& element) {
|
||||
description = element.GetStringAttribute("description");
|
||||
longDescription = element.GetStringAttribute("longDescription");
|
||||
codeOnly = element.GetBoolAttribute("codeOnly");
|
||||
defaultValue = element.GetStringAttribute("defaultValue");
|
||||
name = element.GetStringAttribute("name");
|
||||
}
|
||||
|
||||
|
@@ -144,15 +144,13 @@ class GD_CORE_API ParameterMetadata {
|
||||
/**
|
||||
* \brief Get the default value for the parameter.
|
||||
*/
|
||||
const gd::String &GetDefaultValue() const {
|
||||
return valueTypeMetadata.GetDefaultValue();
|
||||
}
|
||||
const gd::String &GetDefaultValue() const { return defaultValue; }
|
||||
|
||||
/**
|
||||
* \brief Set the default value, if the parameter is optional.
|
||||
*/
|
||||
ParameterMetadata &SetDefaultValue(const gd::String &defaultValue_) {
|
||||
valueTypeMetadata.SetDefaultValue(defaultValue_);
|
||||
defaultValue = defaultValue_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -238,6 +236,8 @@ class GD_CORE_API ParameterMetadata {
|
||||
private:
|
||||
gd::ValueTypeMetadata valueTypeMetadata; ///< Parameter type
|
||||
gd::String longDescription; ///< Long description shown in the editor.
|
||||
gd::String defaultValue; ///< Used as a default value in editor or if an
|
||||
///< optional parameter is empty.
|
||||
gd::String name; ///< The name of the parameter to be used in code
|
||||
///< generation. Optional.
|
||||
};
|
||||
|
@@ -18,16 +18,12 @@ EventsBasedBehavior::EventsBasedBehavior()
|
||||
void EventsBasedBehavior::SerializeTo(SerializerElement& element) const {
|
||||
AbstractEventsBasedEntity::SerializeTo(element);
|
||||
element.SetAttribute("objectType", objectType);
|
||||
if (isPrivate) {
|
||||
element.SetBoolAttribute("private", isPrivate);
|
||||
}
|
||||
}
|
||||
|
||||
void EventsBasedBehavior::UnserializeFrom(gd::Project& project,
|
||||
const SerializerElement& element) {
|
||||
AbstractEventsBasedEntity::UnserializeFrom(project, element);
|
||||
objectType = element.GetStringAttribute("objectType");
|
||||
isPrivate = element.GetBoolAttribute("private");
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -73,21 +73,6 @@ class GD_CORE_API EventsBasedBehavior: public AbstractEventsBasedEntity {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Check if the behavior is private - it can't be used outside of its
|
||||
* extension.
|
||||
*/
|
||||
bool IsPrivate() { return isPrivate; }
|
||||
|
||||
/**
|
||||
* \brief Set that the behavior is private - it can't be used outside of its
|
||||
* extension.
|
||||
*/
|
||||
EventsBasedBehavior& SetPrivate(bool _isPrivate) {
|
||||
isPrivate = _isPrivate;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
|
||||
void UnserializeFrom(gd::Project& project,
|
||||
@@ -95,7 +80,6 @@ class GD_CORE_API EventsBasedBehavior: public AbstractEventsBasedEntity {
|
||||
|
||||
private:
|
||||
gd::String objectType;
|
||||
bool isPrivate = false;
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -59,19 +59,11 @@ const std::vector<gd::ParameterMetadata>& EventsFunction::GetParametersForEvents
|
||||
void EventsFunction::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("name", name);
|
||||
element.SetAttribute("fullName", fullName);
|
||||
if (!description.empty()) {
|
||||
element.SetAttribute("description", description);
|
||||
}
|
||||
element.SetAttribute("description", description);
|
||||
element.SetAttribute("sentence", sentence);
|
||||
if (!group.empty()) {
|
||||
element.SetAttribute("group", group);
|
||||
}
|
||||
if (!getterName.empty()) {
|
||||
element.SetAttribute("getterName", getterName);
|
||||
}
|
||||
if (isPrivate) {
|
||||
element.SetBoolAttribute("private", isPrivate);
|
||||
}
|
||||
element.SetAttribute("group", group);
|
||||
element.SetAttribute("getterName", getterName);
|
||||
element.SetBoolAttribute("private", isPrivate);
|
||||
events.SerializeTo(element.AddChild("events"));
|
||||
|
||||
gd::String functionTypeStr = "Action";
|
||||
|
@@ -62,6 +62,16 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
return object;
|
||||
}
|
||||
|
||||
function getAnchorBehavior(object) {
|
||||
const behavior = object.getBehavior(anchorBehaviorName);
|
||||
if (!(behavior instanceof gdjs.AnchorRuntimeBehavior)) {
|
||||
throw new Error(
|
||||
'Expected behavior to be an instance of gdjs.AnchorBehavior'
|
||||
);
|
||||
}
|
||||
return behavior;
|
||||
}
|
||||
|
||||
describe('(anchor horizontal edge)', function () {
|
||||
['rightEdgeAnchor', 'leftEdgeAnchor'].forEach((objectEdge) => {
|
||||
it(`anchors the ${objectEdge} edge of object to window left (fixed)`, function () {
|
||||
|
@@ -239,14 +239,14 @@ describe('gdjs.LinksManager', function () {
|
||||
manager.removeAllLinksOf(object1A);
|
||||
manager.removeAllLinksOf(object1A);
|
||||
{
|
||||
const { pickedSomething } = pickObjectsLinkedTo(
|
||||
const { pickedSomething, objectsLists } = pickObjectsLinkedTo(
|
||||
object1A,
|
||||
Hashtable.newFrom({ obj2: [object2A, object2B, object2C] })
|
||||
);
|
||||
expect(pickedSomething).to.be(false);
|
||||
}
|
||||
{
|
||||
const { pickedSomething } = pickObjectsLinkedTo(
|
||||
const { pickedSomething, objectsLists } = pickObjectsLinkedTo(
|
||||
object2A,
|
||||
Hashtable.newFrom({ obj1: [object1A, object1B, object1C] })
|
||||
);
|
||||
|
@@ -648,19 +648,6 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddScopedAction("FollowCurrentPlatform",
|
||||
_("Follow the floor"),
|
||||
_("Move the object to follow the platform it's currently"
|
||||
"on if any. This action allows to avoid the 1-frame "
|
||||
"delay induced by the automatic following."),
|
||||
_("Move _PARAM0_ to follow the floor"),
|
||||
_(""),
|
||||
"CppPlatform/Extensions/platformerobjecticon.png",
|
||||
"CppPlatform/Extensions/platformerobjecticon.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "PlatformerObjectBehavior")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddCondition("CurrentSpeed",
|
||||
_("Current horizontal speed"),
|
||||
_("Compare the current horizontal speed of the object "
|
||||
|
@@ -131,14 +131,10 @@ class PlatformBehaviorJsExtension : public gd::PlatformExtension {
|
||||
"getCurrentJumpSpeed");
|
||||
autExpressions["CurrentJumpSpeed"].SetFunctionName("getCurrentJumpSpeed");
|
||||
autActions["PlatformBehavior::SetCanJump"].SetFunctionName("setCanJump");
|
||||
autActions["PlatformBehavior::PlatformerObjectBehavior::SetCanNotAirJump"]
|
||||
.SetFunctionName("setCanNotAirJump");
|
||||
autActions["PlatformBehavior::PlatformerObjectBehavior::AbortJump"]
|
||||
.SetFunctionName("abortJump");
|
||||
autActions
|
||||
["PlatformBehavior::PlatformerObjectBehavior::FollowCurrentPlatform"]
|
||||
.SetFunctionName("followCurrentPlatformIfAny");
|
||||
autConditions["PlatformBehavior::CanJump"].SetFunctionName("canJump");
|
||||
autActions["PlatformBehavior::PlatformerObjectBehavior::SetCanNotAirJump"].SetFunctionName("setCanNotAirJump");
|
||||
autActions["PlatformBehavior::PlatformerObjectBehavior::AbortJump"].SetFunctionName("abortJump");
|
||||
autConditions["PlatformBehavior::CanJump"].SetFunctionName(
|
||||
"canJump");
|
||||
autActions["PlatformBehavior::SimulateLeftKey"].SetFunctionName(
|
||||
"simulateLeftKey");
|
||||
autActions["PlatformBehavior::SimulateRightKey"].SetFunctionName(
|
||||
|
@@ -1143,14 +1143,6 @@ namespace gdjs {
|
||||
return this._gravity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum angle of a slope for the Platformer Object to run on it as a floor.
|
||||
* @returns the slope maximum angle, in degrees.
|
||||
*/
|
||||
getSlopeMaxAngle(): float {
|
||||
return this._slopeMaxAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum falling speed of the Platformer Object.
|
||||
* @returns The maximum falling speed.
|
||||
@@ -1592,17 +1584,6 @@ namespace gdjs {
|
||||
this._currentFallSpeed !== 0
|
||||
);
|
||||
}
|
||||
|
||||
followCurrentPlatformIfAny(): boolean {
|
||||
let hasMoved = false;
|
||||
if (this.isOnFloor()) {
|
||||
const timeDelta = this.owner.getElapsedTime() / 1000;
|
||||
const hasMovedX = this._onFloor.followCurrentPlatformOnX(timeDelta);
|
||||
const hasMovedY = this._onFloor.followCurrentPlatformOnY(timeDelta);
|
||||
hasMoved = hasMovedX || hasMovedY;
|
||||
}
|
||||
return hasMoved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1677,11 +1658,6 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
beforeUpdatingObstacles(timeDelta: float) {
|
||||
this.followCurrentPlatformOnY(timeDelta);
|
||||
}
|
||||
|
||||
followCurrentPlatformOnY(timeDelta: float): boolean {
|
||||
let hasMoved = false;
|
||||
const object = this._behavior.owner;
|
||||
//Stick the object to the floor if its height has changed.
|
||||
if (this._oldHeight !== object.getHeight()) {
|
||||
@@ -1690,7 +1666,6 @@ namespace gdjs {
|
||||
object.getHeight() +
|
||||
(object.getY() - object.getDrawableY())
|
||||
);
|
||||
hasMoved = true;
|
||||
}
|
||||
// Directly follow the floor movement on the Y axis by moving the character.
|
||||
// For the X axis, we follow the floor movement using `_requestedDeltaX`
|
||||
@@ -1711,18 +1686,14 @@ namespace gdjs {
|
||||
// and the platform can go out of the spatial search rectangle
|
||||
// even though they are next to each other, which means
|
||||
// that the character will fall.
|
||||
const floorY = this._floorPlatform!.owner.getY();
|
||||
const deltaY = floorY - this._floorLastY;
|
||||
const deltaY = this._floorPlatform!.owner.getY() - this._floorLastY;
|
||||
if (
|
||||
deltaY !== 0 &&
|
||||
Math.abs(deltaY) <=
|
||||
Math.abs(this._behavior._maxFallingSpeed * timeDelta)
|
||||
) {
|
||||
object.setY(object.getY() + deltaY);
|
||||
this._floorLastY = floorY;
|
||||
hasMoved = true;
|
||||
}
|
||||
return hasMoved;
|
||||
}
|
||||
|
||||
checkTransitionBeforeX() {
|
||||
@@ -1760,29 +1731,11 @@ namespace gdjs {
|
||||
beforeMovingX() {
|
||||
const behavior = this._behavior;
|
||||
// Shift the object according to the floor movement.
|
||||
const floorX = this._floorPlatform!.owner.getX();
|
||||
const deltaX = floorX - this._floorLastX;
|
||||
behavior._requestedDeltaX += deltaX;
|
||||
this._floorLastX = floorX;
|
||||
behavior._requestedDeltaX +=
|
||||
this._floorPlatform!.owner.getX() - this._floorLastX;
|
||||
// See `beforeUpdatingObstacles` for the logic for the Y axis.
|
||||
}
|
||||
|
||||
followCurrentPlatformOnX(timeDelta: float): boolean {
|
||||
let hasMoved = false;
|
||||
const object = this._behavior.owner;
|
||||
|
||||
// Shift the object according to the floor movement.
|
||||
const floorX = this._floorPlatform!.owner.getX();
|
||||
const deltaX = floorX - this._floorLastX;
|
||||
if (deltaX !== 0) {
|
||||
console.log(deltaX);
|
||||
object.setX(object.getX() + deltaX);
|
||||
this._floorLastX = floorX;
|
||||
hasMoved = true;
|
||||
}
|
||||
return hasMoved;
|
||||
}
|
||||
|
||||
checkTransitionBeforeY(timeDelta: float) {
|
||||
const behavior = this._behavior;
|
||||
// Go on a ladder
|
||||
|
@@ -404,9 +404,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
|
||||
// Make the platform under the character feet smaller.
|
||||
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
|
||||
object.setCustomWidthAndHeight(object.getWidth(), 9);
|
||||
});
|
||||
object.setCustomWidthAndHeight(object.getWidth(), 9);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
@@ -681,11 +679,18 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
expect(object.getY()).to.be.within(140.6297999, 140.6298001);
|
||||
|
||||
// Move the platform by 6 pixels to the right.
|
||||
for (let index = 0; index < 6; index++) {
|
||||
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
|
||||
platform.setX(platform.getX() + 1);
|
||||
});
|
||||
}
|
||||
platform.setX(platform.getX() + 1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
platform.setX(platform.getX() + 1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
platform.setX(platform.getX() + 1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
platform.setX(platform.getX() + 1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
platform.setX(platform.getX() + 1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
platform.setX(platform.getX() + 1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
// Ensure the object followed the platform on the X axis.
|
||||
// If the floating point errors caused oscillations between two Y positions,
|
||||
@@ -694,9 +699,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
|
||||
expect(object.getY()).to.be.within(140.6297999, 140.6298001);
|
||||
// The floor following has a 1-frame delay.
|
||||
expect(object.getX()).to.be(5);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getX()).to.be(6);
|
||||
});
|
||||
});
|
||||
|
@@ -59,18 +59,13 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
|
||||
// Check that the object follow the platform, even if the
|
||||
// movement is less than one pixel.
|
||||
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
|
||||
platform.setX(platform.getX() + 0.12);
|
||||
});
|
||||
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
|
||||
platform.setX(platform.getX() + 0.12);
|
||||
});
|
||||
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
|
||||
platform.setX(platform.getX() + 0.12);
|
||||
});
|
||||
// The floor following has a 1-frame delay.
|
||||
expect(object.getX()).to.be(0.24);
|
||||
platform.setX(platform.getX() + 0.12);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
platform.setX(platform.getX() + 0.12);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
platform.setX(platform.getX() + 0.12);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
expect(object.getX()).to.be(0.36);
|
||||
});
|
||||
|
||||
@@ -245,28 +240,22 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
// Check that the object follow the platform, even if the
|
||||
// movement is less than one pixel.
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
const previousPlatformY = platform.getY();
|
||||
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
|
||||
platform.setPosition(
|
||||
platform.getX() + deltaX,
|
||||
platform.getY() + deltaY
|
||||
);
|
||||
});
|
||||
platform.setPosition(
|
||||
platform.getX() + deltaX,
|
||||
platform.getY() + deltaY
|
||||
);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
// The object follow the platform
|
||||
// The rounding error is probably due to a separate call.
|
||||
// TODO Try to make it exact or find why
|
||||
// The floor following has a 1-frame delay.
|
||||
expect(object.getY()).to.be.within(
|
||||
previousPlatformY - object.getHeight() - epsilon,
|
||||
previousPlatformY - object.getHeight() + epsilon
|
||||
platform.getY() - object.getHeight() - epsilon,
|
||||
platform.getY() - object.getHeight() + epsilon
|
||||
);
|
||||
}
|
||||
// The floor following has a 1-frame delay.
|
||||
expect(object.getX()).to.be(0 + 4 * deltaX);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getX()).to.be(0 + 5 * deltaX);
|
||||
});
|
||||
});
|
||||
@@ -389,26 +378,20 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
// Check that the object follow the platform, even if the
|
||||
// movement is less than one pixel.
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
const previousPlatformY = platform.getY();
|
||||
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
|
||||
platform.setPosition(
|
||||
platform.getX() + deltaX,
|
||||
platform.getY() + deltaY
|
||||
);
|
||||
});
|
||||
platform.setPosition(
|
||||
platform.getX() + deltaX,
|
||||
platform.getY() + deltaY
|
||||
);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
// The object must not be inside the platform or it gets stuck
|
||||
// The floor following has a 1-frame delay.
|
||||
expect(object.getY()).to.be.within(
|
||||
previousPlatformY - object.getHeight() - epsilon,
|
||||
previousPlatformY - object.getHeight() + epsilon
|
||||
platform.getY() - object.getHeight() - epsilon,
|
||||
platform.getY() - object.getHeight() + epsilon
|
||||
);
|
||||
}
|
||||
// The floor following has a 1-frame delay.
|
||||
expect(object.getX()).to.be(0 + 4 * deltaX);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getX()).to.be(0 + 5 * deltaX);
|
||||
});
|
||||
});
|
||||
|
@@ -7,7 +7,7 @@
|
||||
},
|
||||
properties: { windowWidth: 800, windowHeight: 600 },
|
||||
});
|
||||
const runtimeScene = new gdjs.TestRuntimeScene(runtimeGame);
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
runtimeScene.loadFromScene({
|
||||
layers: [{ name: '', visibility: true, effects: [] }],
|
||||
variables: [],
|
||||
|
@@ -111,6 +111,7 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
|
||||
const {
|
||||
runtimeScene,
|
||||
gameDomElementContainer,
|
||||
object,
|
||||
} = await setupObjectAndGetDomElementContainer();
|
||||
|
||||
expect(gameDomElementContainer.querySelector('input')).not.to.be(null);
|
||||
|
@@ -170,21 +170,19 @@ namespace gdjs {
|
||||
// Position the input on the container on top of the canvas.
|
||||
workingPoint[0] = canvasLeft;
|
||||
workingPoint[1] = canvasTop;
|
||||
runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
|
||||
workingPoint,
|
||||
const topLeftPageCoordinates = runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
|
||||
workingPoint
|
||||
);
|
||||
const pageLeft = workingPoint[0];
|
||||
const pageTop = workingPoint[1];
|
||||
const pageLeft = topLeftPageCoordinates[0];
|
||||
const pageTop = topLeftPageCoordinates[1];
|
||||
|
||||
workingPoint[0] = canvasRight;
|
||||
workingPoint[1] = canvasBottom;
|
||||
runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
|
||||
workingPoint,
|
||||
const bottomRightPageCoordinates = runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
|
||||
workingPoint
|
||||
);
|
||||
const pageRight = workingPoint[0];
|
||||
const pageBottom = workingPoint[1];
|
||||
const pageRight = bottomRightPageCoordinates[0];
|
||||
const pageBottom = bottomRightPageCoordinates[1];
|
||||
|
||||
const widthInContainer = pageRight - pageLeft;
|
||||
const heightInContainer = pageBottom - pageTop;
|
||||
|
@@ -1,4 +1,6 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Text input object');
|
||||
|
||||
const supportedInputTypes = [
|
||||
'text',
|
||||
'email',
|
||||
|
@@ -124,7 +124,7 @@ describe('gdjs.TileMapCollisionMaskRuntimeObject', function () {
|
||||
index < 200 && tileMap._collisionTileMap.getDimensionX() === 0;
|
||||
index++
|
||||
) {
|
||||
await delay(100);
|
||||
await delay(25);
|
||||
}
|
||||
if (tileMap._collisionTileMap.getDimensionX() === 0) {
|
||||
throw new Error('Timeout reading the tile map JSON file.');
|
||||
|
@@ -1,5 +1,7 @@
|
||||
/// <reference path="helper/TileMapHelper.d.ts" />
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Tilemap object');
|
||||
|
||||
/**
|
||||
* An object that handle hitboxes for a tile map.
|
||||
* @extends gdjs.RuntimeObject
|
||||
@@ -180,7 +182,7 @@ namespace gdjs {
|
||||
updateHitBoxes(): void {
|
||||
this.updateTransformation();
|
||||
// Update the RuntimeObject hitboxes attribute.
|
||||
for (const _ of this._collisionTileMap.getAllHitboxes(
|
||||
for (const hitboxes of this._collisionTileMap.getAllHitboxes(
|
||||
this._collisionMaskTag
|
||||
)) {
|
||||
// RuntimeObject.hitBoxes contains the same polygons instances as the
|
||||
|
@@ -1,6 +1,8 @@
|
||||
/// <reference path="helper/TileMapHelper.d.ts" />
|
||||
/// <reference path="pixi-tilemap/dist/pixi-tilemap.d.ts" />
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Tilemap object');
|
||||
|
||||
/**
|
||||
* The PIXI.js renderer for the Tile map runtime object.
|
||||
*
|
||||
|
@@ -2,6 +2,7 @@
|
||||
namespace gdjs {
|
||||
import PIXI = GlobalPIXIModule.PIXI;
|
||||
|
||||
const logger = new gdjs.Logger('Tilemap object');
|
||||
/**
|
||||
* Displays a Tilemap object (mapeditor.org supported).
|
||||
*/
|
||||
|
@@ -24,6 +24,8 @@ namespace gdjs {
|
||||
private _angle: float = 0;
|
||||
|
||||
//Attributes used when moving
|
||||
private _x: float = 0;
|
||||
private _y: float = 0;
|
||||
private _xVelocity: float = 0;
|
||||
private _yVelocity: float = 0;
|
||||
private _angularSpeed: float = 0;
|
||||
|
@@ -121,62 +121,6 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ") : \"\")";
|
||||
});
|
||||
|
||||
GetAllConditions()["CompareArgumentAsNumber"]
|
||||
.GetCodeExtraInformation()
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
gd::String parameterNameCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator, context, "string",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
|
||||
gd::String operatorCode = codeGenerator.GenerateRelationalOperatorCodes(
|
||||
instruction.GetParameter(1).GetPlainString());
|
||||
|
||||
gd::String operandCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator, context, "number",
|
||||
instruction.GetParameter(2).GetPlainString());
|
||||
|
||||
gd::String resultingBoolean =
|
||||
codeGenerator.GenerateBooleanFullName("conditionTrue", context) +
|
||||
".val";
|
||||
|
||||
return resultingBoolean + " = ((typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"Number(eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ")) || 0 : 0) " + operatorCode + " " +
|
||||
operandCode + ");\n";
|
||||
});
|
||||
|
||||
GetAllConditions()["CompareArgumentAsString"]
|
||||
.GetCodeExtraInformation()
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
gd::String parameterNameCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator, context, "string",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
|
||||
gd::String operatorCode = codeGenerator.GenerateRelationalOperatorCodes(
|
||||
instruction.GetParameter(1).GetPlainString());
|
||||
|
||||
gd::String operandCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator, context, "string",
|
||||
instruction.GetParameter(2).GetPlainString());
|
||||
|
||||
gd::String resultingBoolean =
|
||||
codeGenerator.GenerateBooleanFullName("conditionTrue", context) +
|
||||
".val";
|
||||
|
||||
return resultingBoolean + " = ((typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"\"\" + eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ") : \"\") " + operatorCode + " " +
|
||||
operandCode + ");\n";
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace gdjs
|
||||
} // namespace gdjs
|
||||
|
@@ -55,24 +55,40 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
|
||||
codeGenerator,
|
||||
context,
|
||||
"number",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
|
||||
gd::String operatorCode = codeGenerator.GenerateRelationalOperatorCodes(
|
||||
instruction.GetParameter(1).GetPlainString());
|
||||
instruction.GetParameters()[0].GetPlainString());
|
||||
|
||||
gd::String value2Code =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
context,
|
||||
"number",
|
||||
instruction.GetParameter(2).GetPlainString());
|
||||
instruction.GetParameters()[2].GetPlainString());
|
||||
|
||||
gd::String resultingBoolean =
|
||||
codeGenerator.GenerateBooleanFullName("conditionTrue", context) +
|
||||
".val";
|
||||
|
||||
return resultingBoolean + " = (" + value1Code + " " + operatorCode +
|
||||
" " + value2Code + ");\n";
|
||||
if (instruction.GetParameters()[1].GetPlainString() == "=" ||
|
||||
instruction.GetParameters()[1].GetPlainString().empty())
|
||||
return resultingBoolean + " = (" + value1Code + " == " + value2Code +
|
||||
");\n";
|
||||
else if (instruction.GetParameters()[1].GetPlainString() == ">")
|
||||
return resultingBoolean + " = (" + value1Code + " > " + value2Code +
|
||||
");\n";
|
||||
else if (instruction.GetParameters()[1].GetPlainString() == "<")
|
||||
return resultingBoolean + " = (" + value1Code + " < " + value2Code +
|
||||
");\n";
|
||||
else if (instruction.GetParameters()[1].GetPlainString() == "<=")
|
||||
return resultingBoolean + " = (" + value1Code + " <= " + value2Code +
|
||||
");\n";
|
||||
else if (instruction.GetParameters()[1].GetPlainString() == ">=")
|
||||
return resultingBoolean + " = (" + value1Code + " >= " + value2Code +
|
||||
");\n";
|
||||
else if (instruction.GetParameters()[1].GetPlainString() == "!=")
|
||||
return resultingBoolean + " = (" + value1Code + " != " + value2Code +
|
||||
");\n";
|
||||
|
||||
return gd::String("");
|
||||
});
|
||||
GetAllConditions()["BuiltinCommonInstructions::CompareNumbers"]
|
||||
.codeExtraInformation = GetAllConditions()["Egal"].codeExtraInformation;
|
||||
@@ -86,24 +102,27 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
|
||||
codeGenerator,
|
||||
context,
|
||||
"string",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
|
||||
gd::String operatorCode = codeGenerator.GenerateRelationalOperatorCodes(
|
||||
instruction.GetParameter(1).GetPlainString());
|
||||
instruction.GetParameters()[0].GetPlainString());
|
||||
|
||||
gd::String value2Code =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
context,
|
||||
"string",
|
||||
instruction.GetParameter(2).GetPlainString());
|
||||
instruction.GetParameters()[2].GetPlainString());
|
||||
|
||||
gd::String resultingBoolean =
|
||||
codeGenerator.GenerateBooleanFullName("conditionTrue", context) +
|
||||
".val";
|
||||
|
||||
return resultingBoolean + " = (" + value1Code + " " + operatorCode +
|
||||
" " + value2Code + ");\n";
|
||||
if (instruction.GetParameters()[1].GetPlainString() == "=")
|
||||
return resultingBoolean + " = (" + value1Code + " == " + value2Code +
|
||||
");\n";
|
||||
else if (instruction.GetParameters()[1].GetPlainString() == "!=")
|
||||
return resultingBoolean + " = (" + value1Code + " != " + value2Code +
|
||||
");\n";
|
||||
|
||||
return gd::String("");
|
||||
});
|
||||
GetAllConditions()["BuiltinCommonInstructions::CompareStrings"]
|
||||
.codeExtraInformation =
|
||||
|
@@ -46,7 +46,6 @@ SpriteExtension::SpriteExtension() {
|
||||
spriteConditions["Direction"].SetFunctionName("getDirectionOrAngle");
|
||||
spriteConditions["Sprite"].SetFunctionName("getAnimationFrame");
|
||||
spriteConditions["AnimationEnded"].SetFunctionName("hasAnimationEnded");
|
||||
spriteConditions["AnimationEnded2"].SetFunctionName("hasAnimationEnded2");
|
||||
spriteActions["PauseAnimation"].SetFunctionName("pauseAnimation");
|
||||
spriteActions["PlayAnimation"].SetFunctionName("playAnimation");
|
||||
spriteConditions["AnimStopped"].SetFunctionName("animationPaused");
|
||||
|
@@ -265,6 +265,7 @@ namespace gdjs {
|
||||
cy *= absScaleY;
|
||||
|
||||
// Rotation
|
||||
const oldX = x;
|
||||
const angleInRadians = (this.angle / 180) * Math.PI;
|
||||
const cosValue = Math.cos(angleInRadians);
|
||||
const sinValue = Math.sin(angleInRadians);
|
||||
|
@@ -5,6 +5,9 @@
|
||||
*/
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('CustomRuntimeObject');
|
||||
const setupWarningLogger = new gdjs.Logger(
|
||||
'CustomRuntimeObject (setup warnings)'
|
||||
);
|
||||
|
||||
/**
|
||||
* The instance container of a custom object, containing instances of objects rendered on screen.
|
||||
@@ -197,8 +200,12 @@ namespace gdjs {
|
||||
*/
|
||||
_updateObjectsPreRender() {
|
||||
const allInstancesList = this.getAdhocListOfAllInstances();
|
||||
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
|
||||
const object = allInstancesList[i];
|
||||
for (
|
||||
let i = 0, len = this.getAdhocListOfAllInstances().length;
|
||||
i < len;
|
||||
++i
|
||||
) {
|
||||
const object = this.getAdhocListOfAllInstances()[i];
|
||||
const rendererObject = object.getRendererObject();
|
||||
if (rendererObject) {
|
||||
rendererObject.visible = !object.isHidden();
|
||||
@@ -215,7 +222,7 @@ namespace gdjs {
|
||||
// to see what is rendered).
|
||||
if (this._debugDrawEnabled) {
|
||||
this._debuggerRenderer.renderDebugDraw(
|
||||
allInstancesList,
|
||||
this.getAdhocListOfAllInstances(),
|
||||
this._debugDrawShowHiddenInstances,
|
||||
this._debugDrawShowPointsNames,
|
||||
this._debugDrawShowCustomPoints
|
||||
|
@@ -5,6 +5,9 @@
|
||||
*/
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('RuntimeInstanceContainer');
|
||||
const setupWarningLogger = new gdjs.Logger(
|
||||
'RuntimeInstanceContainer (setup warnings)'
|
||||
);
|
||||
|
||||
/**
|
||||
* A container of object instances rendered on screen.
|
||||
|
@@ -6,6 +6,8 @@
|
||||
namespace gdjs {
|
||||
export namespace evtTools {
|
||||
export namespace network {
|
||||
const logger = new gdjs.Logger('Network requests');
|
||||
|
||||
/**
|
||||
* Send an asynchronous request to the specified URL, with the specified (text)
|
||||
* body, method and contentType (defaults to `application/x-www-form-urlencoded`).
|
||||
|
@@ -18,7 +18,6 @@ namespace gdjs {
|
||||
* @memberOf gdjs
|
||||
*/
|
||||
export namespace evtTools {
|
||||
// @ts-ignore - This variable is unused on purpose.
|
||||
const thisIsUnusedButEnsureTheNamespaceIsDeclared = true;
|
||||
}
|
||||
|
||||
|
@@ -409,22 +409,20 @@ namespace gdjs {
|
||||
* Convert a point from the canvas coordinates to the dom element container coordinates.
|
||||
*
|
||||
* @param canvasCoords The point in the canvas coordinates.
|
||||
* @param result The point to return.
|
||||
* @returns The point in the dom element container coordinates.
|
||||
*/
|
||||
convertCanvasToDomElementContainerCoords(
|
||||
canvasCoords: FloatPoint,
|
||||
result: FloatPoint
|
||||
canvasCoords: FloatPoint
|
||||
): FloatPoint {
|
||||
const pageCoords = result || [0, 0];
|
||||
const pageCoords: FloatPoint = [0, 0];
|
||||
const gameResolutionWidth = this._game.getGameResolutionWidth();
|
||||
const canvasWidth = this._canvasWidth || 1;
|
||||
const gameResolutionHeight = this._game.getGameResolutionHeight();
|
||||
const canvasHeight = this._canvasHeight || 1;
|
||||
|
||||
// Handle the fact that the game is stretched to fill the canvas.
|
||||
pageCoords[0] =
|
||||
(canvasCoords[0] * this._canvasWidth) /
|
||||
this._game.getGameResolutionWidth();
|
||||
pageCoords[1] =
|
||||
(canvasCoords[1] * this._canvasHeight) /
|
||||
this._game.getGameResolutionHeight();
|
||||
pageCoords[0] = (canvasCoords[0] * canvasWidth) / gameResolutionWidth;
|
||||
pageCoords[1] = (canvasCoords[1] * canvasHeight) / gameResolutionHeight;
|
||||
|
||||
return pageCoords;
|
||||
}
|
||||
@@ -713,6 +711,7 @@ namespace gdjs {
|
||||
e.preventDefault();
|
||||
if (e.changedTouches) {
|
||||
for (let i = 0; i < e.changedTouches.length; ++i) {
|
||||
const pos = getEventPosition(e.changedTouches[i]);
|
||||
manager.onTouchEnd(e.changedTouches[i].identifier);
|
||||
}
|
||||
}
|
||||
|
@@ -393,6 +393,8 @@ namespace gdjs {
|
||||
const windowInnerHeight = gdjs.RuntimeGameRenderer.getWindowInnerHeight();
|
||||
|
||||
// Enlarge either the width or the eight to fill the inner window space.
|
||||
let width = this._gameResolutionWidth;
|
||||
let height = this._gameResolutionHeight;
|
||||
if (this._resizeMode === 'adaptWidth') {
|
||||
this._gameResolutionWidth =
|
||||
(this._gameResolutionHeight * windowInnerWidth) /
|
||||
|
@@ -471,19 +471,14 @@ namespace gdjs {
|
||||
];
|
||||
const oldFrame = this._currentFrame;
|
||||
|
||||
const elapsedTime = this.getElapsedTime() / 1000;
|
||||
this._frameElapsedTime += this._animationPaused
|
||||
? 0
|
||||
: elapsedTime * this._animationSpeedScale;
|
||||
|
||||
if (
|
||||
!direction.loop &&
|
||||
this._currentFrame >= direction.frames.length - 1 &&
|
||||
this._frameElapsedTime > direction.timeBetweenFrames
|
||||
) {
|
||||
// *Optimization*: Animation is finished, don't change the current frame
|
||||
// and compute nothing more.
|
||||
//*Optimization*: Animation is finished, don't change the current frame
|
||||
//and compute nothing more.
|
||||
if (!direction.loop && this._currentFrame >= direction.frames.length) {
|
||||
} else {
|
||||
const elapsedTime = this.getElapsedTime() / 1000;
|
||||
this._frameElapsedTime += this._animationPaused
|
||||
? 0
|
||||
: elapsedTime * this._animationSpeedScale;
|
||||
if (this._frameElapsedTime > direction.timeBetweenFrames) {
|
||||
const count = Math.floor(
|
||||
this._frameElapsedTime / direction.timeBetweenFrames
|
||||
@@ -755,10 +750,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Return true if animation has ended.
|
||||
* Prefer using hasAnimationEnded2. This method returns true as soon as
|
||||
* the animation enters the last frame, not at the end of the last frame.
|
||||
*/
|
||||
hasAnimationEnded(): boolean {
|
||||
if (
|
||||
@@ -777,33 +769,6 @@ namespace gdjs {
|
||||
return this._currentFrame === direction.frames.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if animation has ended.
|
||||
* The animation had ended if:
|
||||
* - it's not configured as a loop;
|
||||
* - the current frame is the last frame;
|
||||
* - the last frame has been displayed long enough.
|
||||
*/
|
||||
hasAnimationEnded2(): boolean {
|
||||
if (
|
||||
this._currentAnimation >= this._animations.length ||
|
||||
this._currentDirection >=
|
||||
this._animations[this._currentAnimation].directions.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const direction = this._animations[this._currentAnimation].directions[
|
||||
this._currentDirection
|
||||
];
|
||||
if (direction.loop) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
this._currentFrame === direction.frames.length - 1 &&
|
||||
this._frameElapsedTime > direction.timeBetweenFrames
|
||||
);
|
||||
}
|
||||
|
||||
animationPaused() {
|
||||
return this._animationPaused;
|
||||
}
|
||||
|
@@ -567,14 +567,12 @@ describe('gdjs.CustomRuntimeObject', function () {
|
||||
/** @type {gdjs.CustomRuntimeObject} */
|
||||
let topCustomObject;
|
||||
/** @type {gdjs.RuntimeObject} */
|
||||
// @ts-ignore - we do not use this variable
|
||||
let topLeftSprite;
|
||||
/** @type {gdjs.RuntimeObject} */
|
||||
let topRightSprite;
|
||||
/** @type {gdjs.CustomRuntimeObject} */
|
||||
let bottomCustomObject;
|
||||
/** @type {gdjs.RuntimeObject} */
|
||||
// @ts-ignore - we do not use this variable
|
||||
let bottomLeftSprite;
|
||||
/** @type {gdjs.RuntimeObject} */
|
||||
let bottomRightSprite;
|
||||
|
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* A RuntimeScene that allows to test events side effects.
|
||||
*/
|
||||
gdjs.TestRuntimeScene = class TestRuntimeScene extends gdjs.RuntimeScene {
|
||||
|
||||
/**
|
||||
* @param {gdjs.RuntimeGame} runtimeGame
|
||||
*/
|
||||
constructor(runtimeGame) {
|
||||
super(runtimeGame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} elapsedTime
|
||||
* @param {() => void} eventsFunction
|
||||
*/
|
||||
renderAndStepWithEventsFunction(elapsedTime, eventsFunction) {
|
||||
const runtimeScene = this;
|
||||
this._eventsFunction = (runtimeScene) => eventsFunction();
|
||||
this.renderAndStep(elapsedTime);
|
||||
this._eventsFunction = null;
|
||||
}
|
||||
};
|
@@ -1579,9 +1579,6 @@ interface BehaviorMetadata {
|
||||
|
||||
[Ref] BehaviorMetadata SetObjectType([Const] DOMString objectType);
|
||||
[Const, Ref] DOMString GetObjectType();
|
||||
|
||||
boolean IsPrivate();
|
||||
[Ref] BehaviorMetadata SetPrivate();
|
||||
|
||||
[Ref] Behavior Get();
|
||||
BehaviorsSharedData GetSharedDataInstance();
|
||||
@@ -2355,8 +2352,6 @@ interface EventsBasedBehavior {
|
||||
[Const, Ref] DOMString GetFullName();
|
||||
[Ref] EventsBasedBehavior SetObjectType([Const] DOMString fullName);
|
||||
[Const, Ref] DOMString GetObjectType();
|
||||
[Ref] EventsBasedBehavior SetPrivate(boolean isPrivate);
|
||||
boolean IsPrivate();
|
||||
|
||||
[Ref] EventsFunctionsContainer GetEventsFunctions();
|
||||
[Ref] NamedPropertyDescriptorsList GetPropertyDescriptors();
|
||||
|
@@ -28,8 +28,6 @@ declare class gdBehaviorMetadata {
|
||||
addRequiredFile(resourceFile: string): gdBehaviorMetadata;
|
||||
setObjectType(objectType: string): gdBehaviorMetadata;
|
||||
getObjectType(): string;
|
||||
isPrivate(): boolean;
|
||||
setPrivate(): gdBehaviorMetadata;
|
||||
get(): gdBehavior;
|
||||
getSharedDataInstance(): gdBehaviorsSharedData;
|
||||
delete(): void;
|
||||
|
@@ -9,8 +9,6 @@ declare class gdEventsBasedBehavior {
|
||||
getFullName(): string;
|
||||
setObjectType(fullName: string): gdEventsBasedBehavior;
|
||||
getObjectType(): string;
|
||||
setPrivate(isPrivate: boolean): gdEventsBasedBehavior;
|
||||
isPrivate(): boolean;
|
||||
getEventsFunctions(): gdEventsFunctionsContainer;
|
||||
getPropertyDescriptors(): gdNamedPropertyDescriptorsList;
|
||||
serializeTo(element: gdSerializerElement): void;
|
||||
|
1618
newIDE/app/package-lock.json
generated
1618
newIDE/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@
|
||||
"node-require-function": "^1.2.0",
|
||||
"pixi-simple-gesture": "github:4ian/pixi-simple-gesture#v0.3.3",
|
||||
"pixi.js-legacy": "^6.1.2",
|
||||
"posthog-js": "^1.34.0",
|
||||
"posthog-js": "^1.20.4",
|
||||
"prop-types": "^15.5.10",
|
||||
"randomcolor": "^0.5.3",
|
||||
"raven-js": "^3.19.1",
|
||||
@@ -70,7 +70,7 @@
|
||||
"react-dom": "16.14.0",
|
||||
"react-error-boundary": "^1.2.0",
|
||||
"react-json-view": "^1.16.1",
|
||||
"react-markdown": "^6.0.3",
|
||||
"react-markdown": "^4.0.6",
|
||||
"react-measure": "2.3.0",
|
||||
"react-monaco-editor": "^0.18.0",
|
||||
"react-mosaic-component": "github:4ian/react-mosaic#v3.1.0",
|
||||
|
@@ -35,7 +35,6 @@ import { BoxSearchResults } from '../UI/Search/BoxSearchResults';
|
||||
import Link from '../UI/Link';
|
||||
import PrivateAssetsAuthorizationContext from './PrivateAssets/PrivateAssetsAuthorizationContext';
|
||||
import AuthorizedAssetImage from './PrivateAssets/AuthorizedAssetImage';
|
||||
import { MarkdownText } from '../UI/MarkdownText';
|
||||
|
||||
const FIXED_HEIGHT = 250;
|
||||
const FIXED_WIDTH = 300;
|
||||
@@ -411,9 +410,7 @@ export const AssetDetails = ({
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text size="body" displayInlineAsSpan>
|
||||
<MarkdownText source={asset.description} allowParagraphs />
|
||||
</Text>
|
||||
<Text size="body">{asset.description}</Text>
|
||||
</React.Fragment>
|
||||
) : error ? (
|
||||
<PlaceholderError onRetry={loadAsset}>
|
||||
|
@@ -5,8 +5,6 @@ import {
|
||||
type Environment,
|
||||
getPublicAsset,
|
||||
isPixelArt,
|
||||
isPublicAssetResourceUrl,
|
||||
extractFilenameWithExtensionFromPublicAssetResourceUrl,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { unserializeFromJSObject } from '../Utils/Serializer';
|
||||
@@ -74,23 +72,16 @@ export const installResource = (
|
||||
// Check if the resource that must be installed is already present. Use the "origin"
|
||||
// of the resource (if present), otherwise for compatibility we use the URL.
|
||||
const resourceFileUrl: string = serializedResource.file;
|
||||
const resourceOriginRawName: string = serializedResource.origin
|
||||
const resourceOriginName: string = serializedResource.origin
|
||||
? serializedResource.origin.name
|
||||
: '';
|
||||
// We clean up the name of the resource, to avoid having a resource with a name
|
||||
// too long (for instance, a resource with a SHA for public assets).
|
||||
const resourceOriginCleanedName: string = isPublicAssetResourceUrl(
|
||||
resourceFileUrl
|
||||
)
|
||||
? extractFilenameWithExtensionFromPublicAssetResourceUrl(resourceFileUrl)
|
||||
: resourceOriginRawName;
|
||||
const resourceOriginIdentifier: string = serializedResource.origin
|
||||
? serializedResource.origin.identifier
|
||||
: '';
|
||||
const existingResourceNameFromSameOrigin =
|
||||
resourceOriginCleanedName && resourceOriginIdentifier
|
||||
resourceOriginName && resourceOriginIdentifier
|
||||
? resourcesManager.getResourceNameWithOrigin(
|
||||
resourceOriginCleanedName,
|
||||
resourceOriginName,
|
||||
resourceOriginIdentifier
|
||||
)
|
||||
: '';
|
||||
@@ -127,7 +118,7 @@ export const installResource = (
|
||||
newResource.setSmooth(
|
||||
project.getScaleMode() !== 'nearest' && !isPixelArt(asset)
|
||||
);
|
||||
newResource.setOrigin(resourceOriginCleanedName, resourceOriginIdentifier);
|
||||
newResource.setOrigin(resourceOriginName, resourceOriginIdentifier);
|
||||
resourcesManager.addResource(newResource);
|
||||
newResource.delete();
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import * as React from 'react';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
@@ -39,7 +38,6 @@ import Window from '../Utils/Window';
|
||||
import PrivateAssetsAuthorizationContext from './PrivateAssets/PrivateAssetsAuthorizationContext';
|
||||
import { isPrivateAsset } from '../Utils/GDevelopServices/Asset';
|
||||
import useAlertDialog from '../UI/Alert/useAlertDialog';
|
||||
import { translateExtensionCategory } from '../Utils/Extension/ExtensionCategories';
|
||||
const isDev = Window.isDev();
|
||||
|
||||
const ObjectListItem = ({
|
||||
@@ -84,7 +82,6 @@ type Props = {|
|
||||
onCreateNewObject: (type: string) => void,
|
||||
onObjectAddedFromAsset: gdObject => void,
|
||||
canInstallPrivateAsset: () => boolean,
|
||||
i18n: I18nType,
|
||||
|};
|
||||
|
||||
export default function NewObjectDialog({
|
||||
@@ -99,7 +96,6 @@ export default function NewObjectDialog({
|
||||
onCreateNewObject,
|
||||
onObjectAddedFromAsset,
|
||||
canInstallPrivateAsset,
|
||||
i18n,
|
||||
}: Props) {
|
||||
const {
|
||||
setNewObjectDialogDefaultTab,
|
||||
@@ -117,10 +113,7 @@ export default function NewObjectDialog({
|
||||
() => {
|
||||
const objectsByCategory = {};
|
||||
allObjectMetadata.forEach(objectMetadata => {
|
||||
const category = translateExtensionCategory(
|
||||
objectMetadata.categoryFullName,
|
||||
i18n
|
||||
);
|
||||
const category = objectMetadata.categoryFullName;
|
||||
objectsByCategory[category] = [
|
||||
...(objectsByCategory[category] || []),
|
||||
objectMetadata,
|
||||
@@ -128,7 +121,7 @@ export default function NewObjectDialog({
|
||||
});
|
||||
return objectsByCategory;
|
||||
},
|
||||
[allObjectMetadata, i18n]
|
||||
[allObjectMetadata]
|
||||
);
|
||||
|
||||
React.useEffect(() => setNewObjectDialogDefaultTab(currentTab), [
|
||||
|
@@ -13,7 +13,11 @@ import PriceTag, { formatPrice } from '../../UI/PriceTag';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import { ResponsiveLineStackLayout, LineStackLayout } from '../../UI/Layout';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
LineStackLayout,
|
||||
} from '../../UI/Layout';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import {
|
||||
getUserPublicProfile,
|
||||
@@ -28,7 +32,6 @@ import ResponsiveImagesGallery from '../../UI/ResponsiveImagesGallery';
|
||||
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { sendAssetPackBuyClicked } from '../../Utils/Analytics/EventSender';
|
||||
import { MarkdownText } from '../../UI/MarkdownText';
|
||||
|
||||
const sortedContentType = [
|
||||
'sprite',
|
||||
@@ -205,7 +208,7 @@ const PrivateAssetPackDialog = ({
|
||||
variant="outlined"
|
||||
style={{ padding: windowWidth === 'small' ? 20 : 30 }}
|
||||
>
|
||||
<Column noMargin>
|
||||
<ColumnStackLayout noMargin useLargeSpacer>
|
||||
<Line
|
||||
noMargin
|
||||
expand
|
||||
@@ -215,12 +218,7 @@ const PrivateAssetPackDialog = ({
|
||||
<PriceTag value={prices[0].value} />
|
||||
{getBuyButton(i18n)}
|
||||
</Line>
|
||||
<Text size="body2" displayInlineAsSpan>
|
||||
<MarkdownText
|
||||
source={assetPack.longDescription}
|
||||
allowParagraphs
|
||||
/>
|
||||
</Text>
|
||||
<Text noMargin>{assetPack.longDescription}</Text>
|
||||
<ResponsiveLineStackLayout noMargin noColumnMargin>
|
||||
<Column noMargin expand>
|
||||
<Text size="sub-title">
|
||||
@@ -279,7 +277,7 @@ const PrivateAssetPackDialog = ({
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</ColumnStackLayout>
|
||||
</Paper>
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
|
@@ -14,7 +14,6 @@ type Props = {|
|
||||
value: string,
|
||||
onChange: string => void,
|
||||
disabled?: boolean,
|
||||
eventsFunctionsExtension?: gdEventsFunctionsExtension,
|
||||
|};
|
||||
type State = {|
|
||||
behaviorMetadata: Array<EnumeratedBehaviorMetadata>,
|
||||
@@ -27,8 +26,7 @@ export default class BehaviorTypeSelector extends React.Component<
|
||||
state = {
|
||||
behaviorMetadata: enumerateBehaviorsMetadata(
|
||||
this.props.project.getCurrentPlatform(),
|
||||
this.props.project,
|
||||
this.props.eventsFunctionsExtension
|
||||
this.props.project
|
||||
),
|
||||
};
|
||||
|
||||
|
@@ -15,8 +15,7 @@ export type EnumeratedBehaviorMetadata = {|
|
||||
|
||||
export const enumerateBehaviorsMetadata = (
|
||||
platform: gdPlatform,
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension?: gdEventsFunctionsExtension
|
||||
project: gdProject
|
||||
): Array<EnumeratedBehaviorMetadata> => {
|
||||
const extensionsList = platform.getAllPlatformExtensions();
|
||||
|
||||
@@ -31,12 +30,6 @@ export const enumerateBehaviorsMetadata = (
|
||||
behaviorType,
|
||||
behaviorMetadata: extension.getBehaviorMetadata(behaviorType),
|
||||
}))
|
||||
.filter(
|
||||
({ behaviorMetadata }) =>
|
||||
!behaviorMetadata.isPrivate() ||
|
||||
(eventsFunctionsExtension &&
|
||||
extension.getName() === eventsFunctionsExtension.getName())
|
||||
)
|
||||
.map(({ behaviorType, behaviorMetadata }) => ({
|
||||
extension,
|
||||
behaviorMetadata,
|
||||
|
@@ -82,7 +82,6 @@ const BehaviorListItem = ({
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension?: gdEventsFunctionsExtension,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
open: boolean,
|
||||
@@ -92,7 +91,6 @@ type Props = {|
|
||||
|
||||
export default function NewBehaviorDialog({
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
open,
|
||||
onClose,
|
||||
onChoose,
|
||||
@@ -121,17 +119,13 @@ export default function NewBehaviorDialog({
|
||||
);
|
||||
|
||||
const platform = project.getCurrentPlatform();
|
||||
const behaviorsMetadata: Array<EnumeratedBehaviorMetadata> = React.useMemo(
|
||||
const behaviorMetadata: Array<EnumeratedBehaviorMetadata> = React.useMemo(
|
||||
() => {
|
||||
return project && platform
|
||||
? enumerateBehaviorsMetadata(
|
||||
platform,
|
||||
project,
|
||||
eventsFunctionsExtension
|
||||
)
|
||||
? enumerateBehaviorsMetadata(platform, project)
|
||||
: [];
|
||||
},
|
||||
[project, platform, eventsFunctionsExtension, extensionInstallTime] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[project, platform, extensionInstallTime] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
|
||||
const shouldAutofocusSearchbar = useShouldAutofocusSearchbar();
|
||||
@@ -150,7 +144,7 @@ export default function NewBehaviorDialog({
|
||||
const deprecatedBehaviorsInformation = getDeprecatedBehaviorsInformation();
|
||||
|
||||
const filteredBehaviorMetadata = filterEnumeratedBehaviorMetadata(
|
||||
behaviorsMetadata,
|
||||
behaviorMetadata,
|
||||
searchText
|
||||
);
|
||||
const behaviors = filteredBehaviorMetadata.filter(
|
||||
|
@@ -38,14 +38,12 @@ const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension?: gdEventsFunctionsExtension,
|
||||
object: gdObject,
|
||||
onUpdateBehaviorsSharedData: () => void,
|
||||
onSizeUpdated?: ?() => void,
|
||||
resourceSources: Array<ResourceSource>,
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
onBehaviorsUpdated?: () => void,
|
||||
|};
|
||||
|
||||
const BehaviorsEditor = (props: Props) => {
|
||||
@@ -53,7 +51,7 @@ const BehaviorsEditor = (props: Props) => {
|
||||
false
|
||||
);
|
||||
|
||||
const { object, project, eventsFunctionsExtension } = props;
|
||||
const { object, project } = props;
|
||||
const allBehaviorNames = object.getAllBehaviorNames().toJSArray();
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
@@ -78,7 +76,6 @@ const BehaviorsEditor = (props: Props) => {
|
||||
forceUpdate();
|
||||
if (props.onSizeUpdated) props.onSizeUpdated();
|
||||
props.onUpdateBehaviorsSharedData();
|
||||
if (props.onBehaviorsUpdated) props.onBehaviorsUpdated();
|
||||
};
|
||||
|
||||
const onChangeBehaviorName = (behavior: gdBehavior, newName: string) => {
|
||||
@@ -91,7 +88,6 @@ const BehaviorsEditor = (props: Props) => {
|
||||
if (object.hasBehaviorNamed(newName)) return;
|
||||
object.renameBehavior(behavior.getName(), newName);
|
||||
forceUpdate();
|
||||
if (props.onBehaviorsUpdated) props.onBehaviorsUpdated();
|
||||
};
|
||||
|
||||
const onRemoveBehavior = (behaviorName: string) => {
|
||||
@@ -114,7 +110,6 @@ const BehaviorsEditor = (props: Props) => {
|
||||
dependentBehaviors.forEach(name => object.removeBehavior(name));
|
||||
if (props.onSizeUpdated) props.onSizeUpdated();
|
||||
}
|
||||
if (props.onBehaviorsUpdated) props.onBehaviorsUpdated();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -305,7 +300,6 @@ const BehaviorsEditor = (props: Props) => {
|
||||
onClose={() => setNewBehaviorDialogOpen(false)}
|
||||
onChoose={addBehavior}
|
||||
project={project}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
|
@@ -163,7 +163,8 @@ export class CodeEditor extends React.Component<Props, State> {
|
||||
// (so no need to scroll horizontally
|
||||
// on small code editors) or at 80 columns max
|
||||
// (as a good practice).
|
||||
wordWrap: 'on',
|
||||
wordWrap: 'bounded',
|
||||
wordWrapColumn: 80,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@@ -346,19 +346,6 @@ export default class EventsBasedBehaviorPropertiesEditor extends React.Component
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
{property.getType() === 'Color' && (
|
||||
<ColorField
|
||||
floatingLabelText={<Trans>Default value</Trans>}
|
||||
disableAlpha
|
||||
fullWidth
|
||||
color={property.getValue()}
|
||||
onChange={color => {
|
||||
property.setValue(color);
|
||||
this.forceUpdate();
|
||||
this.props.onPropertiesUpdated();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{property.getType() === 'Choice' && (
|
||||
<SelectField
|
||||
floatingLabelText={<Trans>Default value</Trans>}
|
||||
@@ -388,6 +375,19 @@ export default class EventsBasedBehaviorPropertiesEditor extends React.Component
|
||||
setExtraInfo={this._setChoiceExtraInfo(property)}
|
||||
/>
|
||||
)}
|
||||
{property.getType() === 'Color' && (
|
||||
<ColorField
|
||||
floatingLabelText={<Trans>Color</Trans>}
|
||||
disableAlpha
|
||||
fullWidth
|
||||
color={property.getValue()}
|
||||
onChange={color => {
|
||||
property.setValue(color);
|
||||
this.forceUpdate();
|
||||
this.props.onPropertiesUpdated();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
|
@@ -22,8 +22,6 @@ import {
|
||||
unserializeFromJSObject,
|
||||
} from '../Utils/Serializer';
|
||||
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
|
||||
|
||||
const EVENTS_BASED_BEHAVIOR_CLIPBOARD_KIND = 'Events Based Behavior';
|
||||
|
||||
@@ -31,29 +29,8 @@ const styles = {
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
tooltip: { marginRight: 5, verticalAlign: 'bottom' },
|
||||
};
|
||||
|
||||
const renderEventsBehaviorLabel = (
|
||||
eventsBasedBehavior: gdEventsBasedBehavior
|
||||
) =>
|
||||
eventsBasedBehavior.isPrivate() ? (
|
||||
<>
|
||||
<Tooltip
|
||||
title={
|
||||
<Trans>This behavior won't be visible in the events editor.</Trans>
|
||||
}
|
||||
>
|
||||
<VisibilityOffIcon fontSize="small" style={styles.tooltip} />
|
||||
</Tooltip>
|
||||
<span title={eventsBasedBehavior.getName()}>
|
||||
{eventsBasedBehavior.getName()}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
eventsBasedBehavior.getName()
|
||||
);
|
||||
|
||||
type State = {|
|
||||
renamedEventsBasedBehavior: ?gdEventsBasedBehavior,
|
||||
searchText: string,
|
||||
@@ -184,11 +161,6 @@ export default class EventsBasedBehaviorsList extends React.Component<
|
||||
this.forceUpdateList();
|
||||
};
|
||||
|
||||
_togglePrivate = (eventsBasedBehavior: gdEventsBasedBehavior) => {
|
||||
eventsBasedBehavior.setPrivate(!eventsBasedBehavior.isPrivate());
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
forceUpdateList = () => {
|
||||
this._onEventsBasedBehaviorModified();
|
||||
if (this.sortableList) this.sortableList.forceUpdateGrid();
|
||||
@@ -268,12 +240,6 @@ export default class EventsBasedBehaviorsList extends React.Component<
|
||||
askForConfirmation: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: eventsBasedBehavior.isPrivate()
|
||||
? i18n._(t`Make public`)
|
||||
: i18n._(t`Make private`),
|
||||
click: () => this._togglePrivate(eventsBasedBehavior),
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
@@ -349,7 +315,6 @@ export default class EventsBasedBehaviorsList extends React.Component<
|
||||
height={height}
|
||||
onAddNewItem={this._addNewEventsBasedBehavior}
|
||||
addNewItemLabel={<Trans>Add a new behavior</Trans>}
|
||||
renderItemLabel={renderEventsBehaviorLabel}
|
||||
getItemName={getEventsBasedBehaviorName}
|
||||
selectedItems={
|
||||
selectedEventsBasedBehavior
|
||||
|
@@ -192,7 +192,7 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
};
|
||||
|
||||
render() {
|
||||
const { eventsBasedObject, project, eventsFunctionsExtension } = this.props;
|
||||
const { eventsBasedObject, project } = this.props;
|
||||
|
||||
// TODO EBO When adding an object, filter the object types to excludes
|
||||
// object that depend (transitively) on this object to avoid cycles.
|
||||
@@ -260,7 +260,6 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
object={this.state.editedObjectWithContext.object}
|
||||
initialTab={this.state.editedObjectInitialTab}
|
||||
project={project}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
resourceSources={[]}
|
||||
resourceExternalEditors={[]}
|
||||
onChooseResource={() => Promise.resolve([])}
|
||||
|
@@ -8,7 +8,6 @@ import TextField from '../UI/TextField';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import { Tabs, Tab } from '../UI/Tabs';
|
||||
import DismissableAlertMessage from '../UI/DismissableAlertMessage';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import EventsBasedObjectPropertiesEditor from './EventsBasedObjectPropertiesEditor';
|
||||
import EventBasedObjectChildrenEditor from './EventBasedObjectChildrenEditor';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
@@ -70,13 +69,6 @@ export default class EventsBasedObjectEditor extends React.Component<
|
||||
<Line expand useFullHeight>
|
||||
{currentTab === 'configuration' && (
|
||||
<ColumnStackLayout expand>
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
The custom object editor is at a very early stage. A lot of
|
||||
features are missing or broken. Extensions written with it may
|
||||
no longer work in future GDevelop releases.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
<DismissableAlertMessage
|
||||
identifier="events-based-object-explanation"
|
||||
kind="info"
|
||||
|
@@ -202,67 +202,54 @@ export const ExtensionOptionsEditor = ({
|
||||
eventsFunctionsExtension.setCategory(category);
|
||||
forceUpdate();
|
||||
}}
|
||||
// TODO Sort by translated value.
|
||||
dataSource={[
|
||||
{
|
||||
text: '',
|
||||
value: 'General',
|
||||
translatableValue: 'General',
|
||||
},
|
||||
{
|
||||
text: 'Ads',
|
||||
value: 'Ads',
|
||||
translatableValue: 'Ads',
|
||||
},
|
||||
{
|
||||
text: 'Visual effect',
|
||||
value: 'Visual effect',
|
||||
translatableValue: 'Visual effect',
|
||||
},
|
||||
{
|
||||
text: 'Audio',
|
||||
value: 'Audio',
|
||||
translatableValue: 'Audio',
|
||||
},
|
||||
{
|
||||
text: 'Advanced',
|
||||
value: 'Advanced',
|
||||
translatableValue: 'Advanced',
|
||||
},
|
||||
{
|
||||
text: 'Camera',
|
||||
value: 'Camera',
|
||||
translatableValue: 'Camera',
|
||||
},
|
||||
{
|
||||
text: 'Input',
|
||||
value: 'Input',
|
||||
translatableValue: 'Input',
|
||||
},
|
||||
{
|
||||
text: 'Game mechanic',
|
||||
value: 'Game mechanic',
|
||||
translatableValue: 'Game mechanic',
|
||||
},
|
||||
{
|
||||
text: 'Movement',
|
||||
value: 'Movement',
|
||||
translatableValue: 'Movement',
|
||||
},
|
||||
{
|
||||
text: 'Network',
|
||||
value: 'Network',
|
||||
translatableValue: 'Network',
|
||||
},
|
||||
{
|
||||
text: 'Third-party',
|
||||
value: 'Third-party',
|
||||
translatableValue: 'Third-party',
|
||||
},
|
||||
{
|
||||
text: 'User interface',
|
||||
value: 'User interface',
|
||||
translatableValue: 'User interface',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
@@ -46,9 +46,12 @@ import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import { ParametersIndexOffsets } from '../EventsFunctionsExtensionsLoader';
|
||||
import { sendEventsExtractedAsFunction } from '../Utils/Analytics/EventSender';
|
||||
import Window from '../Utils/Window';
|
||||
import { type OnFetchNewlyAddedResourcesFunction } from '../ProjectsStorage/ResourceFetcher';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const isDev = Window.isDev();
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
@@ -93,9 +96,9 @@ type State = {|
|
||||
|
||||
// The event based object editor is hidden in releases
|
||||
// because it's not handled by GDJS.
|
||||
const getInitialMosaicEditorNodes = (showEventBasedObjectsEditor: boolean) => ({
|
||||
const initialMosaicEditorNodes = {
|
||||
direction: 'row',
|
||||
first: showEventBasedObjectsEditor
|
||||
first: isDev
|
||||
? {
|
||||
direction: 'column',
|
||||
first: 'free-functions-list',
|
||||
@@ -120,7 +123,7 @@ const getInitialMosaicEditorNodes = (showEventBasedObjectsEditor: boolean) => ({
|
||||
splitPercentage: 25,
|
||||
},
|
||||
splitPercentage: 25,
|
||||
});
|
||||
};
|
||||
|
||||
export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
Props,
|
||||
@@ -1421,7 +1424,6 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
{({
|
||||
getDefaultEditorMosaicNode,
|
||||
setDefaultEditorMosaicNode,
|
||||
getShowEventBasedObjectsEditor,
|
||||
}) => (
|
||||
<EditorMosaic
|
||||
ref={editorMosaic => (this._editorMosaic = editorMosaic)}
|
||||
@@ -1433,28 +1435,21 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
)
|
||||
}
|
||||
initialNodes={
|
||||
// "objects-list" must only appear when the setting is enabled
|
||||
// and reciprocally.
|
||||
getShowEventBasedObjectsEditor() ===
|
||||
// "objects-list" must only appear in dev mode.
|
||||
isDev ===
|
||||
mosaicContainsNode(
|
||||
getDefaultEditorMosaicNode(
|
||||
'events-functions-extension-editor'
|
||||
) ||
|
||||
getInitialMosaicEditorNodes(
|
||||
getShowEventBasedObjectsEditor()
|
||||
),
|
||||
) || initialMosaicEditorNodes,
|
||||
'objects-list'
|
||||
)
|
||||
? getDefaultEditorMosaicNode(
|
||||
'events-functions-extension-editor'
|
||||
) ||
|
||||
getInitialMosaicEditorNodes(
|
||||
getShowEventBasedObjectsEditor()
|
||||
)
|
||||
) || initialMosaicEditorNodes
|
||||
: // Force the mosaic to reset to default.
|
||||
getInitialMosaicEditorNodes(
|
||||
getShowEventBasedObjectsEditor()
|
||||
)
|
||||
// It contains "objects-list" only
|
||||
// in dev mode.
|
||||
initialMosaicEditorNodes
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@@ -67,7 +67,7 @@ export const declareBehaviorMetadata = (
|
||||
extension: gdPlatformExtension,
|
||||
eventsBasedBehavior: gdEventsBasedBehavior
|
||||
): gdBehaviorMetadata => {
|
||||
const behaviorMetadata = extension
|
||||
return extension
|
||||
.addEventsBasedBehavior(
|
||||
eventsBasedBehavior.getName(),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
@@ -76,10 +76,6 @@ export const declareBehaviorMetadata = (
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
.setObjectType(eventsBasedBehavior.getObjectType());
|
||||
|
||||
if (eventsBasedBehavior.isPrivate()) behaviorMetadata.setPrivate();
|
||||
|
||||
return behaviorMetadata;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,19 +87,12 @@ export const declareObjectMetadata = (
|
||||
extension: gdPlatformExtension,
|
||||
eventsBasedObject: gdEventsBasedObject
|
||||
): gdObjectMetadata => {
|
||||
const objectMetadata = extension
|
||||
.addEventsBasedObject(
|
||||
eventsBasedObject.getName(),
|
||||
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
|
||||
eventsBasedObject.getDescription(),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
// TODO Change the metadata model to only set a category on the extension.
|
||||
// If an extension has behavior or object across several categories,
|
||||
// we can assume it's not scoped correctly.
|
||||
// Note: We shouldn't rely on gdPlatformExtension but this line will
|
||||
// be removed soon.
|
||||
.setCategoryFullName(extension.getCategory());
|
||||
const objectMetadata = extension.addEventsBasedObject(
|
||||
eventsBasedObject.getName(),
|
||||
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
|
||||
eventsBasedObject.getDescription(),
|
||||
getExtensionIconUrl(extension)
|
||||
);
|
||||
|
||||
// TODO EBO Use full type to identify object to avoid collision.
|
||||
// Objects are identified by their name alone.
|
||||
@@ -199,8 +188,8 @@ export const declareObjectMetadata = (
|
||||
i18n._('Flip the object horizontally'),
|
||||
i18n._('Flip horizontally _PARAM0_: _PARAM1_'),
|
||||
i18n._('Effects'),
|
||||
'res/actions/flipX24.png',
|
||||
'res/actions/flipX.png'
|
||||
'res/actions/flipX24_black.png',
|
||||
'res/actions/flipX_black.png'
|
||||
)
|
||||
.addParameter('object', i18n._('Object'), objectType)
|
||||
.addParameter('yesorno', i18n._('Activate flipping'))
|
||||
@@ -215,8 +204,8 @@ export const declareObjectMetadata = (
|
||||
i18n._('Flip the object vertically'),
|
||||
i18n._('Flip vertically _PARAM0_: _PARAM1_'),
|
||||
i18n._('Effects'),
|
||||
'res/actions/flipY24.png',
|
||||
'res/actions/flipY.png'
|
||||
'res/actions/flipY24_black.png',
|
||||
'res/actions/flipY_black.png'
|
||||
)
|
||||
.addParameter('object', i18n._('Object'), objectType)
|
||||
.addParameter('yesorno', i18n._('Activate flipping'))
|
||||
@@ -231,8 +220,8 @@ export const declareObjectMetadata = (
|
||||
i18n._('Check if the object is horizontally flipped'),
|
||||
i18n._('_PARAM0_ is horizontally flipped'),
|
||||
i18n._('Effects'),
|
||||
'res/actions/flipX24.png',
|
||||
'res/actions/flipX.png'
|
||||
'res/actions/flipX24_black.png',
|
||||
'res/actions/flipX_black.png'
|
||||
)
|
||||
.addParameter('object', i18n._('Object'), objectType)
|
||||
.getCodeExtraInformation()
|
||||
@@ -245,8 +234,8 @@ export const declareObjectMetadata = (
|
||||
i18n._('Check if the object is vertically flipped'),
|
||||
i18n._('_PARAM0_ is vertically flipped'),
|
||||
i18n._('Effects'),
|
||||
'res/actions/flipY24.png',
|
||||
'res/actions/flipY.png'
|
||||
'res/actions/flipY24_black.png',
|
||||
'res/actions/flipY_black.png'
|
||||
)
|
||||
.addParameter('object', i18n._('Object'), objectType)
|
||||
.getCodeExtraInformation()
|
||||
@@ -428,14 +417,12 @@ export const shiftSentenceParamIndexes = (
|
||||
sentence: string,
|
||||
offset: number
|
||||
): string => {
|
||||
const parameterIndexesStrings = sentence.match(/_PARAM\d+_/g);
|
||||
const parameterIndexesStrings = sentence.match(/(?<=_PARAM)(\d+)(?=_)/g);
|
||||
if (!parameterIndexesStrings) {
|
||||
return sentence;
|
||||
}
|
||||
const parameterIndexes = parameterIndexesStrings.map(indexString =>
|
||||
Number.parseInt(
|
||||
indexString.substring('_PARAM'.length, indexString.length - '_'.length)
|
||||
)
|
||||
Number.parseInt(indexString)
|
||||
);
|
||||
const sentenceElements = sentence.split(/_PARAM\d+_/);
|
||||
let shiftedSentence = '';
|
||||
@@ -696,30 +683,7 @@ export const declareObjectInstructionOrExpressionMetadata = (
|
||||
|
||||
type gdInstructionOrExpressionMetadata =
|
||||
| gdInstructionMetadata
|
||||
| gdExpressionMetadata
|
||||
| gdMultipleInstructionMetadata;
|
||||
|
||||
const convertPropertyTypeToValueType = (propertyType: string): string => {
|
||||
switch (propertyType) {
|
||||
case 'Number':
|
||||
return 'number';
|
||||
case 'Boolean':
|
||||
return 'boolean';
|
||||
case 'Color':
|
||||
return 'color';
|
||||
case 'Choice':
|
||||
return 'stringWithSelector';
|
||||
case 'String':
|
||||
default:
|
||||
return 'string';
|
||||
}
|
||||
};
|
||||
|
||||
const getStringifiedExtraInfo = (property: gdPropertyDescriptor) => {
|
||||
return property.getType() === 'Choice'
|
||||
? JSON.stringify(property.getExtraInfo().toJSArray())
|
||||
: '';
|
||||
};
|
||||
| gdExpressionMetadata;
|
||||
|
||||
/**
|
||||
* Declare the instructions (actions/conditions) and expressions for the
|
||||
@@ -766,37 +730,176 @@ export const declareBehaviorPropertiesInstructionAndExpressions = (
|
||||
|
||||
mapVector(eventsBasedBehavior.getPropertyDescriptors(), property => {
|
||||
const propertyType = property.getType();
|
||||
if (propertyType === 'Behavior') {
|
||||
// Required behaviors don't need accessors and mutators.
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyName = property.getName();
|
||||
const getterName = gd.BehaviorCodeGenerator.getBehaviorPropertyGetterName(
|
||||
propertyName
|
||||
);
|
||||
const setterName = gd.BehaviorCodeGenerator.getBehaviorPropertySetterName(
|
||||
propertyName
|
||||
);
|
||||
const propertyLabel = i18n._(
|
||||
t`${property.getLabel() || propertyName} property`
|
||||
);
|
||||
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addExpressionAndConditionAndAction(
|
||||
convertPropertyTypeToValueType(propertyType),
|
||||
gd.EventsBasedBehavior.getPropertyExpressionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`the value of ${propertyLabel}`),
|
||||
i18n._(t`the value of ${propertyLabel}`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension)
|
||||
if (propertyType === 'String' || propertyType === 'Choice') {
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addStrExpression(
|
||||
gd.EventsBasedBehavior.getPropertyExpressionName(propertyName),
|
||||
propertyLabel,
|
||||
propertyLabel,
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
)
|
||||
.useStandardParameters(
|
||||
convertPropertyTypeToValueType(propertyType),
|
||||
getStringifiedExtraInfo(property)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(getterName);
|
||||
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addScopedCondition(
|
||||
gd.EventsBasedBehavior.getPropertyConditionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Compare the content of ${propertyLabel}`),
|
||||
i18n._(t`the property ${propertyName}`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.setFunctionName(
|
||||
gd.BehaviorCodeGenerator.getBehaviorPropertySetterName(propertyName)
|
||||
.useStandardRelationalOperatorParameters('string')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(getterName);
|
||||
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addScopedAction(
|
||||
gd.EventsBasedBehavior.getPropertyActionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Change the content of ${propertyLabel}`),
|
||||
i18n._(t`the property ${propertyName}`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.setGetter(
|
||||
gd.BehaviorCodeGenerator.getBehaviorPropertyGetterName(propertyName)
|
||||
);
|
||||
.useStandardOperatorParameters('string')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(setterName)
|
||||
.setManipulatedType('string')
|
||||
.setGetter(getterName);
|
||||
} else if (propertyType === 'Number') {
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addExpression(
|
||||
gd.EventsBasedBehavior.getPropertyExpressionName(propertyName),
|
||||
propertyLabel,
|
||||
propertyLabel,
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(getterName);
|
||||
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addScopedCondition(
|
||||
gd.EventsBasedBehavior.getPropertyConditionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Compare the value of ${propertyLabel}`),
|
||||
i18n._(t`the property ${propertyName}`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.useStandardRelationalOperatorParameters('number')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(getterName);
|
||||
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addScopedAction(
|
||||
gd.EventsBasedBehavior.getPropertyActionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Change the value of ${propertyLabel}`),
|
||||
i18n._(t`the property ${propertyName}`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.useStandardOperatorParameters('number')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(setterName)
|
||||
.setGetter(getterName);
|
||||
} else if (propertyType === 'Boolean') {
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addScopedCondition(
|
||||
gd.EventsBasedBehavior.getPropertyConditionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Check the value of ${propertyLabel}`),
|
||||
i18n._(t`Property ${propertyName} of _PARAM0_ is true`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(getterName);
|
||||
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addScopedAction(
|
||||
gd.EventsBasedBehavior.getPropertyActionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Update the value of ${propertyLabel}`),
|
||||
i18n._(t`Set property ${propertyName} of _PARAM0_ to _PARAM2_`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.addParameter('yesorno', i18n._(t`New value to set`), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(setterName);
|
||||
} else if (propertyType === 'Color') {
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addScopedCondition(
|
||||
gd.EventsBasedBehavior.getPropertyConditionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Check the color of ${propertyLabel}`),
|
||||
i18n._(t`Color ${propertyName}`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.useStandardRelationalOperatorParameters('string')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(getterName);
|
||||
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addScopedAction(
|
||||
gd.EventsBasedBehavior.getPropertyActionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Update the color of ${propertyLabel}`),
|
||||
i18n._(t`Change color ${propertyName} of _PARAM0_ to _PARAM2_`),
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.addParameter('color', i18n._(t`New color to set`), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(setterName);
|
||||
|
||||
addObjectAndBehaviorParameters(
|
||||
behaviorMetadata.addStrExpression(
|
||||
gd.EventsBasedBehavior.getPropertyExpressionName(propertyName),
|
||||
propertyLabel,
|
||||
propertyLabel,
|
||||
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName(getterName);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -830,36 +933,6 @@ export const declareObjectPropertiesInstructionAndExpressions = (
|
||||
return instructionOrExpression;
|
||||
};
|
||||
|
||||
mapVector(eventsBasedObject.getPropertyDescriptors(), property => {
|
||||
const propertyType = property.getType();
|
||||
const propertyName = property.getName();
|
||||
const propertyLabel = i18n._(
|
||||
t`${property.getLabel() || propertyName} property`
|
||||
);
|
||||
|
||||
addObjectParameter(
|
||||
objectMetadata.addExpressionAndConditionAndAction(
|
||||
convertPropertyTypeToValueType(propertyType),
|
||||
gd.EventsBasedObject.getPropertyExpressionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`the value of ${propertyLabel}`),
|
||||
i18n._(t`the value of ${propertyLabel}`),
|
||||
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
|
||||
getExtensionIconUrl(extension)
|
||||
)
|
||||
)
|
||||
.useStandardParameters(
|
||||
convertPropertyTypeToValueType(propertyType),
|
||||
getStringifiedExtraInfo(property)
|
||||
)
|
||||
.setFunctionName(
|
||||
gd.BehaviorCodeGenerator.getBehaviorPropertySetterName(propertyName)
|
||||
)
|
||||
.setGetter(
|
||||
gd.BehaviorCodeGenerator.getBehaviorPropertyGetterName(propertyName)
|
||||
);
|
||||
});
|
||||
|
||||
mapVector(eventsBasedObject.getPropertyDescriptors(), property => {
|
||||
const propertyType = property.getType();
|
||||
const propertyName = property.getName();
|
||||
@@ -980,7 +1053,7 @@ export const declareObjectPropertiesInstructionAndExpressions = (
|
||||
gd.EventsBasedObject.getPropertyActionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Update the value of ${propertyLabel}`),
|
||||
i18n._(t`Set property ${propertyName} of _PARAM0_ to _PARAM1_`),
|
||||
i18n._(t`Set property ${propertyName} of _PARAM0_ to _PARAM2_`),
|
||||
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
@@ -1010,7 +1083,7 @@ export const declareObjectPropertiesInstructionAndExpressions = (
|
||||
gd.EventsBasedObject.getPropertyActionName(propertyName),
|
||||
propertyLabel,
|
||||
i18n._(t`Update the color of ${propertyLabel}`),
|
||||
i18n._(t`Change color ${propertyName} of _PARAM0_ to _PARAM1_`),
|
||||
i18n._(t`Change color ${propertyName} of _PARAM0_ to _PARAM2_`),
|
||||
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
|
||||
getExtensionIconUrl(extension),
|
||||
getExtensionIconUrl(extension)
|
||||
|
@@ -31,7 +31,6 @@ const styles = {
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
tooltip: { marginRight: 5, verticalAlign: 'bottom' },
|
||||
};
|
||||
|
||||
export type EventsFunctionCreationParameters = {|
|
||||
@@ -42,12 +41,11 @@ export type EventsFunctionCreationParameters = {|
|
||||
const renderEventsFunctionLabel = (eventsFunction: gdEventsFunction) =>
|
||||
eventsFunction.isPrivate() ? (
|
||||
<>
|
||||
<Tooltip
|
||||
title={
|
||||
<Trans>This function won't be visible in the events editor.</Trans>
|
||||
}
|
||||
>
|
||||
<VisibilityOffIcon fontSize="small" style={styles.tooltip} />
|
||||
<Tooltip title="This function won't be visible in the events editor">
|
||||
<VisibilityOffIcon
|
||||
fontSize="small"
|
||||
style={{ marginRight: 5, verticalAlign: 'bottom' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span title={eventsFunction.getName()}>{eventsFunction.getName()}</span>
|
||||
</>
|
||||
|
@@ -42,9 +42,6 @@ const styles = {
|
||||
color: '#d4d4d4',
|
||||
overflowX: 'hidden',
|
||||
maxWidth: '100%',
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'normal',
|
||||
overflowWrap: 'anywhere',
|
||||
},
|
||||
comment: {
|
||||
color: '#777',
|
||||
|
@@ -15,8 +15,8 @@ import {
|
||||
} from '../../InstructionOrExpression/CreateTree';
|
||||
import {
|
||||
enumerateAllInstructions,
|
||||
enumerateFreeInstructions,
|
||||
deduplicateInstructionsList,
|
||||
enumerateFreeInstructionsWithTranslatedCategories,
|
||||
} from '../../InstructionOrExpression/EnumerateInstructions';
|
||||
import {
|
||||
type EnumeratedInstructionMetadata,
|
||||
@@ -96,7 +96,6 @@ type Props = {|
|
||||
onSearchStartOrReset?: () => void,
|
||||
style?: Object,
|
||||
onClickMore?: () => void,
|
||||
i18n: I18nType,
|
||||
|};
|
||||
|
||||
const iconSize = 24;
|
||||
@@ -121,10 +120,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
|
||||
// Free instructions, to be displayed in a tab next to the objects.
|
||||
freeInstructionsInfo: Array<EnumeratedInstructionMetadata> = filterEnumeratedInstructionOrExpressionMetadataByScope(
|
||||
enumerateFreeInstructionsWithTranslatedCategories(
|
||||
this.props.isCondition,
|
||||
this.props.i18n
|
||||
),
|
||||
enumerateFreeInstructions(this.props.isCondition),
|
||||
this.props.scope
|
||||
);
|
||||
freeInstructionsInfoTree: InstructionOrExpressionTreeNode = createTree(
|
||||
@@ -140,12 +136,9 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
groupSearchApi = null;
|
||||
tagSearchApi = null;
|
||||
|
||||
reEnumerateInstructions = (i18n: I18nType) => {
|
||||
reEnumerateInstructions = () => {
|
||||
this.freeInstructionsInfo = filterEnumeratedInstructionOrExpressionMetadataByScope(
|
||||
enumerateFreeInstructionsWithTranslatedCategories(
|
||||
this.props.isCondition,
|
||||
i18n
|
||||
),
|
||||
enumerateFreeInstructions(this.props.isCondition),
|
||||
this.props.scope
|
||||
);
|
||||
this.freeInstructionsInfoTree = createTree(this.freeInstructionsInfo);
|
||||
|
@@ -1,7 +1,5 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
|
||||
import * as React from 'react';
|
||||
import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
|
||||
@@ -196,10 +194,10 @@ export default function NewInstructionEditorDialog({
|
||||
chooseObject(chosenObject.getName());
|
||||
};
|
||||
|
||||
const onExtensionInstalled = (i18n: I18nType) => {
|
||||
const onExtensionInstalled = () => {
|
||||
setNewExtensionDialogOpen(false);
|
||||
freeInstructionComponentRef.current &&
|
||||
freeInstructionComponentRef.current.reEnumerateInstructions(i18n);
|
||||
freeInstructionComponentRef.current.reEnumerateInstructions();
|
||||
};
|
||||
|
||||
// Focus the parameters when showing them
|
||||
@@ -227,38 +225,31 @@ export default function NewInstructionEditorDialog({
|
||||
: undefined;
|
||||
|
||||
const renderInstructionOrObjectSelector = () => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<InstructionOrObjectSelector
|
||||
key="instruction-or-object-selector"
|
||||
style={styles.fullHeightSelector}
|
||||
project={project}
|
||||
scope={scope}
|
||||
ref={freeInstructionComponentRef}
|
||||
currentTab={currentInstructionOrObjectSelectorTab}
|
||||
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
objectsContainer={objectsContainer}
|
||||
isCondition={isCondition}
|
||||
chosenInstructionType={
|
||||
!chosenObjectName ? instructionType : undefined
|
||||
}
|
||||
onChooseInstruction={(instructionType: string) => {
|
||||
chooseInstruction(instructionType);
|
||||
setStep('parameters');
|
||||
}}
|
||||
chosenObjectName={chosenObjectName}
|
||||
onChooseObject={(chosenObjectName: string) => {
|
||||
chooseObject(chosenObjectName);
|
||||
setStep('object-instructions');
|
||||
}}
|
||||
focusOnMount={!instructionType}
|
||||
onSearchStartOrReset={forceUpdate}
|
||||
onClickMore={() => setNewExtensionDialogOpen(true)}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
<InstructionOrObjectSelector
|
||||
key="instruction-or-object-selector"
|
||||
style={styles.fullHeightSelector}
|
||||
project={project}
|
||||
scope={scope}
|
||||
ref={freeInstructionComponentRef}
|
||||
currentTab={currentInstructionOrObjectSelectorTab}
|
||||
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
objectsContainer={objectsContainer}
|
||||
isCondition={isCondition}
|
||||
chosenInstructionType={!chosenObjectName ? instructionType : undefined}
|
||||
onChooseInstruction={(instructionType: string) => {
|
||||
chooseInstruction(instructionType);
|
||||
setStep('parameters');
|
||||
}}
|
||||
chosenObjectName={chosenObjectName}
|
||||
onChooseObject={(chosenObjectName: string) => {
|
||||
chooseObject(chosenObjectName);
|
||||
setStep('object-instructions');
|
||||
}}
|
||||
focusOnMount={!instructionType}
|
||||
onSearchStartOrReset={forceUpdate}
|
||||
onClickMore={() => setNewExtensionDialogOpen(true)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderParameters = () => (
|
||||
@@ -400,7 +391,6 @@ export default function NewInstructionEditorDialog({
|
||||
{newBehaviorDialogOpen && chosenObject && (
|
||||
<NewBehaviorDialog
|
||||
project={project}
|
||||
eventsFunctionsExtension={scope.eventsFunctionsExtension}
|
||||
open={newBehaviorDialogOpen}
|
||||
objectType={chosenObject.getType()}
|
||||
objectBehaviorsTypes={listObjectBehaviorsTypes(chosenObject)}
|
||||
@@ -409,16 +399,12 @@ export default function NewInstructionEditorDialog({
|
||||
/>
|
||||
)}
|
||||
{newExtensionDialogOpen && (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<ExtensionsSearchDialog
|
||||
project={project}
|
||||
onClose={() => setNewExtensionDialogOpen(false)}
|
||||
onInstallExtension={() => {}}
|
||||
onExtensionInstalled={() => onExtensionInstalled(i18n)}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
<ExtensionsSearchDialog
|
||||
project={project}
|
||||
onClose={() => setNewExtensionDialogOpen(false)}
|
||||
onInstallExtension={() => {}}
|
||||
onExtensionInstalled={onExtensionInstalled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import Popover from '@material-ui/core/Popover';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
@@ -139,38 +138,31 @@ export default function NewInstructionEditorMenu({
|
||||
};
|
||||
|
||||
const renderInstructionOrObjectSelector = () => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<InstructionOrObjectSelector
|
||||
key="instruction-or-object-selector"
|
||||
style={styles.fullHeightSelector}
|
||||
project={project}
|
||||
scope={scope}
|
||||
currentTab={currentInstructionOrObjectSelectorTab}
|
||||
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
objectsContainer={objectsContainer}
|
||||
isCondition={isCondition}
|
||||
chosenInstructionType={
|
||||
!chosenObjectName ? instructionType : undefined
|
||||
}
|
||||
onChooseInstruction={(instructionType: string) => {
|
||||
const { instruction, chosenObjectName } = chooseInstruction(
|
||||
instructionType
|
||||
);
|
||||
submitInstruction({ instruction, chosenObjectName });
|
||||
}}
|
||||
chosenObjectName={chosenObjectName}
|
||||
onChooseObject={chosenObjectName => {
|
||||
chooseObject(chosenObjectName);
|
||||
setStep('object-instructions');
|
||||
}}
|
||||
focusOnMount={!instructionType}
|
||||
onSearchStartOrReset={forceUpdate}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
<InstructionOrObjectSelector
|
||||
key="instruction-or-object-selector"
|
||||
style={styles.fullHeightSelector}
|
||||
project={project}
|
||||
scope={scope}
|
||||
currentTab={currentInstructionOrObjectSelectorTab}
|
||||
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
objectsContainer={objectsContainer}
|
||||
isCondition={isCondition}
|
||||
chosenInstructionType={!chosenObjectName ? instructionType : undefined}
|
||||
onChooseInstruction={(instructionType: string) => {
|
||||
const { instruction, chosenObjectName } = chooseInstruction(
|
||||
instructionType
|
||||
);
|
||||
submitInstruction({ instruction, chosenObjectName });
|
||||
}}
|
||||
chosenObjectName={chosenObjectName}
|
||||
onChooseObject={chosenObjectName => {
|
||||
chooseObject(chosenObjectName);
|
||||
setStep('object-instructions');
|
||||
}}
|
||||
focusOnMount={!instructionType}
|
||||
onSearchStartOrReset={forceUpdate}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderObjectInstructionSelector = () =>
|
||||
|
@@ -72,7 +72,6 @@ const components = {
|
||||
operator: OperatorField,
|
||||
yesorno: YesNoField,
|
||||
trueorfalse: TrueFalseField,
|
||||
number: ExpressionField,
|
||||
expression: ExpressionField,
|
||||
string: StringField,
|
||||
stringWithSelector: StringWithSelectorField,
|
||||
@@ -129,7 +128,6 @@ const userFriendlyTypeName: { [string]: MessageDescriptor } = {
|
||||
yesorno: t`Yes or No`,
|
||||
trueorfalse: t`True or False`,
|
||||
expression: t`Number`,
|
||||
number: t`Number`,
|
||||
string: t`String`,
|
||||
stringWithSelector: t`String`,
|
||||
behavior: t`Behavior`,
|
||||
|
@@ -891,6 +891,9 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
locatingEvent: gdBaseEvent,
|
||||
parameterContext: ParameterContext
|
||||
) => {
|
||||
// Prevent state from changing when clicking on a parameter with inline parameter
|
||||
// editor (it will close the editor).
|
||||
if (this.state.editedParameter.instruction) return;
|
||||
const { instruction, parameterIndex } = parameterContext;
|
||||
|
||||
this.setState({
|
||||
|
@@ -28,7 +28,7 @@ import { formatScore } from '../../Leaderboard/LeaderboardScoreFormatter';
|
||||
type Props = {|
|
||||
entries: ?Array<LeaderboardDisplayData>,
|
||||
customizationSettings: ?LeaderboardCustomizationSettings,
|
||||
onDeleteEntry: (entry: LeaderboardDisplayData) => Promise<void>,
|
||||
onDeleteEntry: (entryId: string) => Promise<void>,
|
||||
isLoading: boolean,
|
||||
erroredEntry?: {| entryId: string, message: React.Node |},
|
||||
navigation: {|
|
||||
@@ -108,7 +108,7 @@ const LeaderboardEntriesTable = ({
|
||||
<Line>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onDeleteEntry(entry)}
|
||||
onClick={() => onDeleteEntry(entry.id)}
|
||||
disabled={isLoading}
|
||||
tooltip={t`Remove entry`}
|
||||
>
|
||||
|
@@ -1,71 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import Text from '../../UI/Text';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import { Line, Column } from '../../UI/Grid';
|
||||
import { type Limits } from '../../Utils/GDevelopServices/Usage';
|
||||
|
||||
type Props = {|
|
||||
onUpgrade: () => void,
|
||||
onClose: () => void,
|
||||
limits: Limits,
|
||||
|};
|
||||
|
||||
const MaxLeaderboardCountAlertMessage = ({
|
||||
onUpgrade,
|
||||
onClose,
|
||||
limits,
|
||||
}: Props) => {
|
||||
const leaderboardLimits = limits.capabilities.leaderboards;
|
||||
if (!leaderboardLimits) return null;
|
||||
|
||||
return (
|
||||
<Line>
|
||||
<Column expand>
|
||||
<AlertMessage
|
||||
kind="warning"
|
||||
onHide={onClose}
|
||||
renderRightButton={
|
||||
leaderboardLimits.canMaximumCountPerGameBeIncreased
|
||||
? () => (
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Check our premiums plans</Trans>}
|
||||
onClick={onUpgrade}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Text size="block-title">
|
||||
<Trans>
|
||||
You've reached your maximum of{' '}
|
||||
{leaderboardLimits.maximumCountPerGame} leaderboards for your game
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
{leaderboardLimits.canMaximumCountPerGameBeIncreased ? (
|
||||
<Trans>
|
||||
Update to GDevelop Premium to get more leaderboards, storage,
|
||||
and one-click packagings!
|
||||
</Trans>
|
||||
) : (
|
||||
// This should not happen at the moment since leaderboards are unlimited
|
||||
// in any paid plans but it could happen in the future with a plan that
|
||||
// cannot be increased and that has a max number of leaderboards.
|
||||
<Trans>
|
||||
To keep using GDevelop leaderboards, consider deleting old,
|
||||
unused leaderboards.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</AlertMessage>
|
||||
</Column>
|
||||
</Line>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaxLeaderboardCountAlertMessage;
|
@@ -49,11 +49,11 @@ import {
|
||||
type Leaderboard,
|
||||
type LeaderboardCustomizationSettings,
|
||||
type LeaderboardUpdatePayload,
|
||||
type LeaderboardDisplayData,
|
||||
shortenUuidForDisplay,
|
||||
} from '../../Utils/GDevelopServices/Play';
|
||||
import LeaderboardContext from '../../Leaderboard/LeaderboardContext';
|
||||
import LeaderboardProvider from '../../Leaderboard/LeaderboardProvider';
|
||||
import Window from '../../Utils/Window';
|
||||
import LeaderboardEntriesTable from './LeaderboardEntriesTable';
|
||||
import { ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
@@ -68,10 +68,6 @@ import { type LeaderboardSortOption } from '../../Utils/GDevelopServices/Play';
|
||||
import { formatScore } from '../../Leaderboard/LeaderboardScoreFormatter';
|
||||
import Toggle from '../../UI/Toggle';
|
||||
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import SubscriptionDialog from '../../Profile/SubscriptionDialog';
|
||||
import MaxLeaderboardCountAlertMessage from './MaxLeaderboardCountAlertMessage';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
|
||||
type Props = {|
|
||||
onLoading: boolean => void,
|
||||
@@ -193,18 +189,6 @@ export const LeaderboardAdmin = ({
|
||||
const [isEditingAppearance, setIsEditingAppearance] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const { showConfirmation, showDeleteConfirmation } = useAlertDialog();
|
||||
const [
|
||||
displayMaxLeaderboardCountReachedWarning,
|
||||
setDisplayMaxLeaderboardCountReachedWarning,
|
||||
] = React.useState<boolean>(false);
|
||||
const [
|
||||
subscriptionDialogOpen,
|
||||
setSubscriptionDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const { limits } = authenticatedUser;
|
||||
|
||||
const [
|
||||
isEditingSortOptions,
|
||||
setIsEditingSortOptions,
|
||||
@@ -328,18 +312,6 @@ export const LeaderboardAdmin = ({
|
||||
setIsLoading(true);
|
||||
setApiError(null);
|
||||
try {
|
||||
if (limits && leaderboards) {
|
||||
const leaderboardLimits = limits.capabilities.leaderboards;
|
||||
if (
|
||||
leaderboardLimits &&
|
||||
leaderboardLimits.maximumCountPerGame > 0 &&
|
||||
leaderboards.length >= leaderboardLimits.maximumCountPerGame
|
||||
) {
|
||||
setDisplayMaxLeaderboardCountReachedWarning(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await createLeaderboard({
|
||||
name: 'New leaderboard',
|
||||
sort: 'ASC',
|
||||
@@ -361,11 +333,11 @@ export const LeaderboardAdmin = ({
|
||||
};
|
||||
|
||||
const onResetLeaderboard = async (i18n: I18nType) => {
|
||||
if (!currentLeaderboard) return;
|
||||
const answer = await showConfirmation({
|
||||
title: t`Reset leaderboard ${currentLeaderboard.name}`,
|
||||
message: t`All current entries will be deleted, are you sure you want to reset this leaderboard? This can't be undone.`,
|
||||
});
|
||||
const answer = Window.showConfirmDialog(
|
||||
i18n._(
|
||||
t`All current entries will be deleted, are you sure you want to reset this leaderboard? This can't be undone.`
|
||||
)
|
||||
);
|
||||
if (!answer) return;
|
||||
|
||||
setIsLoading(true);
|
||||
@@ -395,13 +367,11 @@ export const LeaderboardAdmin = ({
|
||||
};
|
||||
|
||||
const onDeleteLeaderboard = async (i18n: I18nType) => {
|
||||
if (!currentLeaderboard) return;
|
||||
const answer = await showDeleteConfirmation({
|
||||
title: t`Delete leaderboard ${currentLeaderboard.name}`,
|
||||
message: t`Are you sure you want to delete this leaderboard and all of its entries? This can't be undone.`,
|
||||
confirmText: currentLeaderboard.name,
|
||||
fieldMessage: t`Type the name of the leaderboard:`,
|
||||
});
|
||||
const answer = Window.showConfirmDialog(
|
||||
i18n._(
|
||||
t`Are you sure you want to delete this leaderboard and all of its entries? This can't be undone.`
|
||||
)
|
||||
);
|
||||
if (!answer) return;
|
||||
|
||||
setIsLoading(true);
|
||||
@@ -424,21 +394,18 @@ export const LeaderboardAdmin = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteEntry = async (
|
||||
i18n: I18nType,
|
||||
entry: LeaderboardDisplayData
|
||||
) => {
|
||||
if (!currentLeaderboard) return;
|
||||
const answer = await showConfirmation({
|
||||
title: t`Delete score ${entry.score} from ${entry.playerName}`,
|
||||
message: t`Are you sure you want to delete this entry? This can't be undone.`,
|
||||
});
|
||||
const onDeleteEntry = async (i18n: I18nType, entryId: string) => {
|
||||
const answer = Window.showConfirmDialog(
|
||||
i18n._(
|
||||
t`Are you sure you want to delete this entry? This can't be undone.`
|
||||
)
|
||||
);
|
||||
if (!answer) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setApiError(null);
|
||||
try {
|
||||
await deleteLeaderboardEntry(entry.id);
|
||||
await deleteLeaderboardEntry(entryId);
|
||||
} catch (err) {
|
||||
console.error('An error occurred when deleting entry', err);
|
||||
setApiError({
|
||||
@@ -448,7 +415,7 @@ export const LeaderboardAdmin = ({
|
||||
An error occurred when deleting the entry, please try again.
|
||||
</Trans>
|
||||
),
|
||||
itemId: entry.id,
|
||||
itemId: entryId,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -872,201 +839,183 @@ export const LeaderboardAdmin = ({
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<>
|
||||
<Column noMargin expand>
|
||||
{displayMaxLeaderboardCountReachedWarning && limits && (
|
||||
<MaxLeaderboardCountAlertMessage
|
||||
onUpgrade={() => setSubscriptionDialogOpen(true)}
|
||||
onClose={() =>
|
||||
setDisplayMaxLeaderboardCountReachedWarning(false)
|
||||
}
|
||||
limits={limits}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveLineStackLayout noMargin expand noColumnMargin>
|
||||
<div style={styles.leftColumn}>
|
||||
<Paper
|
||||
elevation={5}
|
||||
style={{
|
||||
...styles.leaderboardConfigurationPaper,
|
||||
backgroundColor: gdevelopTheme.palette.alternateCanvasColor,
|
||||
}}
|
||||
>
|
||||
<Column>
|
||||
<Line noMargin>
|
||||
{currentLeaderboard && leaderboards ? (
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Leaderboard name</Trans>}
|
||||
value={currentLeaderboard.id}
|
||||
onChange={(e, i, leaderboardId) => {
|
||||
selectLeaderboard(leaderboardId);
|
||||
}}
|
||||
>
|
||||
{leaderboards.map(leaderboard => (
|
||||
<SelectOption
|
||||
key={leaderboard.id}
|
||||
value={leaderboard.id}
|
||||
primaryText={
|
||||
leaderboard.primary
|
||||
? t`${leaderboard.name} (default)`
|
||||
: leaderboard.name
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SelectField>
|
||||
) : null}
|
||||
<IconButton
|
||||
onClick={onCreateLeaderboard}
|
||||
disabled={isEditingName || isRequestPending}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</Line>
|
||||
{currentLeaderboard ? (
|
||||
<>
|
||||
<List>
|
||||
{getLeaderboardDescription(
|
||||
i18n,
|
||||
currentLeaderboard
|
||||
).map((item, index) => (
|
||||
<React.Fragment key={`fragment-${item.key}`}>
|
||||
{index > 0 ? (
|
||||
<Divider
|
||||
key={`divider-${item.key}`}
|
||||
component="li"
|
||||
/>
|
||||
) : null}
|
||||
<ListItem key={item.key} disableGutters>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{item.avatar}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
disableTypography
|
||||
secondary={item.secondaryText}
|
||||
>
|
||||
{item.text}
|
||||
</ListItemText>
|
||||
{item.secondaryAction ? (
|
||||
<ListItemSecondaryAction>
|
||||
{item.secondaryAction}
|
||||
</ListItemSecondaryAction>
|
||||
) : null}
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
<Line justifyContent="space-between">
|
||||
<FlatButton
|
||||
leftIcon={<Delete />}
|
||||
label={<Trans>Delete</Trans>}
|
||||
disabled={isRequestPending || isEditingName}
|
||||
onClick={() => onDeleteLeaderboard(i18n)}
|
||||
/>
|
||||
<RaisedButton
|
||||
label={
|
||||
currentLeaderboard.primary ? (
|
||||
<Trans>Default</Trans>
|
||||
) : (
|
||||
<Trans>Set as default</Trans>
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
isRequestPending ||
|
||||
isEditingName ||
|
||||
currentLeaderboard.primary
|
||||
}
|
||||
onClick={() =>
|
||||
onUpdateLeaderboard(i18n, { primary: true })
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
{apiError &&
|
||||
(apiError.action === 'leaderboardDeletion' ||
|
||||
apiError.action === 'leaderboardPrimaryUpdate') ? (
|
||||
<PlaceholderError>
|
||||
{apiError.message}
|
||||
</PlaceholderError>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</Column>
|
||||
</Paper>
|
||||
</div>
|
||||
<div
|
||||
<ResponsiveLineStackLayout noMargin expand noColumnMargin>
|
||||
<div style={styles.leftColumn}>
|
||||
<Paper
|
||||
elevation={5}
|
||||
style={{
|
||||
...styles.rightColumn,
|
||||
paddingLeft: windowWidth === 'small' ? 0 : 20,
|
||||
...styles.leaderboardConfigurationPaper,
|
||||
backgroundColor: gdevelopTheme.palette.alternateCanvasColor,
|
||||
}}
|
||||
>
|
||||
<Line alignItems="center" justifyContent="flex-end">
|
||||
<Toggle
|
||||
size="small"
|
||||
labelPosition="left"
|
||||
toggled={displayOnlyBestEntry}
|
||||
onToggle={(e, newValue) =>
|
||||
setDisplayOnlyBestEntry(newValue)
|
||||
}
|
||||
label={
|
||||
<Tooltip
|
||||
title={i18n._(
|
||||
t`When checked, will only display the best score of each player (only for the display below).`
|
||||
)}
|
||||
<Column>
|
||||
<Line>
|
||||
{currentLeaderboard && leaderboards ? (
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Leaderboard name</Trans>}
|
||||
value={currentLeaderboard.id}
|
||||
onChange={(e, i, leaderboardId) => {
|
||||
selectLeaderboard(leaderboardId);
|
||||
}}
|
||||
>
|
||||
<Text size="body2">
|
||||
<Trans>Player best entry</Trans>
|
||||
</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<LargeSpacer />
|
||||
<Divider orientation="vertical" />
|
||||
<Spacer />
|
||||
<IconButton
|
||||
onClick={onFetchLeaderboardEntries}
|
||||
disabled={isRequestPending || isEditingName}
|
||||
tooltip={t`Refresh`}
|
||||
size="small"
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
<Spacer />
|
||||
</Line>
|
||||
{apiError && apiError.action === 'entriesFetching' ? (
|
||||
<CenteredError>
|
||||
<PlaceholderError onRetry={onFetchLeaderboardEntries}>
|
||||
{apiError.message}
|
||||
</PlaceholderError>
|
||||
</CenteredError>
|
||||
) : (
|
||||
<LeaderboardEntriesTable
|
||||
entries={entries}
|
||||
customizationSettings={
|
||||
currentLeaderboard
|
||||
? currentLeaderboard.customizationSettings
|
||||
: null
|
||||
}
|
||||
onDeleteEntry={entry => onDeleteEntry(i18n, entry)}
|
||||
isLoading={isRequestPending || isEditingName}
|
||||
navigation={{
|
||||
goToNextPage,
|
||||
goToPreviousPage,
|
||||
goToFirstPage,
|
||||
}}
|
||||
erroredEntry={
|
||||
apiError &&
|
||||
apiError.action === 'entryDeletion' &&
|
||||
apiError.itemId
|
||||
? {
|
||||
entryId: apiError.itemId,
|
||||
message: apiError.message,
|
||||
{leaderboards.map(leaderboard => (
|
||||
<SelectOption
|
||||
key={leaderboard.id}
|
||||
value={leaderboard.id}
|
||||
primaryText={
|
||||
leaderboard.primary
|
||||
? t`${leaderboard.name} (default)`
|
||||
: leaderboard.name
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SelectField>
|
||||
) : null}
|
||||
<IconButton
|
||||
onClick={onCreateLeaderboard}
|
||||
disabled={isEditingName || isRequestPending}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</Line>
|
||||
{currentLeaderboard ? (
|
||||
<>
|
||||
<List>
|
||||
{getLeaderboardDescription(
|
||||
i18n,
|
||||
currentLeaderboard
|
||||
).map((item, index) => (
|
||||
<React.Fragment key={`fragment-${item.key}`}>
|
||||
{index > 0 ? (
|
||||
<Divider
|
||||
key={`divider-${item.key}`}
|
||||
component="li"
|
||||
/>
|
||||
) : null}
|
||||
<ListItem key={item.key} disableGutters>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{item.avatar}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
disableTypography
|
||||
secondary={item.secondaryText}
|
||||
>
|
||||
{item.text}
|
||||
</ListItemText>
|
||||
{item.secondaryAction ? (
|
||||
<ListItemSecondaryAction>
|
||||
{item.secondaryAction}
|
||||
</ListItemSecondaryAction>
|
||||
) : null}
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
<Line justifyContent="space-between">
|
||||
<FlatButton
|
||||
leftIcon={<Delete />}
|
||||
label={<Trans>Delete</Trans>}
|
||||
disabled={isRequestPending || isEditingName}
|
||||
onClick={() => onDeleteLeaderboard(i18n)}
|
||||
/>
|
||||
<RaisedButton
|
||||
label={
|
||||
currentLeaderboard.primary ? (
|
||||
<Trans>Default</Trans>
|
||||
) : (
|
||||
<Trans>Set as default</Trans>
|
||||
)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
disabled={
|
||||
isRequestPending ||
|
||||
isEditingName ||
|
||||
currentLeaderboard.primary
|
||||
}
|
||||
onClick={() =>
|
||||
onUpdateLeaderboard(i18n, { primary: true })
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
{apiError &&
|
||||
(apiError.action === 'leaderboardDeletion' ||
|
||||
apiError.action === 'leaderboardPrimaryUpdate') ? (
|
||||
<PlaceholderError>{apiError.message}</PlaceholderError>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</Column>
|
||||
</Paper>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...styles.rightColumn,
|
||||
paddingLeft: windowWidth === 'small' ? 0 : 20,
|
||||
}}
|
||||
>
|
||||
<Line alignItems="center" justifyContent="flex-end">
|
||||
<Toggle
|
||||
size="small"
|
||||
labelPosition="left"
|
||||
toggled={displayOnlyBestEntry}
|
||||
onToggle={(e, newValue) => setDisplayOnlyBestEntry(newValue)}
|
||||
label={
|
||||
<Tooltip
|
||||
title={i18n._(
|
||||
t`When checked, will only display the best score of each player (only for the display below).`
|
||||
)}
|
||||
>
|
||||
<Text size="body2">
|
||||
<Trans>Player best entry</Trans>
|
||||
</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<LargeSpacer />
|
||||
<Divider orientation="vertical" />
|
||||
<Spacer />
|
||||
<IconButton
|
||||
onClick={onFetchLeaderboardEntries}
|
||||
disabled={isRequestPending || isEditingName}
|
||||
tooltip={t`Refresh`}
|
||||
size="small"
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
<Spacer />
|
||||
</Line>
|
||||
{apiError && apiError.action === 'entriesFetching' ? (
|
||||
<CenteredError>
|
||||
<PlaceholderError onRetry={onFetchLeaderboardEntries}>
|
||||
{apiError.message}
|
||||
</PlaceholderError>
|
||||
</CenteredError>
|
||||
) : (
|
||||
<LeaderboardEntriesTable
|
||||
entries={entries}
|
||||
customizationSettings={
|
||||
currentLeaderboard
|
||||
? currentLeaderboard.customizationSettings
|
||||
: null
|
||||
}
|
||||
onDeleteEntry={entryId => onDeleteEntry(i18n, entryId)}
|
||||
isLoading={isRequestPending || isEditingName}
|
||||
navigation={{
|
||||
goToNextPage,
|
||||
goToPreviousPage,
|
||||
goToFirstPage,
|
||||
}}
|
||||
erroredEntry={
|
||||
apiError &&
|
||||
apiError.action === 'entryDeletion' &&
|
||||
apiError.itemId
|
||||
? { entryId: apiError.itemId, message: apiError.message }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ResponsiveLineStackLayout>
|
||||
{isEditingAppearance ? (
|
||||
<LeaderboardAppearanceDialog
|
||||
open
|
||||
@@ -1109,12 +1058,6 @@ export const LeaderboardAdmin = ({
|
||||
extremeAllowedScore={currentLeaderboard.extremeAllowedScore}
|
||||
/>
|
||||
) : null}
|
||||
{subscriptionDialogOpen && (
|
||||
<SubscriptionDialog
|
||||
open
|
||||
onClose={() => setSubscriptionDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -1,10 +1,8 @@
|
||||
// @flow
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import {
|
||||
type EnumeratedInstructionMetadata,
|
||||
type InstructionOrExpressionScope,
|
||||
} from './EnumeratedInstructionOrExpressionMetadata';
|
||||
import { translateExtensionCategory } from '../Utils/Extension/ExtensionCategories.js';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -72,17 +70,6 @@ export const getExtensionPrefix = (extension: gdPlatformExtension): string => {
|
||||
return extension.getCategory() + GROUP_DELIMITER + extension.getFullName();
|
||||
};
|
||||
|
||||
const getExtensionTranslatedPrefix = (
|
||||
extension: gdPlatformExtension,
|
||||
i18n: I18nType
|
||||
): string => {
|
||||
return (
|
||||
translateExtensionCategory(extension.getCategory(), i18n) +
|
||||
GROUP_DELIMITER +
|
||||
extension.getFullName()
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* When all instructions are searched, some can be duplicated
|
||||
* (on purpose, so that it's easier to find them for users)
|
||||
@@ -472,30 +459,6 @@ export const enumerateObjectAndBehaviorsInstructions = (
|
||||
*/
|
||||
export const enumerateFreeInstructions = (
|
||||
isCondition: boolean
|
||||
): Array<EnumeratedInstructionMetadata> => {
|
||||
return doEnumerateFreeInstructions(isCondition, getExtensionPrefix);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enumerate all the instructions that are not directly tied
|
||||
* to an object.
|
||||
*/
|
||||
export const enumerateFreeInstructionsWithTranslatedCategories = (
|
||||
isCondition: boolean,
|
||||
i18n: I18nType
|
||||
): Array<EnumeratedInstructionMetadata> => {
|
||||
return doEnumerateFreeInstructions(isCondition, extension =>
|
||||
getExtensionTranslatedPrefix(extension, i18n)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enumerate all the instructions that are not directly tied
|
||||
* to an object.
|
||||
*/
|
||||
const doEnumerateFreeInstructions = (
|
||||
isCondition: boolean,
|
||||
getExtensionPrefix: (extension: gdPlatformExtension) => string
|
||||
): Array<EnumeratedInstructionMetadata> => {
|
||||
let allFreeInstructions = [];
|
||||
|
||||
|
@@ -18,7 +18,7 @@ describe('EnumerateInstructions', () => {
|
||||
expect.objectContaining({
|
||||
displayedName: 'Animation finished',
|
||||
fullGroupName: 'General/Sprite/Animations and images',
|
||||
type: 'AnimationEnded2',
|
||||
type: 'AnimationEnded',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
displayedName: 'Trigger once while true',
|
||||
@@ -105,11 +105,11 @@ describe('EnumerateInstructions', () => {
|
||||
expect(triggerOnce).not.toBeUndefined();
|
||||
expect(getObjectParameterIndex(triggerOnce.metadata)).toBe(-1);
|
||||
|
||||
const spriteAnimationEnded = conditions.filter(
|
||||
({ type }) => type === 'AnimationEnded2'
|
||||
const spriteAnimatedEnded = conditions.filter(
|
||||
({ type }) => type === 'AnimationEnded'
|
||||
)[0];
|
||||
expect(spriteAnimationEnded).not.toBeUndefined();
|
||||
expect(getObjectParameterIndex(spriteAnimationEnded.metadata)).toBe(0);
|
||||
expect(spriteAnimatedEnded).not.toBeUndefined();
|
||||
expect(getObjectParameterIndex(spriteAnimatedEnded.metadata)).toBe(0);
|
||||
});
|
||||
|
||||
it('can enumerate instructions for an object (Sprite)', () => {
|
||||
@@ -129,7 +129,7 @@ describe('EnumerateInstructions', () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
displayedName: 'Animation finished',
|
||||
type: 'AnimationEnded2',
|
||||
type: 'AnimationEnded',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
displayedName: 'The cursor/touch is on an object',
|
||||
|
@@ -57,34 +57,38 @@ export const filterEnumeratedInstructionOrExpressionMetadataByScope = <
|
||||
scope: EventsScope
|
||||
): Array<T> => {
|
||||
return list.filter(enumeratedInstructionOrExpressionMetadata => {
|
||||
if (!enumeratedInstructionOrExpressionMetadata.isPrivate) return true;
|
||||
|
||||
// The instruction or expression is marked as "private":
|
||||
// we now compare its scope (where it was declared) and the current scope
|
||||
// (where we are) to see if we should filter it or not.
|
||||
|
||||
const {
|
||||
behaviorMetadata,
|
||||
extension,
|
||||
} = enumeratedInstructionOrExpressionMetadata.scope;
|
||||
const { eventsBasedBehavior, eventsFunctionsExtension } = scope;
|
||||
|
||||
return (
|
||||
(!enumeratedInstructionOrExpressionMetadata.isPrivate &&
|
||||
(!behaviorMetadata || !behaviorMetadata.isPrivate())) ||
|
||||
// The instruction or expression is marked as "private":
|
||||
// we now compare its scope (where it was declared) and the current scope
|
||||
// (where we are) to see if we should filter it or not.
|
||||
// Show private behavior functions when editing the behavior
|
||||
if (
|
||||
behaviorMetadata &&
|
||||
eventsBasedBehavior &&
|
||||
eventsFunctionsExtension &&
|
||||
getBehaviorFullType(
|
||||
eventsFunctionsExtension.getName(),
|
||||
eventsBasedBehavior.getName()
|
||||
) === behaviorMetadata.getName()
|
||||
)
|
||||
return true;
|
||||
|
||||
// Show private behavior functions when editing the behavior
|
||||
(behaviorMetadata &&
|
||||
eventsBasedBehavior &&
|
||||
eventsFunctionsExtension &&
|
||||
getBehaviorFullType(
|
||||
eventsFunctionsExtension.getName(),
|
||||
eventsBasedBehavior.getName()
|
||||
) === behaviorMetadata.getName()) ||
|
||||
// When editing the extension...
|
||||
(eventsFunctionsExtension &&
|
||||
eventsFunctionsExtension.getName() === extension.getName() &&
|
||||
// ...show public functions of a private behavior
|
||||
(!enumeratedInstructionOrExpressionMetadata.isPrivate ||
|
||||
// ...show private non-behavior functions
|
||||
!behaviorMetadata))
|
||||
);
|
||||
// Show private non-behavior functions when editing the extension
|
||||
if (
|
||||
!behaviorMetadata &&
|
||||
eventsFunctionsExtension &&
|
||||
eventsFunctionsExtension.getName() === extension.getName()
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
@@ -53,10 +53,7 @@ const LayerEditorDialog = (props: Props) => {
|
||||
hotReloadPreviewButtonProps,
|
||||
} = props;
|
||||
const forceUpdate = useForceUpdate();
|
||||
const {
|
||||
onCancelChanges,
|
||||
notifyOfChange,
|
||||
} = useSerializableObjectCancelableEditor({
|
||||
const onCancelChanges = useSerializableObjectCancelableEditor({
|
||||
serializableObject: layer,
|
||||
onCancel: onClose,
|
||||
});
|
||||
@@ -146,7 +143,6 @@ const LayerEditorDialog = (props: Props) => {
|
||||
onCheck={(e, checked) => {
|
||||
layer.setVisibility(!checked);
|
||||
forceUpdate();
|
||||
notifyOfChange();
|
||||
}}
|
||||
tooltipOrHelperText={
|
||||
<Trans>
|
||||
@@ -167,7 +163,6 @@ const LayerEditorDialog = (props: Props) => {
|
||||
onCheck={(e, checked) => {
|
||||
layer.setFollowBaseLayerCamera(checked);
|
||||
forceUpdate();
|
||||
notifyOfChange();
|
||||
}}
|
||||
/>
|
||||
<ColorField
|
||||
@@ -179,21 +174,15 @@ const LayerEditorDialog = (props: Props) => {
|
||||
g: layer.getAmbientLightColorGreen(),
|
||||
b: layer.getAmbientLightColorBlue(),
|
||||
})}
|
||||
onChange={newColor => {
|
||||
const currentRgbColor = {
|
||||
r: layer.getAmbientLightColorRed(),
|
||||
g: layer.getAmbientLightColorGreen(),
|
||||
b: layer.getAmbientLightColorBlue(),
|
||||
};
|
||||
const newRgbColor = rgbStringAndAlphaToRGBColor(newColor);
|
||||
if (newRgbColor) {
|
||||
onChange={color => {
|
||||
const rgbColor = rgbStringAndAlphaToRGBColor(color);
|
||||
if (rgbColor) {
|
||||
layer.setAmbientLightColor(
|
||||
newRgbColor.r,
|
||||
newRgbColor.g,
|
||||
newRgbColor.b
|
||||
rgbColor.r,
|
||||
rgbColor.g,
|
||||
rgbColor.b
|
||||
);
|
||||
forceUpdate();
|
||||
if (currentRgbColor !== newRgbColor) notifyOfChange();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -215,10 +204,9 @@ const LayerEditorDialog = (props: Props) => {
|
||||
onChooseResource={props.onChooseResource}
|
||||
resourceExternalEditors={props.resourceExternalEditors}
|
||||
effectsContainer={layer.getEffects()}
|
||||
onEffectsUpdated={() => {
|
||||
forceUpdate(); /*Force update to ensure dialog is properly positioned*/
|
||||
notifyOfChange();
|
||||
}}
|
||||
onEffectsUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positioned*/
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
|
@@ -45,8 +45,8 @@ export const MaxProjectCountAlertMessage = ({ onUpgrade, limits }: Props) => {
|
||||
<Text>
|
||||
{canMaximumCountBeIncreased ? (
|
||||
<Trans>
|
||||
Update to GDevelop Premium to get more storage, leaderboards,
|
||||
and one-click packagings!
|
||||
Update to GDevelop Premium to get more storage, one click
|
||||
packagings, and a shiny unicorn!
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
|
@@ -206,7 +206,6 @@ export type PreferencesValues = {|
|
||||
eventsSheetCancelInlineParameter: 'cancel' | 'apply',
|
||||
showCommunityExtensions: boolean,
|
||||
showGetStartedSection: boolean,
|
||||
showEventBasedObjectsEditor: boolean,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -269,8 +268,6 @@ export type Preferences = {|
|
||||
setEventsSheetCancelInlineParameter: (value: string) => void,
|
||||
setShowCommunityExtensions: (enabled: boolean) => void,
|
||||
setShowGetStartedSection: (enabled: boolean) => void,
|
||||
setShowEventBasedObjectsEditor: (enabled: boolean) => void,
|
||||
getShowEventBasedObjectsEditor: () => boolean,
|
||||
|};
|
||||
|
||||
export const initialPreferences = {
|
||||
@@ -310,7 +307,6 @@ export const initialPreferences = {
|
||||
eventsSheetCancelInlineParameter: 'apply',
|
||||
showCommunityExtensions: false,
|
||||
showGetStartedSection: true,
|
||||
showEventBasedObjectsEditor: false,
|
||||
},
|
||||
setLanguage: () => {},
|
||||
setThemeName: () => {},
|
||||
@@ -363,8 +359,6 @@ export const initialPreferences = {
|
||||
setEventsSheetCancelInlineParameter: () => {},
|
||||
setShowCommunityExtensions: () => {},
|
||||
setShowGetStartedSection: (enabled: boolean) => {},
|
||||
setShowEventBasedObjectsEditor: (enabled: boolean) => {},
|
||||
getShowEventBasedObjectsEditor: () => false,
|
||||
};
|
||||
|
||||
const PreferencesContext = React.createContext<Preferences>(initialPreferences);
|
||||
|
@@ -58,7 +58,6 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
|
||||
setIsAlwaysOnTopInPreview,
|
||||
setEventsSheetCancelInlineParameter,
|
||||
setShowCommunityExtensions,
|
||||
setShowEventBasedObjectsEditor,
|
||||
} = React.useContext(PreferencesContext);
|
||||
|
||||
return (
|
||||
@@ -330,16 +329,6 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<Toggle
|
||||
onToggle={(e, check) => setShowEventBasedObjectsEditor(check)}
|
||||
toggled={values.showEventBasedObjectsEditor}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Show custom objects in the extension editor (experimental)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{electron && (
|
||||
<>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
|
@@ -145,12 +145,6 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
),
|
||||
setShowCommunityExtensions: this._setShowCommunityExtensions.bind(this),
|
||||
setShowGetStartedSection: this._setShowGetStartedSection.bind(this),
|
||||
setShowEventBasedObjectsEditor: this._setShowEventBasedObjectsEditor.bind(
|
||||
this
|
||||
),
|
||||
getShowEventBasedObjectsEditor: this._getShowEventBasedObjectsEditor.bind(
|
||||
this
|
||||
),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -358,22 +352,6 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
_setShowEventBasedObjectsEditor(showEventBasedObjectsEditor: boolean) {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
showEventBasedObjectsEditor,
|
||||
},
|
||||
}),
|
||||
() => this._persistValuesToLocalStorage(this.state)
|
||||
);
|
||||
}
|
||||
|
||||
_getShowEventBasedObjectsEditor() {
|
||||
return this.state.values.showEventBasedObjectsEditor;
|
||||
}
|
||||
|
||||
_checkUpdates(forceDownload?: boolean) {
|
||||
// Checking for updates is only done on Electron.
|
||||
// Note: This could be abstracted away later if other updates mechanisms
|
||||
|
@@ -16,7 +16,6 @@ export type EditorProps = {|
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
onSizeUpdated: () => void,
|
||||
onObjectUpdated?: () => void,
|
||||
objectName: string,
|
||||
unsavedChanges?: UnsavedChanges,
|
||||
|};
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import React from 'react';
|
||||
import { Column, Line } from '../../../UI/Grid';
|
||||
import { Column } from '../../../UI/Grid';
|
||||
import { LineStackLayout } from '../../../UI/Layout';
|
||||
import ImagePreview from '../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
import Replay from '@material-ui/icons/Replay';
|
||||
@@ -16,6 +16,9 @@ import useForceUpdate from '../../../Utils/UseForceUpdate';
|
||||
import PlaceholderLoader from '../../../UI/PlaceholderLoader';
|
||||
|
||||
const styles = {
|
||||
imageContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
loaderContainer: {
|
||||
position: 'absolute',
|
||||
left: 'calc(50% - 30px)',
|
||||
@@ -45,7 +48,6 @@ type Props = {|
|
||||
fixedHeight?: number,
|
||||
fixedWidth?: number,
|
||||
isAssetPrivate?: boolean,
|
||||
hideAnimationLoader?: boolean,
|
||||
|};
|
||||
|
||||
const AnimationPreview = ({
|
||||
@@ -63,7 +65,6 @@ const AnimationPreview = ({
|
||||
fixedHeight,
|
||||
fixedWidth,
|
||||
isAssetPrivate,
|
||||
hideAnimationLoader,
|
||||
}: Props) => {
|
||||
const forceUdpate = useForceUpdate();
|
||||
|
||||
@@ -80,7 +81,6 @@ const AnimationPreview = ({
|
||||
const timeBetweenFramesRef = React.useRef(timeBetweenFrames);
|
||||
const pausedRef = React.useRef(false);
|
||||
const currentFrameIndexRef = React.useRef(0);
|
||||
const currentResourceNameRef = React.useRef(resourceNames[0]);
|
||||
const isLoopingRef = React.useRef(isLooping);
|
||||
const animationNameRef = React.useRef(animationName);
|
||||
const imagesLoadedArray = React.useRef(
|
||||
@@ -161,28 +161,17 @@ const AnimationPreview = ({
|
||||
|
||||
currentFrameIndexRef.current = newFrameIndex;
|
||||
currentFrameElapsedTimeRef.current = newFrameElapsedTime;
|
||||
const newResourceName = resourceNames[currentFrameIndexRef.current];
|
||||
// Ensure we trigger an update if the frame changes,
|
||||
// Ensure we trigger an update if the animation changes,
|
||||
// as the refs will not do it.
|
||||
if (currentFrameIndex !== newFrameIndex) {
|
||||
if (newResourceName === currentResourceNameRef.current) {
|
||||
// Important: if the resource name is the same on the following frame,
|
||||
// it means the same image is used for multiple frames in the animation.
|
||||
// In this case, we can consider the image as already loaded.
|
||||
// Not doing so will cause the animation to be stuck on this frame,
|
||||
// as the image onLoad will never be triggered.
|
||||
imagesLoadedArray.current[currentFrameIndexRef.current] = true;
|
||||
} else {
|
||||
imagesLoadedArray.current[currentFrameIndexRef.current] = false;
|
||||
// When the array of loaders changes, wait a bit to display the loader to avoid flickering.
|
||||
loaderTimeout.current = setTimeout(() => {
|
||||
console.warn(
|
||||
'The image took too long to load, displaying a loader.'
|
||||
);
|
||||
setIsStillLoadingResources(true);
|
||||
}, 500);
|
||||
}
|
||||
currentResourceNameRef.current = newResourceName;
|
||||
imagesLoadedArray.current[currentFrameIndexRef.current] = false;
|
||||
// When the array of loaders changes, wait a bit to display the loader to avoid flickering.
|
||||
loaderTimeout.current = setTimeout(() => {
|
||||
console.warn(
|
||||
'The image took too long to load, displaying a loader.'
|
||||
);
|
||||
setIsStillLoadingResources(true);
|
||||
}, 500);
|
||||
forceUdpate();
|
||||
}
|
||||
}
|
||||
@@ -232,7 +221,7 @@ const AnimationPreview = ({
|
||||
|
||||
return (
|
||||
<Column expand noOverflowParent noMargin>
|
||||
<Line noMargin expand>
|
||||
<div style={styles.imageContainer}>
|
||||
<ImagePreview
|
||||
resourceName={resourceName}
|
||||
imageResourceSource={getImageResourceSource(resourceName)}
|
||||
@@ -247,12 +236,12 @@ const AnimationPreview = ({
|
||||
isImagePrivate={isAssetPrivate}
|
||||
hideLoader // Handled by the animation preview, important to let the browser cache the image.
|
||||
/>
|
||||
{!hideAnimationLoader && isStillLoadingResources && (
|
||||
{isStillLoadingResources && (
|
||||
<div style={styles.loaderContainer}>
|
||||
<PlaceholderLoader />
|
||||
</div>
|
||||
)}
|
||||
</Line>
|
||||
</div>
|
||||
{!hideControls && (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Text>
|
||||
|
@@ -46,15 +46,9 @@ type Props = {|
|
||||
objectConfiguration: gdSpriteObject,
|
||||
resourcesLoader: typeof ResourcesLoader,
|
||||
project: gdProject,
|
||||
onMasksUpdated?: () => void,
|
||||
|};
|
||||
|
||||
const CollisionMasksEditor = ({
|
||||
objectConfiguration,
|
||||
resourcesLoader,
|
||||
project,
|
||||
onMasksUpdated,
|
||||
}: Props) => {
|
||||
const CollisionMasksEditor = (props: Props) => {
|
||||
const [animationIndex, setAnimationIndex] = React.useState(0);
|
||||
const [directionIndex, setDirectionIndex] = React.useState(0);
|
||||
const [spriteIndex, setSpriteIndex] = React.useState(0);
|
||||
@@ -81,7 +75,9 @@ const CollisionMasksEditor = ({
|
||||
const [spriteHeight, setSpriteHeight] = React.useState(0);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const spriteConfiguration = gd.asSpriteConfiguration(objectConfiguration);
|
||||
const spriteConfiguration = gd.asSpriteConfiguration(
|
||||
props.objectConfiguration
|
||||
);
|
||||
const { animation, sprite } = getCurrentElements(
|
||||
spriteConfiguration,
|
||||
animationIndex,
|
||||
@@ -103,7 +99,6 @@ const CollisionMasksEditor = ({
|
||||
}
|
||||
|
||||
forceUpdate(); // Refresh the preview and the list
|
||||
if (onMasksUpdated) onMasksUpdated();
|
||||
},
|
||||
[
|
||||
animation,
|
||||
@@ -112,7 +107,6 @@ const CollisionMasksEditor = ({
|
||||
sameCollisionMasksForAnimations,
|
||||
sameCollisionMasksForSprites,
|
||||
forceUpdate,
|
||||
onMasksUpdated,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -203,7 +197,7 @@ const CollisionMasksEditor = ({
|
||||
const editorNodes =
|
||||
screenSize === 'small' ? verticalMosaicNodes : horizontalMosaicNodes;
|
||||
|
||||
if (!objectConfiguration.getAnimationsCount()) return null;
|
||||
if (!props.objectConfiguration.getAnimationsCount()) return null;
|
||||
const resourceName = sprite ? sprite.getImageName() : '';
|
||||
|
||||
const editors: { [string]: Editor } = {
|
||||
@@ -214,16 +208,16 @@ const CollisionMasksEditor = ({
|
||||
<Background>
|
||||
<ImagePreview
|
||||
resourceName={resourceName}
|
||||
imageResourceSource={resourcesLoader.getResourceFullUrl(
|
||||
project,
|
||||
imageResourceSource={props.resourcesLoader.getResourceFullUrl(
|
||||
props.project,
|
||||
resourceName,
|
||||
{}
|
||||
)}
|
||||
isImageResourceSmooth={isProjectImageResourceSmooth(
|
||||
project,
|
||||
props.project,
|
||||
resourceName
|
||||
)}
|
||||
project={project}
|
||||
project={props.project}
|
||||
onSize={setCurrentSpriteSize}
|
||||
renderOverlay={overlayProps =>
|
||||
sprite && (
|
||||
|
@@ -185,7 +185,6 @@ export default class DirectionTools extends Component<Props, State> {
|
||||
this.setState({ timeBetweenFrames: text })
|
||||
}
|
||||
isLooping={direction.isLooping()}
|
||||
hideAnimationLoader // No need to show a loader in the Direction Tools.
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
|
@@ -46,15 +46,9 @@ type Props = {|
|
||||
objectConfiguration: gdSpriteObject,
|
||||
resourcesLoader: typeof ResourcesLoader,
|
||||
project: gdProject,
|
||||
onPointsUpdated?: () => void,
|
||||
|};
|
||||
|
||||
const PointsEditor = ({
|
||||
objectConfiguration,
|
||||
resourcesLoader,
|
||||
project,
|
||||
onPointsUpdated,
|
||||
}: Props) => {
|
||||
const PointsEditor = (props: Props) => {
|
||||
const [animationIndex, setAnimationIndex] = React.useState(0);
|
||||
const [directionIndex, setDirectionIndex] = React.useState(0);
|
||||
const [spriteIndex, setSpriteIndex] = React.useState(0);
|
||||
@@ -76,7 +70,9 @@ const PointsEditor = ({
|
||||
const [samePointsForSprites, setSamePointsForSprites] = React.useState(false);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const spriteConfiguration = gd.asSpriteConfiguration(objectConfiguration);
|
||||
const spriteConfiguration = gd.asSpriteConfiguration(
|
||||
props.objectConfiguration
|
||||
);
|
||||
const { animation, sprite } = getCurrentElements(
|
||||
spriteConfiguration,
|
||||
animationIndex,
|
||||
@@ -98,7 +94,6 @@ const PointsEditor = ({
|
||||
}
|
||||
|
||||
forceUpdate(); // Refresh the preview
|
||||
if (onPointsUpdated) onPointsUpdated();
|
||||
},
|
||||
[
|
||||
animation,
|
||||
@@ -107,7 +102,6 @@ const PointsEditor = ({
|
||||
samePointsForAnimations,
|
||||
samePointsForSprites,
|
||||
forceUpdate,
|
||||
onPointsUpdated,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -180,7 +174,7 @@ const PointsEditor = ({
|
||||
const editorNodes =
|
||||
screenSize === 'small' ? verticalMosaicNodes : horizontalMosaicNodes;
|
||||
|
||||
if (!objectConfiguration.getAnimationsCount()) return null;
|
||||
if (!props.objectConfiguration.getAnimationsCount()) return null;
|
||||
const resourceName = sprite ? sprite.getImageName() : '';
|
||||
|
||||
const editors: { [string]: Editor } = {
|
||||
@@ -191,16 +185,16 @@ const PointsEditor = ({
|
||||
<Background>
|
||||
<ImagePreview
|
||||
resourceName={resourceName}
|
||||
imageResourceSource={resourcesLoader.getResourceFullUrl(
|
||||
project,
|
||||
imageResourceSource={props.resourcesLoader.getResourceFullUrl(
|
||||
props.project,
|
||||
resourceName,
|
||||
{}
|
||||
)}
|
||||
isImageResourceSmooth={isProjectImageResourceSmooth(
|
||||
project,
|
||||
props.project,
|
||||
resourceName
|
||||
)}
|
||||
project={project}
|
||||
project={props.project}
|
||||
renderOverlay={overlayProps =>
|
||||
sprite && (
|
||||
<PointsPreview
|
||||
|
@@ -198,7 +198,6 @@ type AnimationsListContainerProps = {|
|
||||
extraBottomTools: React.Node,
|
||||
onSizeUpdated: () => void,
|
||||
objectName: string,
|
||||
onObjectUpdated?: () => void,
|
||||
|};
|
||||
|
||||
type AnimationsListContainerState = {|
|
||||
@@ -228,7 +227,6 @@ class AnimationsListContainer extends React.Component<
|
||||
emptyAnimation.delete();
|
||||
this.forceUpdate();
|
||||
this.props.onSizeUpdated();
|
||||
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
|
||||
};
|
||||
|
||||
removeAnimation = i => {
|
||||
@@ -240,7 +238,6 @@ class AnimationsListContainer extends React.Component<
|
||||
this.props.spriteConfiguration.removeAnimation(i);
|
||||
this.forceUpdate();
|
||||
this.props.onSizeUpdated();
|
||||
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -267,7 +264,6 @@ class AnimationsListContainer extends React.Component<
|
||||
|
||||
spriteConfiguration.getAnimation(i).setName(newName);
|
||||
this.forceUpdate();
|
||||
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
|
||||
};
|
||||
|
||||
deleteSelection = () => {
|
||||
@@ -281,7 +277,6 @@ class AnimationsListContainer extends React.Component<
|
||||
this.setState({
|
||||
selectedSprites: {},
|
||||
});
|
||||
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
|
||||
};
|
||||
|
||||
duplicateSelection = () => {
|
||||
@@ -295,7 +290,6 @@ class AnimationsListContainer extends React.Component<
|
||||
this.setState({
|
||||
selectedSprites: {},
|
||||
});
|
||||
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
|
||||
};
|
||||
|
||||
openSpriteContextMenu = (x, y, sprite, index) => {
|
||||
@@ -317,7 +311,6 @@ class AnimationsListContainer extends React.Component<
|
||||
.getAnimation(animationId)
|
||||
.setDirection(newDirection, directionId);
|
||||
this.forceUpdate();
|
||||
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -400,7 +393,6 @@ export default function SpriteEditor({
|
||||
onChooseResource,
|
||||
resourceExternalEditors,
|
||||
onSizeUpdated,
|
||||
onObjectUpdated,
|
||||
objectName,
|
||||
}: EditorProps) {
|
||||
const [pointsEditorOpen, setPointsEditorOpen] = React.useState(false);
|
||||
@@ -423,7 +415,6 @@ export default function SpriteEditor({
|
||||
project={project}
|
||||
objectName={objectName}
|
||||
onSizeUpdated={onSizeUpdated}
|
||||
onObjectUpdated={onObjectUpdated}
|
||||
extraBottomTools={
|
||||
<ResponsiveLineStackLayout noMargin noColumnMargin>
|
||||
<RaisedButton
|
||||
@@ -475,7 +466,6 @@ export default function SpriteEditor({
|
||||
spriteConfiguration.setUpdateIfNotVisible(!value);
|
||||
|
||||
forceUpdate();
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
@@ -508,7 +498,6 @@ export default function SpriteEditor({
|
||||
objectConfiguration={spriteConfiguration}
|
||||
resourcesLoader={ResourcesLoader}
|
||||
project={project}
|
||||
onPointsUpdated={onObjectUpdated}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
@@ -539,7 +528,6 @@ export default function SpriteEditor({
|
||||
objectConfiguration={spriteConfiguration}
|
||||
resourcesLoader={ResourcesLoader}
|
||||
project={project}
|
||||
onMasksUpdated={onObjectUpdated}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
|
@@ -56,9 +56,6 @@ type Props = {|
|
||||
onUpdateBehaviorsSharedData: () => void,
|
||||
initialTab: ?ObjectEditorTab,
|
||||
|
||||
// Passed down to the behaviors editor:
|
||||
eventsFunctionsExtension?: gdEventsFunctionsExtension,
|
||||
|
||||
// Preview:
|
||||
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|
||||
|};
|
||||
@@ -77,10 +74,7 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
);
|
||||
const [newObjectName, setNewObjectName] = React.useState(props.objectName);
|
||||
const forceUpdate = useForceUpdate();
|
||||
const {
|
||||
onCancelChanges,
|
||||
notifyOfChange,
|
||||
} = useSerializableObjectCancelableEditor({
|
||||
const onCancelChanges = useSerializableObjectCancelableEditor({
|
||||
serializableObject: props.object,
|
||||
useProjectToUnserialize: props.project,
|
||||
onCancel: props.onCancel,
|
||||
@@ -208,7 +202,6 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
|
||||
if (props.canRenameObject(text)) {
|
||||
setNewObjectName(text);
|
||||
notifyOfChange();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -224,7 +217,6 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
forceUpdate /*Force update to ensure dialog is properly positionned*/
|
||||
}
|
||||
objectName={props.objectName}
|
||||
onObjectUpdated={notifyOfChange}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
@@ -232,7 +224,6 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
<BehaviorsEditor
|
||||
object={props.object}
|
||||
project={props.project}
|
||||
eventsFunctionsExtension={props.eventsFunctionsExtension}
|
||||
resourceSources={props.resourceSources}
|
||||
onChooseResource={props.onChooseResource}
|
||||
resourceExternalEditors={props.resourceExternalEditors}
|
||||
@@ -240,7 +231,6 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
forceUpdate /*Force update to ensure dialog is properly positionned*/
|
||||
}
|
||||
onUpdateBehaviorsSharedData={props.onUpdateBehaviorsSharedData}
|
||||
onBehaviorsUpdated={notifyOfChange}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'variables' && (
|
||||
@@ -264,7 +254,6 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
}
|
||||
helpPagePath={'/all-features/variables/object-variables'}
|
||||
onComputeAllVariableNames={props.onComputeAllVariableNames}
|
||||
onVariablesUpdated={notifyOfChange}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
@@ -276,10 +265,9 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
onChooseResource={props.onChooseResource}
|
||||
resourceExternalEditors={props.resourceExternalEditors}
|
||||
effectsContainer={props.object.getEffects()}
|
||||
onEffectsUpdated={() => {
|
||||
forceUpdate(); /*Force update to ensure dialog is properly positionned*/
|
||||
notifyOfChange();
|
||||
}}
|
||||
onEffectsUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positionned*/
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
|
@@ -25,10 +25,7 @@ const ObjectGroupEditorDialog = ({
|
||||
objectsContainer,
|
||||
}: Props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const {
|
||||
onCancelChanges,
|
||||
notifyOfChange,
|
||||
} = useSerializableObjectCancelableEditor({
|
||||
const onCancelChanges = useSerializableObjectCancelableEditor({
|
||||
serializableObject: group,
|
||||
onCancel,
|
||||
});
|
||||
@@ -64,7 +61,6 @@ const ObjectGroupEditorDialog = ({
|
||||
onSizeUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positionned*/
|
||||
}
|
||||
onObjectGroupUpdated={notifyOfChange}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
|
@@ -22,7 +22,6 @@ type Props = {|
|
||||
globalObjectsContainer: gdObjectsContainer,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
onSizeUpdated?: () => void,
|
||||
onObjectGroupUpdated?: () => void,
|
||||
|};
|
||||
|
||||
const ObjectGroupEditor = ({
|
||||
@@ -31,23 +30,20 @@ const ObjectGroupEditor = ({
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
onSizeUpdated,
|
||||
onObjectGroupUpdated,
|
||||
}: Props) => {
|
||||
const [objectName, setObjectName] = React.useState<string>('');
|
||||
const [newObjectName, setNewObjectName] = React.useState<string>('');
|
||||
const objectsInGroup = group.getAllObjectsNames().toJSArray();
|
||||
|
||||
const removeObject = (objectName: string) => {
|
||||
group.removeObject(objectName);
|
||||
|
||||
if (onSizeUpdated) onSizeUpdated();
|
||||
if (onObjectGroupUpdated) onObjectGroupUpdated();
|
||||
};
|
||||
|
||||
const addObject = (objectName: string) => {
|
||||
group.addObject(objectName);
|
||||
setObjectName('');
|
||||
setNewObjectName('');
|
||||
if (onSizeUpdated) onSizeUpdated();
|
||||
if (onObjectGroupUpdated) onObjectGroupUpdated();
|
||||
};
|
||||
|
||||
const renderExplanation = () => {
|
||||
@@ -118,9 +114,9 @@ const ObjectGroupEditor = ({
|
||||
project={project}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
objectsContainer={objectsContainer}
|
||||
value={objectName}
|
||||
value={newObjectName}
|
||||
excludedObjectOrGroupNames={objectsInGroup}
|
||||
onChange={setObjectName}
|
||||
onChange={setNewObjectName}
|
||||
onChoose={addObject}
|
||||
openOnFocus
|
||||
noGroups
|
||||
|
@@ -813,24 +813,19 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
placeholder={t`Search objects`}
|
||||
/>
|
||||
{newObjectDialogOpen && (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<NewObjectDialog
|
||||
onClose={() => setNewObjectDialogOpen(false)}
|
||||
onCreateNewObject={addObject}
|
||||
onObjectAddedFromAsset={onObjectAddedFromAsset}
|
||||
project={project}
|
||||
layout={layout}
|
||||
objectsContainer={objectsContainer}
|
||||
resourceSources={resourceSources}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
onFetchNewlyAddedResources={onFetchNewlyAddedResources}
|
||||
canInstallPrivateAsset={canInstallPrivateAsset}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
<NewObjectDialog
|
||||
onClose={() => setNewObjectDialogOpen(false)}
|
||||
onCreateNewObject={addObject}
|
||||
onObjectAddedFromAsset={onObjectAddedFromAsset}
|
||||
project={project}
|
||||
layout={layout}
|
||||
objectsContainer={objectsContainer}
|
||||
resourceSources={resourceSources}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
onFetchNewlyAddedResources={onFetchNewlyAddedResources}
|
||||
canInstallPrivateAsset={canInstallPrivateAsset}
|
||||
/>
|
||||
)}
|
||||
{tagEditedObject && (
|
||||
<EditTagsDialog
|
||||
|
@@ -23,9 +23,8 @@ import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import Text from '../UI/Text';
|
||||
|
||||
type Props = {|
|
||||
type Props = {
|
||||
loadingScreen: gdLoadingScreen,
|
||||
onLoadingScreenUpdated: () => void,
|
||||
onChangeSubscription: () => void,
|
||||
|
||||
// For resources:
|
||||
@@ -33,11 +32,10 @@ type Props = {|
|
||||
resourceSources: Array<ResourceSource>,
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
|};
|
||||
};
|
||||
|
||||
export const LoadingScreenEditor = ({
|
||||
loadingScreen,
|
||||
onLoadingScreenUpdated,
|
||||
onChangeSubscription,
|
||||
project,
|
||||
resourceSources,
|
||||
@@ -47,11 +45,6 @@ export const LoadingScreenEditor = ({
|
||||
const subscriptionChecker = React.useRef<?SubscriptionChecker>(null);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const onUpdate = () => {
|
||||
forceUpdate();
|
||||
onLoadingScreenUpdated();
|
||||
};
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -68,11 +61,9 @@ export const LoadingScreenEditor = ({
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
resourceKind="image"
|
||||
resourceName={loadingScreen.getBackgroundImageResourceName()}
|
||||
onChange={newResourceName => {
|
||||
const currentResourceName = loadingScreen.getBackgroundImageResourceName();
|
||||
if (currentResourceName === newResourceName) return;
|
||||
loadingScreen.setBackgroundImageResourceName(newResourceName);
|
||||
onUpdate();
|
||||
onChange={resourceName => {
|
||||
loadingScreen.setBackgroundImageResourceName(resourceName);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
@@ -82,12 +73,9 @@ export const LoadingScreenEditor = ({
|
||||
floatingLabelText={<Trans>Background color</Trans>}
|
||||
disableAlpha
|
||||
color={hexNumberToRGBString(loadingScreen.getBackgroundColor())}
|
||||
onChange={newColor => {
|
||||
const currentBackgroundColor = loadingScreen.getBackgroundColor();
|
||||
const newBackgroundColor = rgbStringToHexNumber(newColor);
|
||||
if (currentBackgroundColor === newBackgroundColor) return;
|
||||
loadingScreen.setBackgroundColor(newBackgroundColor);
|
||||
onUpdate();
|
||||
onChange={color => {
|
||||
loadingScreen.setBackgroundColor(rgbStringToHexNumber(color));
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
@@ -98,21 +86,11 @@ export const LoadingScreenEditor = ({
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + loadingScreen.getBackgroundFadeInDuration()}
|
||||
onChange={newValue => {
|
||||
const currentBackgroundFadeInDuration = loadingScreen.getBackgroundFadeInDuration();
|
||||
const newBackgroundFadeInDuration = Math.max(
|
||||
0,
|
||||
parseFloat(newValue)
|
||||
);
|
||||
if (
|
||||
currentBackgroundFadeInDuration ===
|
||||
newBackgroundFadeInDuration
|
||||
)
|
||||
return;
|
||||
onChange={value => {
|
||||
loadingScreen.setBackgroundFadeInDuration(
|
||||
newBackgroundFadeInDuration
|
||||
Math.max(0, parseFloat(value))
|
||||
);
|
||||
onUpdate();
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
@@ -133,18 +111,16 @@ export const LoadingScreenEditor = ({
|
||||
return;
|
||||
}
|
||||
loadingScreen.showGDevelopSplash(checked);
|
||||
onUpdate();
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>GDevelop logo style</Trans>}
|
||||
value={loadingScreen.getGDevelopLogoStyle()}
|
||||
onChange={(e, i, newGdevelopLogoStyle: string) => {
|
||||
const currentGDevelopLogoStyle = loadingScreen.getGDevelopLogoStyle();
|
||||
if (currentGDevelopLogoStyle === newGdevelopLogoStyle) return;
|
||||
loadingScreen.setGDevelopLogoStyle(newGdevelopLogoStyle);
|
||||
onUpdate();
|
||||
onChange={(e, i, value: string) => {
|
||||
loadingScreen.setGDevelopLogoStyle(value);
|
||||
forceUpdate();
|
||||
}}
|
||||
>
|
||||
<SelectOption value="light" primaryText={t`Light (plain)`} />
|
||||
@@ -167,21 +143,11 @@ export const LoadingScreenEditor = ({
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + loadingScreen.getLogoAndProgressLogoFadeInDelay()}
|
||||
onChange={newValue => {
|
||||
const currentLogoAndProgressLogoFadeInDelay = loadingScreen.getLogoAndProgressLogoFadeInDelay();
|
||||
const newLogoAndProgressLogoFadeInDelay = Math.max(
|
||||
0,
|
||||
parseFloat(newValue)
|
||||
);
|
||||
if (
|
||||
currentLogoAndProgressLogoFadeInDelay ===
|
||||
newLogoAndProgressLogoFadeInDelay
|
||||
)
|
||||
return;
|
||||
onChange={value => {
|
||||
loadingScreen.setLogoAndProgressLogoFadeInDelay(
|
||||
newLogoAndProgressLogoFadeInDelay
|
||||
Math.max(0, parseFloat(value))
|
||||
);
|
||||
onUpdate();
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
@@ -192,21 +158,11 @@ export const LoadingScreenEditor = ({
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + loadingScreen.getLogoAndProgressFadeInDuration()}
|
||||
onChange={newValue => {
|
||||
const currentLogoAndProgressFadeInDuration = loadingScreen.getLogoAndProgressFadeInDuration();
|
||||
const newLogoAndProgressFadeInDuration = Math.max(
|
||||
0,
|
||||
parseFloat(newValue)
|
||||
);
|
||||
if (
|
||||
currentLogoAndProgressFadeInDuration ===
|
||||
newLogoAndProgressFadeInDuration
|
||||
)
|
||||
return;
|
||||
onChange={value => {
|
||||
loadingScreen.setLogoAndProgressFadeInDuration(
|
||||
newLogoAndProgressFadeInDuration
|
||||
Math.max(0, parseFloat(value))
|
||||
);
|
||||
onUpdate();
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
@@ -218,7 +174,7 @@ export const LoadingScreenEditor = ({
|
||||
checked={loadingScreen.getShowProgressBar()}
|
||||
onCheck={(e, checked) => {
|
||||
loadingScreen.setShowProgressBar(checked);
|
||||
onUpdate();
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
@@ -227,17 +183,11 @@ export const LoadingScreenEditor = ({
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + loadingScreen.getProgressBarMinWidth()}
|
||||
onChange={newValue => {
|
||||
const currentProgressBarMinWidth = loadingScreen.getProgressBarMinWidth();
|
||||
const newProgressBarMinWidth = Math.max(
|
||||
0,
|
||||
parseFloat(newValue)
|
||||
onChange={value => {
|
||||
loadingScreen.setProgressBarMinWidth(
|
||||
Math.max(0, parseFloat(value))
|
||||
);
|
||||
if (currentProgressBarMinWidth === newProgressBarMinWidth) {
|
||||
return;
|
||||
}
|
||||
loadingScreen.setProgressBarMinWidth(newProgressBarMinWidth);
|
||||
onUpdate();
|
||||
forceUpdate();
|
||||
}}
|
||||
helperMarkdownText={i18n._(t`In pixels. 0 to ignore.`)}
|
||||
/>
|
||||
@@ -246,22 +196,11 @@ export const LoadingScreenEditor = ({
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + loadingScreen.getProgressBarWidthPercent()}
|
||||
onChange={newValue => {
|
||||
const currentProgressBarWidthPercent = loadingScreen.getProgressBarWidthPercent();
|
||||
const newProgressBarWidthPercent = Math.min(
|
||||
100,
|
||||
Math.max(1, parseFloat(newValue))
|
||||
);
|
||||
if (
|
||||
currentProgressBarWidthPercent === newProgressBarWidthPercent
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange={value => {
|
||||
loadingScreen.setProgressBarWidthPercent(
|
||||
newProgressBarWidthPercent
|
||||
Math.min(100, Math.max(1, parseFloat(value)))
|
||||
);
|
||||
onUpdate();
|
||||
forceUpdate();
|
||||
}}
|
||||
helperMarkdownText={i18n._(t`As a percent of the game width.`)}
|
||||
/>
|
||||
@@ -270,17 +209,11 @@ export const LoadingScreenEditor = ({
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + loadingScreen.getProgressBarMaxWidth()}
|
||||
onChange={newValue => {
|
||||
const currentProgressBarMaxWidth = loadingScreen.getProgressBarMaxWidth();
|
||||
const newProgressBarMaxWidth = Math.max(
|
||||
0,
|
||||
parseFloat(newValue)
|
||||
onChange={value => {
|
||||
loadingScreen.setProgressBarMaxWidth(
|
||||
Math.max(0, parseFloat(value))
|
||||
);
|
||||
if (currentProgressBarMaxWidth === newProgressBarMaxWidth) {
|
||||
return;
|
||||
}
|
||||
loadingScreen.setProgressBarMaxWidth(newProgressBarMaxWidth);
|
||||
onUpdate();
|
||||
forceUpdate();
|
||||
}}
|
||||
helperMarkdownText={i18n._(t`In pixels. 0 to ignore.`)}
|
||||
/>
|
||||
@@ -290,14 +223,11 @@ export const LoadingScreenEditor = ({
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + loadingScreen.getProgressBarHeight()}
|
||||
onChange={newValue => {
|
||||
const currentProgressBarHeight = loadingScreen.getProgressBarHeight();
|
||||
const newProgressBarHeight = Math.max(1, parseFloat(newValue));
|
||||
if (currentProgressBarHeight === newProgressBarHeight) {
|
||||
return;
|
||||
}
|
||||
loadingScreen.setProgressBarHeight(newProgressBarHeight);
|
||||
onUpdate();
|
||||
onChange={value => {
|
||||
loadingScreen.setProgressBarHeight(
|
||||
Math.max(1, parseFloat(value))
|
||||
);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<ColorField
|
||||
@@ -305,14 +235,9 @@ export const LoadingScreenEditor = ({
|
||||
floatingLabelText={<Trans>Progress bar color</Trans>}
|
||||
disableAlpha
|
||||
color={hexNumberToRGBString(loadingScreen.getProgressBarColor())}
|
||||
onChange={newColor => {
|
||||
const currentProgressBarColor = loadingScreen.getProgressBarColor();
|
||||
const newProgressBarColor = rgbStringToHexNumber(newColor);
|
||||
if (currentProgressBarColor === newProgressBarColor) {
|
||||
return;
|
||||
}
|
||||
loadingScreen.setProgressBarColor(newProgressBarColor);
|
||||
onUpdate();
|
||||
onChange={color => {
|
||||
loadingScreen.setProgressBarColor(rgbStringToHexNumber(color));
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<Text size="block-title">
|
||||
@@ -326,14 +251,9 @@ export const LoadingScreenEditor = ({
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + loadingScreen.getMinDuration()}
|
||||
onChange={newValue => {
|
||||
const currentMinDuration = loadingScreen.getMinDuration();
|
||||
const newMinDuration = Math.max(0, parseFloat(newValue));
|
||||
if (currentMinDuration === newMinDuration) {
|
||||
return;
|
||||
}
|
||||
loadingScreen.setMinDuration(newMinDuration);
|
||||
onUpdate();
|
||||
onChange={value => {
|
||||
loadingScreen.setMinDuration(Math.max(0, parseFloat(value)));
|
||||
forceUpdate();
|
||||
}}
|
||||
helperMarkdownText={i18n._(
|
||||
t`When previewing the game in the editor, this duration is ignored (the game preview starts as soon as possible).`
|
||||
|
@@ -196,17 +196,11 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
'properties' | 'loading-screen'
|
||||
>(props.initialTab);
|
||||
|
||||
const {
|
||||
onCancelChanges: onCancelLoadingScreenChanges,
|
||||
notifyOfChange: notifyOfLoadingScreenChange,
|
||||
} = useSerializableObjectCancelableEditor({
|
||||
const onCancelLoadingScreenChanges = useSerializableObjectCancelableEditor({
|
||||
serializableObject: project.getLoadingScreen(),
|
||||
onCancel: props.onClose,
|
||||
});
|
||||
const {
|
||||
onCancelChanges,
|
||||
notifyOfChange,
|
||||
} = useSerializableObjectCancelableEditor({
|
||||
const onCancelChanges = useSerializableObjectCancelableEditor({
|
||||
serializableObject: project.getExtensionProperties(),
|
||||
onCancel: onCancelLoadingScreenChanges,
|
||||
});
|
||||
@@ -308,35 +302,16 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
</Text>
|
||||
<PublicGameProperties
|
||||
name={name}
|
||||
setName={newName => {
|
||||
if (newName.trim() === name) {
|
||||
return;
|
||||
}
|
||||
setName(newName.trim());
|
||||
notifyOfChange();
|
||||
}}
|
||||
setName={newName => setName(newName.trim())}
|
||||
description={description}
|
||||
setDescription={newDescription => {
|
||||
if (newDescription === description) {
|
||||
return;
|
||||
}
|
||||
setDescription(newDescription.trim());
|
||||
notifyOfChange();
|
||||
}}
|
||||
setDescription={newDescription =>
|
||||
setDescription(newDescription.trim())
|
||||
}
|
||||
project={project}
|
||||
authorIds={authorIds}
|
||||
setAuthorIds={newAuthorIds => {
|
||||
setAuthorIds(newAuthorIds);
|
||||
notifyOfChange();
|
||||
}}
|
||||
setAuthorIds={setAuthorIds}
|
||||
orientation={orientation}
|
||||
setOrientation={newOrientation => {
|
||||
if (newOrientation === orientation) {
|
||||
return;
|
||||
}
|
||||
setOrientation(newOrientation);
|
||||
notifyOfChange();
|
||||
}}
|
||||
setOrientation={setOrientation}
|
||||
/>
|
||||
<Text size="block-title">
|
||||
<Trans>Packaging</Trans>
|
||||
@@ -349,13 +324,7 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
hintText={defaultPackageName}
|
||||
type="text"
|
||||
value={packageName}
|
||||
onChange={newPackageName => {
|
||||
if (newPackageName === packageName) {
|
||||
return;
|
||||
}
|
||||
setPackageName(newPackageName);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={setPackageName}
|
||||
errorText={
|
||||
validatePackageName(packageName) ? (
|
||||
undefined
|
||||
@@ -374,13 +343,7 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
hintText={defaultVersion}
|
||||
type="text"
|
||||
value={version}
|
||||
onChange={newVersion => {
|
||||
if (newVersion === version) {
|
||||
return;
|
||||
}
|
||||
setVersion(newVersion);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={setVersion}
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Publisher name</Trans>}
|
||||
@@ -391,13 +354,7 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
)}
|
||||
type="text"
|
||||
value={author}
|
||||
onChange={newAuthor => {
|
||||
if (newAuthor === author) {
|
||||
return;
|
||||
}
|
||||
setAuthor(newAuthor);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={setAuthor}
|
||||
/>
|
||||
{useDeprecatedZeroAsDefaultZOrder ? (
|
||||
<React.Fragment>
|
||||
@@ -427,7 +384,6 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
if (!answer) return;
|
||||
|
||||
setUseDeprecatedZeroAsDefaultZOrder(false);
|
||||
notifyOfChange();
|
||||
}}
|
||||
label={
|
||||
<Trans>
|
||||
@@ -451,34 +407,18 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + gameResolutionWidth}
|
||||
onChange={value => {
|
||||
const newResolutionWidth = Math.max(
|
||||
1,
|
||||
parseInt(value, 10)
|
||||
);
|
||||
if (newResolutionWidth === gameResolutionWidth) {
|
||||
return;
|
||||
}
|
||||
setGameResolutionWidth(newResolutionWidth);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={value =>
|
||||
setGameResolutionWidth(Math.max(1, parseInt(value, 10)))
|
||||
}
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Game resolution height</Trans>}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + gameResolutionHeight}
|
||||
onChange={value => {
|
||||
const newResolutionHeight = Math.max(
|
||||
1,
|
||||
parseInt(value, 10)
|
||||
);
|
||||
if (newResolutionHeight === gameResolutionHeight) {
|
||||
return;
|
||||
}
|
||||
setGameResolutionHeight(newResolutionHeight);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={value =>
|
||||
setGameResolutionHeight(Math.max(1, parseInt(value, 10)))
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<SelectField
|
||||
@@ -489,13 +429,9 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
</Trans>
|
||||
}
|
||||
value={sizeOnStartupMode}
|
||||
onChange={(e, i, newSizeOnStartupMode: string) => {
|
||||
if (newSizeOnStartupMode === sizeOnStartupMode) {
|
||||
return;
|
||||
}
|
||||
setSizeOnStartupMode(newSizeOnStartupMode);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={(e, i, value: string) =>
|
||||
setSizeOnStartupMode(value)
|
||||
}
|
||||
>
|
||||
<SelectOption
|
||||
value=""
|
||||
@@ -519,10 +455,9 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
}
|
||||
disabled={sizeOnStartupMode === ''}
|
||||
checked={adaptGameResolutionAtRuntime}
|
||||
onCheck={(e, checked) => {
|
||||
setAdaptGameResolutionAtRuntime(checked);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onCheck={(e, checked) =>
|
||||
setAdaptGameResolutionAtRuntime(checked)
|
||||
}
|
||||
/>
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
<SemiControlledTextField
|
||||
@@ -530,14 +465,9 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + minFPS}
|
||||
onChange={value => {
|
||||
const newMinFPS = Math.max(0, parseInt(value, 10));
|
||||
if (newMinFPS === minFPS) {
|
||||
return;
|
||||
}
|
||||
setMinFPS(newMinFPS);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={value =>
|
||||
setMinFPS(Math.max(0, parseInt(value, 10)))
|
||||
}
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={
|
||||
@@ -546,14 +476,9 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
fullWidth
|
||||
type="number"
|
||||
value={'' + maxFPS}
|
||||
onChange={value => {
|
||||
const newMaxFPS = Math.max(0, parseInt(value, 10));
|
||||
if (newMaxFPS === maxFPS) {
|
||||
return;
|
||||
}
|
||||
setMaxFPS(newMaxFPS);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={value =>
|
||||
setMaxFPS(Math.max(0, parseInt(value, 10)))
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
{maxFPS > 0 && maxFPS < 60 && (
|
||||
@@ -592,13 +517,7 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
<Trans>Scale mode (also called "Sampling")</Trans>
|
||||
}
|
||||
value={scaleMode}
|
||||
onChange={(e, i, newScaleMode: string) => {
|
||||
if (newScaleMode === scaleMode) {
|
||||
return;
|
||||
}
|
||||
setScaleMode(newScaleMode);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={(e, i, value: string) => setScaleMode(value)}
|
||||
>
|
||||
<SelectOption
|
||||
value="linear"
|
||||
@@ -617,10 +536,7 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
</Trans>
|
||||
}
|
||||
checked={pixelsRounding}
|
||||
onCheck={(e, checked) => {
|
||||
setPixelsRounding(checked);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onCheck={(e, checked) => setPixelsRounding(checked)}
|
||||
/>
|
||||
{scaleMode === 'nearest' && (
|
||||
<DismissableAlertMessage
|
||||
@@ -654,14 +570,9 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Project file type</Trans>}
|
||||
value={isFolderProject ? 'folder-project' : 'single-file'}
|
||||
onChange={(e, i, value: string) => {
|
||||
const newIsFolderProject = value === 'folder-project';
|
||||
if (newIsFolderProject === isFolderProject) {
|
||||
return;
|
||||
}
|
||||
setIsFolderProject(newIsFolderProject);
|
||||
notifyOfChange();
|
||||
}}
|
||||
onChange={(e, i, value: string) =>
|
||||
setIsFolderProject(value === 'folder-project')
|
||||
}
|
||||
helperMarkdownText={i18n._(
|
||||
t`Note that this option will only have an effect when saving your project on your computer's filesystem from the desktop app.`
|
||||
)}
|
||||
@@ -681,7 +592,6 @@ function ProjectPropertiesDialog(props: Props) {
|
||||
{currentTab === 'loading-screen' && (
|
||||
<LoadingScreenEditor
|
||||
loadingScreen={project.getLoadingScreen()}
|
||||
onLoadingScreenUpdated={notifyOfLoadingScreenChange}
|
||||
onChangeSubscription={() => {
|
||||
onCancelChanges();
|
||||
props.onChangeSubscription();
|
||||
|
@@ -11,11 +11,28 @@ import {
|
||||
} from '../../Utils/BlobDownloader';
|
||||
import { type FileMetadata } from '../index';
|
||||
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
extractFilenameWithExtensionFromProductAuthorizedUrl,
|
||||
isProductAuthorizedResourceUrl,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { isBlobURL, isURL } from '../../ResourcesList/ResourceUtils';
|
||||
import { extractFilenameAndExtensionFromProductAuthorizedUrl } from '../../Utils/GDevelopServices/Shop';
|
||||
|
||||
const isURL = (filename: string) => {
|
||||
return (
|
||||
filename.startsWith('http://') ||
|
||||
filename.startsWith('https://') ||
|
||||
filename.startsWith('ftp://') ||
|
||||
filename.startsWith('blob:') ||
|
||||
filename.startsWith('data:')
|
||||
);
|
||||
};
|
||||
|
||||
const isPrivateAssetUrl = (filename: string) => {
|
||||
return (
|
||||
filename.startsWith('https://private-assets-dev.gdevelop.io') ||
|
||||
filename.startsWith('https://private-assets.gdevelop.io')
|
||||
);
|
||||
};
|
||||
|
||||
const isBlobURL = (filename: string) => {
|
||||
return filename.startsWith('blob:');
|
||||
};
|
||||
|
||||
export const moveUrlResourcesToCloudFilesIfPrivate = async ({
|
||||
project,
|
||||
@@ -56,14 +73,17 @@ export const moveUrlResourcesToCloudFilesIfPrivate = async ({
|
||||
const resourceFile = resource.getFile();
|
||||
|
||||
if (isURL(resourceFile)) {
|
||||
if (isProductAuthorizedResourceUrl(resourceFile)) {
|
||||
const filenameWithExtension = extractFilenameWithExtensionFromProductAuthorizedUrl(
|
||||
if (isPrivateAssetUrl(resourceFile)) {
|
||||
const {
|
||||
extension,
|
||||
filenameWithoutExtension,
|
||||
} = extractFilenameAndExtensionFromProductAuthorizedUrl(
|
||||
resourceFile
|
||||
);
|
||||
return {
|
||||
resource,
|
||||
url: resourceFile,
|
||||
filename: filenameWithExtension,
|
||||
filename: filenameWithoutExtension + extension,
|
||||
};
|
||||
} else if (isBlobURL(resourceFile)) {
|
||||
result.erroredResources.push({
|
||||
|
@@ -16,7 +16,20 @@ import {
|
||||
downloadUrlsToBlobs,
|
||||
type ItemResult,
|
||||
} from '../../Utils/BlobDownloader';
|
||||
import { isBlobURL, isURL } from '../../ResourcesList/ResourceUtils';
|
||||
|
||||
const isURL = (filename: string) => {
|
||||
return (
|
||||
filename.startsWith('http://') ||
|
||||
filename.startsWith('https://') ||
|
||||
filename.startsWith('ftp://') ||
|
||||
filename.startsWith('blob:') ||
|
||||
filename.startsWith('data:')
|
||||
);
|
||||
};
|
||||
|
||||
const isBlobURL = (filename: string) => {
|
||||
return filename.startsWith('blob:');
|
||||
};
|
||||
|
||||
export const moveAllCloudProjectResourcesToCloudProject = async ({
|
||||
project,
|
||||
|
@@ -4,20 +4,20 @@ import PromisePool from '@supercharge/promise-pool';
|
||||
import { retryIfFailed } from '../../Utils/RetryIfFailed';
|
||||
import newNameGenerator from '../../Utils/NewNameGenerator';
|
||||
import { type FileMetadata } from '../index';
|
||||
import {
|
||||
extractFilenameWithExtensionFromProductAuthorizedUrl,
|
||||
isProductAuthorizedResourceUrl,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
extractFilenameWithExtensionFromPublicAssetResourceUrl,
|
||||
isPublicAssetResourceUrl,
|
||||
} from '../../Utils/GDevelopServices/Asset';
|
||||
import { isFetchableUrl } from '../../ResourcesList/ResourceUtils';
|
||||
import { extractFilenameAndExtensionFromProductAuthorizedUrl } from '../../Utils/GDevelopServices/Shop';
|
||||
const electron = optionalRequire('electron');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
const fs = optionalRequire('fs-extra');
|
||||
const path = optionalRequire('path');
|
||||
|
||||
const isFetchableUrl = (filename: string) => {
|
||||
return (
|
||||
filename.startsWith('http://') ||
|
||||
filename.startsWith('https://') ||
|
||||
filename.startsWith('ftp://')
|
||||
);
|
||||
};
|
||||
|
||||
type Options = {|
|
||||
project: gdProject,
|
||||
fileMetadata: FileMetadata,
|
||||
@@ -53,45 +53,25 @@ export const moveUrlResourcesToLocalFiles = async ({
|
||||
const resource = resourcesManager.getResource(resourceName);
|
||||
|
||||
const url = resource.getFile();
|
||||
let filenameWithExtension;
|
||||
if (isProductAuthorizedResourceUrl(url)) {
|
||||
// Resource is a private asset.
|
||||
filenameWithExtension = extractFilenameWithExtensionFromProductAuthorizedUrl(
|
||||
url
|
||||
);
|
||||
} else if (isPublicAssetResourceUrl(url)) {
|
||||
// Resource is a public asset.
|
||||
filenameWithExtension = extractFilenameWithExtensionFromPublicAssetResourceUrl(
|
||||
url
|
||||
);
|
||||
} else {
|
||||
// Resource is a generic url.
|
||||
filenameWithExtension = path.basename(url);
|
||||
}
|
||||
const extension = path.extname(filenameWithExtension);
|
||||
const filenameWithoutExtension = path.basename(
|
||||
filenameWithExtension,
|
||||
extension
|
||||
);
|
||||
const {
|
||||
extension,
|
||||
filenameWithoutExtension,
|
||||
} = extractFilenameAndExtensionFromProductAuthorizedUrl(url);
|
||||
const name = newNameGenerator(filenameWithoutExtension, name => {
|
||||
const tentativePath = path.join(baseAssetsPath, name) + extension;
|
||||
return (
|
||||
fs.existsSync(tentativePath) || downloadedFilePaths.has(tentativePath)
|
||||
);
|
||||
});
|
||||
const downloadedFilePath = path.join(baseAssetsPath, name) + extension;
|
||||
downloadedFilePaths.add(downloadedFilePath);
|
||||
const newPath = path.join(baseAssetsPath, name) + extension;
|
||||
downloadedFilePaths.add(newPath);
|
||||
|
||||
try {
|
||||
await retryIfFailed({ times: 2 }, async () => {
|
||||
await fs.ensureDir(baseAssetsPath);
|
||||
await ipcRenderer.invoke(
|
||||
'local-file-download',
|
||||
url,
|
||||
downloadedFilePath
|
||||
);
|
||||
await ipcRenderer.invoke('local-file-download', url, newPath);
|
||||
resource.setFile(
|
||||
path.relative(projectPath, downloadedFilePath).replace(/\\/g, '/')
|
||||
path.relative(projectPath, newPath).replace(/\\/g, '/')
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
|
@@ -9,39 +9,23 @@ jest.mock('../../Utils/OptionalRequire');
|
||||
|
||||
const mockFn = (fn: Function): JestMockFn<any, any> => fn;
|
||||
|
||||
const classicUrl = 'https://www.example.com/file-to-download.png';
|
||||
const productAuthorizedUrl =
|
||||
'https://private-assets.gdevelop.io/a2adcae7-ceba-4c0d-ad0f-411bf83692ea/resources/Misc/stars_levels (3).png?token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnZGV2ZWxvcC1zaG9wLWFwaSIsImF1ZCI6IjNwejJvWEZHSmVTaTVyVjROQ0pkclU4MjVUVDIiLCJleHAiOjE2NjY5NjM5NDY1OTUsInN1YiI6WyJhMmFkY2FlNy1jZWJhLTRjMGQtYWQwZi00MTFiZjgzNjkyZWEiLCJjM2ZmZjUyZS1lMTZjLTQxMTYtYTYzNS03ZjUzOGRmN2Y1YWEiXX0%3D.WY0V%2B2ypgT0PEWPUKVPSaiazKNfl4ib%2Bf89CpgcdxGo';
|
||||
const publicResourceUrl =
|
||||
'https://asset-resources.gdevelop.io/public-resources/16x16 Dungeon Tileset/Armor/0a130324cd2501a97027b518b41231896a81e25034fd3a7baaca9581d079f8b6_Imp_Run_2.png';
|
||||
const localFileUrl = 'some-local-file.png';
|
||||
|
||||
const makeTestProjectWithResourcesToDownload = () => {
|
||||
const { project } = makeTestProject(gd);
|
||||
|
||||
// Add a resource that uses a URL, which will be download
|
||||
// by the LocalResourceMover.
|
||||
// by the LocalResourceMover (whatever the origin).
|
||||
{
|
||||
const newResource = new gd.ImageResource();
|
||||
newResource.setName('MyResourceToDownload');
|
||||
newResource.setFile(classicUrl);
|
||||
newResource.setFile('http://example/file-to-download.png');
|
||||
project.getResourcesManager().addResource(newResource);
|
||||
newResource.delete();
|
||||
}
|
||||
// Resource with an authorized URL
|
||||
{
|
||||
const newResource = new gd.ImageResource();
|
||||
newResource.setName('MyAuthorizedResourceToDownload');
|
||||
newResource.setFile(productAuthorizedUrl);
|
||||
project.getResourcesManager().addResource(newResource);
|
||||
newResource.delete();
|
||||
}
|
||||
|
||||
// Resource with a public asset URL
|
||||
{
|
||||
const newResource = new gd.ImageResource();
|
||||
newResource.setName('MyPublicResourceToDownload');
|
||||
newResource.setFile(publicResourceUrl);
|
||||
newResource.setName('MyResourceToDownload');
|
||||
newResource.setFile('http://example/file-to-download.png?token=123');
|
||||
project.getResourcesManager().addResource(newResource);
|
||||
newResource.delete();
|
||||
}
|
||||
@@ -51,7 +35,7 @@ const makeTestProjectWithResourcesToDownload = () => {
|
||||
{
|
||||
const newResource = new gd.ImageResource();
|
||||
newResource.setName('MyAlreadyLocalResource');
|
||||
newResource.setFile(localFileUrl);
|
||||
newResource.setFile('some-local-file.png');
|
||||
project.getResourcesManager().addResource(newResource);
|
||||
newResource.delete();
|
||||
}
|
||||
@@ -76,9 +60,15 @@ describe('LocalResourceMover', () => {
|
||||
const project = makeTestProjectWithResourcesToDownload();
|
||||
|
||||
// Mock a proper download
|
||||
mockFn(optionalRequire.mockFsExtra.ensureDir).mockResolvedValue({});
|
||||
mockFn(optionalRequire.mockFsExtra.existsSync).mockReturnValue(false);
|
||||
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke).mockResolvedValue();
|
||||
mockFn(optionalRequire.mockFsExtra.ensureDir).mockImplementation(
|
||||
async () => {}
|
||||
);
|
||||
mockFn(optionalRequire.mockFsExtra.existsSync).mockImplementation(
|
||||
() => false
|
||||
);
|
||||
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke).mockImplementation(
|
||||
() => Promise.resolve()
|
||||
);
|
||||
|
||||
const options = makeMoveAllProjectResourcesOptions(project);
|
||||
const fetchedResources = await moveUrlResourcesToLocalFiles(options);
|
||||
@@ -88,71 +78,42 @@ describe('LocalResourceMover', () => {
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
'local-file-download',
|
||||
classicUrl,
|
||||
'http://example/file-to-download.png',
|
||||
path.join('assets', 'file-to-download.png')
|
||||
);
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
'local-file-download',
|
||||
productAuthorizedUrl,
|
||||
path.join('assets', 'stars_levels (3).png')
|
||||
);
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
'local-file-download',
|
||||
publicResourceUrl,
|
||||
path.join('assets', 'Imp_Run_2.png')
|
||||
);
|
||||
expect(fetchedResources.erroredResources).toEqual([]);
|
||||
});
|
||||
|
||||
it('reports errors in case of download failure', async () => {
|
||||
const project = makeTestProjectWithResourcesToDownload();
|
||||
|
||||
mockFn(optionalRequire.mockFsExtra.ensureDir).mockResolvedValue({});
|
||||
mockFn(optionalRequire.mockFsExtra.existsSync).mockReturnValue(false);
|
||||
// Mock failed download twice for first file
|
||||
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke).mockRejectedValue(
|
||||
new Error('Fake download failure')
|
||||
// Mock a failed download
|
||||
mockFn(optionalRequire.mockFsExtra.ensureDir).mockImplementation(
|
||||
async () => {}
|
||||
);
|
||||
mockFn(optionalRequire.mockFsExtra.existsSync).mockImplementation(
|
||||
() => false
|
||||
);
|
||||
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke).mockImplementation(
|
||||
() => Promise.reject(new Error('Fake download failure'))
|
||||
);
|
||||
|
||||
const options = makeMoveAllProjectResourcesOptions(project);
|
||||
const fetchedResources = await moveUrlResourcesToLocalFiles(options);
|
||||
|
||||
// Verify that download was done and reported as failed, even after 2 tries for each file.
|
||||
// Verify that download was done and reported as failed, even after 2 tries.
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledTimes(6);
|
||||
).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
'local-file-download',
|
||||
classicUrl,
|
||||
'http://example/file-to-download.png',
|
||||
path.join('assets', 'file-to-download.png')
|
||||
);
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
'local-file-download',
|
||||
productAuthorizedUrl,
|
||||
path.join('assets', 'stars_levels (3).png')
|
||||
);
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
'local-file-download',
|
||||
publicResourceUrl,
|
||||
path.join('assets', 'Imp_Run_2.png')
|
||||
);
|
||||
expect(fetchedResources.erroredResources).toEqual([
|
||||
{ resourceName: 'MyResourceToDownload', error: expect.any(Error) },
|
||||
{
|
||||
resourceName: 'MyAuthorizedResourceToDownload',
|
||||
error: expect.any(Error),
|
||||
},
|
||||
{ resourceName: 'MyPublicResourceToDownload', error: expect.any(Error) },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -160,10 +121,16 @@ describe('LocalResourceMover', () => {
|
||||
const project = makeTestProjectWithResourcesToDownload();
|
||||
|
||||
// Mock a failed download once, then successful
|
||||
mockFn(optionalRequire.mockFsExtra.ensureDir).mockResolvedValue({});
|
||||
mockFn(optionalRequire.mockFsExtra.existsSync).mockReturnValue(false);
|
||||
mockFn(optionalRequire.mockFsExtra.ensureDir).mockImplementation(
|
||||
async () => {}
|
||||
);
|
||||
mockFn(optionalRequire.mockFsExtra.existsSync).mockImplementation(
|
||||
() => false
|
||||
);
|
||||
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke)
|
||||
.mockRejectedValueOnce(new Error('Fake download failure'))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error('Fake download failure'))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.resolve());
|
||||
|
||||
const options = makeMoveAllProjectResourcesOptions(project);
|
||||
@@ -172,27 +139,22 @@ describe('LocalResourceMover', () => {
|
||||
// Verify that download was done.
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledTimes(4);
|
||||
).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'local-file-download',
|
||||
classicUrl,
|
||||
'http://example/file-to-download.png',
|
||||
path.join('assets', 'file-to-download.png')
|
||||
);
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'local-file-download',
|
||||
productAuthorizedUrl,
|
||||
path.join('assets', 'stars_levels (3).png')
|
||||
);
|
||||
expect(
|
||||
optionalRequire.mockElectron.ipcRenderer.invoke
|
||||
).toHaveBeenCalledWith(
|
||||
'local-file-download',
|
||||
publicResourceUrl,
|
||||
path.join('assets', 'Imp_Run_2.png')
|
||||
'http://example/file-to-download.png',
|
||||
path.join('assets', 'file-to-download.png')
|
||||
);
|
||||
expect(fetchedResources.erroredResources).toEqual([]);
|
||||
});
|
||||
|
@@ -11,8 +11,20 @@ import UrlStorageProvider from '../UrlStorageProvider';
|
||||
import DownloadFileStorageProvider from '../DownloadFileStorageProvider';
|
||||
import { checkIfIsGDevelopCloudBucketUrl } from '../../Utils/CrossOrigin';
|
||||
import { moveAllCloudProjectResourcesToCloudProject } from '../CloudStorageProvider/CloudResourceMover';
|
||||
import { isBlobURL, isURL } from '../../ResourcesList/ResourceUtils';
|
||||
|
||||
const isURL = (filename: string) => {
|
||||
return (
|
||||
filename.startsWith('http://') ||
|
||||
filename.startsWith('https://') ||
|
||||
filename.startsWith('ftp://') ||
|
||||
filename.startsWith('blob:') ||
|
||||
filename.startsWith('data:')
|
||||
);
|
||||
};
|
||||
|
||||
const isBlobURL = (filename: string) => {
|
||||
return filename.startsWith('blob:');
|
||||
};
|
||||
const ensureNoCloudProjectResources = async ({
|
||||
project,
|
||||
}: MoveAllProjectResourcesOptions): Promise<MoveAllProjectResourcesResult> => {
|
||||
|
@@ -1,9 +1,26 @@
|
||||
// @flow
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
import axios from 'axios';
|
||||
import { isFetchableUrl, isURL } from '../../ResourcesList/ResourceUtils';
|
||||
import { type FileMetadata } from '../index';
|
||||
|
||||
const isURL = (filename: string) => {
|
||||
return (
|
||||
filename.startsWith('http://') ||
|
||||
filename.startsWith('https://') ||
|
||||
filename.startsWith('ftp://') ||
|
||||
filename.startsWith('blob:') ||
|
||||
filename.startsWith('data:')
|
||||
);
|
||||
};
|
||||
|
||||
const isFetchableUrl = (url: string) => {
|
||||
return (
|
||||
url.startsWith('http://') ||
|
||||
url.startsWith('https://') ||
|
||||
url.startsWith('ftp://')
|
||||
);
|
||||
};
|
||||
|
||||
type Options = {
|
||||
project: gdProject,
|
||||
fileMetadata: FileMetadata,
|
||||
|
@@ -152,25 +152,3 @@ export const renameResourcesInProject = (
|
||||
project.exposeResources(resourcesRenamer);
|
||||
resourcesRenamer.delete();
|
||||
};
|
||||
|
||||
export const isFetchableUrl = (url: string) => {
|
||||
return (
|
||||
url.startsWith('http://') ||
|
||||
url.startsWith('https://') ||
|
||||
url.startsWith('ftp://')
|
||||
);
|
||||
};
|
||||
|
||||
export const isURL = (filename: string) => {
|
||||
return (
|
||||
filename.startsWith('http://') ||
|
||||
filename.startsWith('https://') ||
|
||||
filename.startsWith('ftp://') ||
|
||||
filename.startsWith('blob:') ||
|
||||
filename.startsWith('data:')
|
||||
);
|
||||
};
|
||||
|
||||
export const isBlobURL = (filename: string) => {
|
||||
return filename.startsWith('blob:');
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import { renameResourcesInProject } from './ResourceUtils';
|
||||
import { makeTestProject } from '../fixtures/TestProject';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const addNewAnimationWithImageToSpriteObject = (
|
||||
|
@@ -74,7 +74,7 @@ type Props = {|
|
||||
* If `onApply` is also specified, this must be interpreted as a "cancelling"
|
||||
* of changes.
|
||||
*/
|
||||
onRequestClose?: () => void | Promise<void>,
|
||||
onRequestClose?: () => void,
|
||||
|
||||
/**
|
||||
* If specified, will be called when the dialog is dismissed in a way where changes
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user