Compare commits

...

72 Commits

Author SHA1 Message Date
Clément Pasteau
5cd1581be1 Playing around with hitboxes 2021-12-02 19:51:11 +01:00
Clément Pasteau
cfb1d6888e Fix sounds sometimes not playing after the first time being played (#3325) 2021-12-02 10:48:10 +00:00
Florian Rival
816dc8cc74 Fix tweens automatically deleting the object sometimes affecting newly created objects (#3321) 2021-12-02 09:40:14 +00:00
Arthur Pacaud
106549e5fa Allow usage of custom ICE servers in the P2P extension (#3301) 2021-12-01 20:08:25 +00:00
Clément Pasteau
f8ca06d530 Fix web debugger icon not updating properly
Do not show in changelog
2021-12-01 19:32:18 +01:00
AlexandreS
34cbcdbc3a Add carousel component for new start page (don't show in changelog) 2021-12-01 15:11:56 +01:00
Clément Pasteau
3b208502ae Improve DismissableTutorialMessage story
Do not show in changelog
2021-11-30 15:50:28 +01:00
Clément Pasteau
e3654fca99 improve tutorials to be fetched from backend services
Do not show in changelog
2021-11-30 14:43:57 +01:00
Florian Rival
2a386cdcf1 Fix the layer of the created object not shown for the "Create object" action in the events sheet 2021-11-29 23:43:50 +00:00
Leo_Red
b134896687 Fix wrong mention of extensions instead of examples in a text (#3306) 2021-11-29 13:16:34 +00:00
Leo_Red
705dff43bc Move scene variables into their own category in actions/conditions/expressions (#3300) 2021-11-27 16:33:31 +00:00
Leo_Red
d9eaf71ed1 Fix tabs in preference dialog disappearing when scrolling in the dialog (#3299) 2021-11-27 16:06:20 +00:00
D8H
008b4291ab Fix the "separate" action when there are several obstacles (#3236)
* Previously an object colliding with multiple objects would have been "pushed" too far from the two colliding objects. Now, it gets properly separated, in a much more natural way, avoiding some shaking/flickering on corners or when involving multiple obstacles.
2021-11-25 17:45:38 +00:00
Clément Pasteau
3596896b16 Bump IDE version 2021-11-24 17:12:25 +01:00
Clément Pasteau
db05e98bc8 Improve object lock behavior in the editor
* The objects are now selectable, to simplify unlocking them
2021-11-24 17:12:01 +01:00
Florian Rival
98c1a93da5 Update a wording the instance properties editor
Don't show in changelog
2021-11-24 15:26:44 +00:00
Florian Rival
c39d3ee35c Fix a React warning
Don't show in changelog
2021-11-24 15:13:05 +00:00
Florian Rival
c68a25573d Hide direction related actions/conditions/expressions
The builtin "8 directions" is not officially supported anymore and we should re-enable it later with an improved interface/more flexible choices.

Don't shoe in changelog
2021-11-24 15:05:18 +00:00
github-actions[bot]
2b72b6b3e7 Update translations [skip ci] (#3285)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-11-24 14:24:53 +00:00
Clément Pasteau
db60151150 Fix Tiled Sprite being incorrectly displayed (pixelated) when the X/Y offset was too large (#3287) 2021-11-24 14:05:04 +00:00
Leo_Red
0971a4b464 Move the object variables editor into the objects editor (easier to find and faster to access) (#3263)
* Also rework the other variables editor dialogs to have the toolbar buttons always at the bottom of the window.
2021-11-24 13:21:38 +00:00
Clément Pasteau
93a57b1a31 Fix the display of the decreased build limits after exporting a game (#3284) 2021-11-24 09:40:27 +00:00
github-actions[bot]
d0f7e2517d Update translations [skip ci] (#3262)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-11-23 21:48:25 +00:00
AlexandreS
9523c98cad Add multiple achievements ("badges") that can be earned while using GDevelop (#3256)
* These achievements are visible in your user profile.
* They are all for now fairly simple to collect, but we'll also later add some a bit more complex to win, to reward contributors, people invested in the community and people building great games!
2021-11-23 21:29:27 +00:00
Florian Rival
ea38a2ff0f Add conditions and expressions to compare the coordinates of the bounding box of an object (#3275) 2021-11-23 14:56:49 +00:00
Clément Pasteau
3065ba53b1 Fix arrow keys wrongly triggering page scroll when playing a game embedded in a web page (#3280) 2021-11-23 10:57:09 +00:00
Clément Pasteau
dc19f030fc Improve scene editor scrolling speed to be faster/slower according to the zoom level (#3279) 2021-11-23 10:33:21 +00:00
Florian Rival
9fb36a375f Add minor UI changes
* Don't open asset categories in the asset store
* Remove a button redirecting to the asset store as there is already a tab for it
* Make the new object dialog always take the full height of the window
2021-11-21 18:03:09 +00:00
D8H
a366934fdb Avoid to fetch again a JSON in the game engine when it's already being fetched (#3261) 2021-11-21 12:30:59 +00:00
Florian Rival
9626ea6dcf Indicate on link events that they can't be used in an extension/custom behavior 2021-11-20 17:18:21 +00:00
Oxey405
08388893bf Don't list the current scene/external event name in a Link event when choosing what to include (#3228)
* This prevents an infinite loop/crash if launching a preview after selecting the current scene/external events in a link of the same scene/external events.
2021-11-20 15:20:45 +00:00
Arthur Pacaud
2f933f2cad Refactor the implementation of the Tween behavior (#3218)
Only show in developer changelog
2021-11-20 12:37:32 +00:00
Clément Pasteau
2517b47401 Feature/allow user receive game stats (#3259)
Add a checkbox to user profile to receive weekly game stats via email
2021-11-19 16:03:23 +01:00
D8H
86cad60194 Fix platforms sometimes not properly detected when rotated and at the edge of the scene (#3260) 2021-11-19 09:32:24 +00:00
github-actions[bot]
b1658d4619 Update translations [skip ci] (#3253)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-11-16 21:24:17 +01:00
Florian Rival
c72026e8cd Show the usernames of the contributors on the wiki pages for extensions 2021-11-16 16:07:05 +00:00
Florian Rival
4936b4b104 Fix compilation on MinGW
Don't show in changelog
2021-11-16 14:50:05 +00:00
Florian Rival
5623d12eac Fix TimeScale expression not shown in expressions autocompletion
* Also fix inconsistent naming of time scale related actions/conditions and expressions.

Fix #3248
2021-11-16 11:18:29 +00:00
Clément Pasteau
8757cfe8b2 Fix autocomplete not displaying options if only translatable values (#3254)
Do not show in changelog
2021-11-16 10:51:51 +00:00
MyNameIsRinax
968402e99f Fix a typo for rotate toward angle and position in the event sheet sentence (#3252) 2021-11-16 08:21:43 +01:00
github-actions[bot]
6f59a0921d Update translations [skip ci] (#3239)
Don't show in changelog
Co-authored-by: Bouh <Bouh@users.noreply.github.com>
2021-11-15 17:26:12 +01:00
Leo_Red
167307f1c4 Fix a typo for rotate action in the event sheet sentence (#3249) 2021-11-15 17:17:04 +01:00
Florian Rival
36fb4ec9b2 Bump newIDE version 2021-11-12 11:47:21 +00:00
Florian Rival
124e1f3683 Improve changelog extraction script
Don't show in changelog
2021-11-12 11:46:57 +00:00
github-actions[bot]
9c350729a8 Update translations [skip ci] (#3235) 2021-11-12 11:43:27 +00:00
Florian Rival
9186daa782 Update Platformer starter to allow the character to walk on rotated platforms
Don't show in changelog
2021-11-11 17:18:14 +00:00
Florian Rival
c6161c4752 Add an option in the Sprite editor to allow animations to play even when the object is hidden/outside the screen 2021-11-11 17:08:40 +00:00
D8H
5d3f207216 Add major improvements to the platformer engine to better handle slopes and moving platforms (#3009)
* The characters on platform are no long stopping when going from one rotated platform to another.
* Platforms going up and down are now properly handled by characters - they won't fall or vibrate like before. You can now freely use platforms doing any movement and characters will stay on them and can move freely on them.
* Characters Y position will now stay stable when moving on a flat platform and between jumps.
* Note that if you use the collision condition to check if an object is touching a platform, you should instead use the condition "Is object on given floor": this will always work consistently.
2021-11-11 16:11:02 +00:00
Florian Rival
cf462f6c6e Fix volume sounds and musics not clamped at exactly 0 and 1 (#3234)
Don't show in changelog
2021-11-10 11:36:20 +00:00
github-actions[bot]
bc979031e3 Update translations (#3202) 2021-11-09 16:08:14 +00:00
Clément Pasteau
406bae5e12 Fix a glitch where a sound being played at a low volume can actually be heard at a high volume for a split second 2021-11-09 16:00:56 +00:00
Florian Rival
5f5f50e039 Fix wording
Don't show in changelog
2021-11-09 14:43:53 +00:00
Florian Rival
394fb4c587 Fix warning
Don't show in changelog
2021-11-08 15:24:07 +00:00
Florian Rival
599d48afca Refactor scan/removal of unused resources
Don't show in changelog
2021-11-07 19:38:50 +00:00
Florian Rival
bccef185cb Clean up some code
Don't show in changelog
2021-11-07 19:38:50 +00:00
Florian Rival
d0f4370026 Add support for blob urls in the web-app export/preview
Experimental support - just so the behavior of the export/preview are correct.

Don't show in changelog
2021-11-07 19:38:50 +00:00
Florian Rival
77d6f0310c Add support for using arbitrary URLs for images/sounds/resources in the web-app 2021-11-07 19:38:50 +00:00
Florian Rival
c73a5a046f Fix semaphore CI npm cache not working because of npm 7+ upgrading the package-lock.json
Using "npm ci" to allow clean installation without changes to the lock files.

Don't show in changelog
2021-11-04 11:48:04 +00:00
Florian Rival
c37e129a5b Implement support for the Debugger in the web-app
* One or more preview windows can be launched and used with the GDevelop Debugger, like on the desktop app.
  * To run the Debugger, click on the button next to the Play button in the toolbar and choose "Start Preview with Debugger and Performance Profiler"
* This is useful to inspect instances of objects, inspect internal messages or run the performance profiler.
* A right click on the Play button will also allow to launch a new preview, in a new window.
* Also fix the loading screen not shown in the preview on the web-app even when asked to be shown (using the game properties preview button)
2021-11-04 11:48:04 +00:00
Florian Rival
aeecb0e29f Revert Rectangle to Line in the Particle Emitter editor
Don't show in changelog
2021-11-02 10:26:57 +00:00
Tristan Rhodes
a6525e5617 Clarify names of particle types in the Particle Emitter editor (#3217) 2021-11-01 14:35:10 +00:00
Arthur Pacaud
f67aeedaeb Update esbuild (#3220)
Only show in developer changelog
2021-11-01 10:01:52 +00:00
Leo_Red
0c2f023c63 Fix contributors list (#3221) 2021-11-01 09:55:03 +00:00
Leo_Red
d6d4569dbf Update naming of events in the menus to make them easier to understand (#3212) 2021-10-29 14:47:09 +01:00
Florian Rival
965ec330cf Improve preview in the web-app so that it opens in a separate window using the size from the game properties 2021-10-28 22:51:27 +01:00
Arthur Pacaud
c09d29a959 Show the preview window, when corresponding preference is enabled, above the editor but not above all windows on the screen (#3203) 2021-10-28 20:11:03 +01:00
Florian Rival
67612009d1 Add CrazyGames.com in the links where to publish a HTML5 game (#3211) 2021-10-28 18:05:35 +01:00
Florian Rival
2da5194672 Fix crash in the Debugger
Don't show in the changelog
2021-10-28 17:32:42 +01:00
Florian Rival
7f5821a299 Fix user not always logged when opening export after relaunching the app (#3205)
* Also fix signup/login dialog closing before the signup/login is entirely finished
2021-10-27 15:43:39 +01:00
Florian Rival
a3fdeec6a7 Refactor gdjs.Logger to allow disabling specific log groups in the console (#3204)
* This reduces the logs during GDJS tests, as this was cluttering the
terminal.

Only show in developer changelog
2021-10-27 11:39:36 +01:00
Florian Rival
852ad1d92b Do not make the preview window always on top by default in preferences
Don't show in changelog
2021-10-25 22:30:21 +01:00
Florian Rival
8fdba503ab Fix potential crash in the scene editor 2021-10-25 22:21:17 +01:00
281 changed files with 16143 additions and 8349 deletions

View File

@@ -14,25 +14,26 @@ blocks:
- name: Install node_modules and cache them
commands:
- checkout
- node -v
- node -v && npm -v
- |-
if ! cache has_key newIDE-app-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum newIDE/app/package-lock.json); then
cd newIDE/app
npm i
npm ci
cd ../..
cache store newIDE-app-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum newIDE/app/package-lock.json) newIDE/app/node_modules
fi
- |-
if ! cache has_key GDJS-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum GDJS/package-lock.json); then
cd GDJS
npm i
git checkout package-lock.json # Ensure no changes was made by newIDE post-install tasks.
npm ci
cd ..
cache store GDJS-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum GDJS/package-lock.json) GDJS/node_modules
fi
- |-
if ! cache has_key GDJS-tests-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum GDJS/tests/package-lock.json); then
cd GDJS/tests
npm i
npm ci
cd ../..
cache store GDJS-tests-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum GDJS/tests/package-lock.json) GDJS/tests/node_modules
fi

View File

@@ -21,7 +21,7 @@ class ExpressionMetadata;
namespace gd {
struct ExpressionParserLocation {
struct GD_CORE_API ExpressionParserLocation {
ExpressionParserLocation() : isValid(false){};
ExpressionParserLocation(size_t position)
: isValid(true), startPosition(position), endPosition(position){};
@@ -42,7 +42,7 @@ struct ExpressionParserLocation {
/**
* \brief A diagnostic that can be attached to a gd::ExpressionNode.
*/
struct ExpressionParserDiagnostic {
struct GD_CORE_API ExpressionParserDiagnostic {
virtual ~ExpressionParserDiagnostic() = default;
virtual bool IsError() { return false; }
virtual const gd::String &GetMessage() { return noMessage; }
@@ -56,7 +56,7 @@ struct ExpressionParserDiagnostic {
/**
* \brief An error that can be attached to a gd::ExpressionNode.
*/
struct ExpressionParserError : public ExpressionParserDiagnostic {
struct GD_CORE_API ExpressionParserError : public ExpressionParserDiagnostic {
ExpressionParserError(const gd::String &type_,
const gd::String &message_,
size_t position_)
@@ -85,7 +85,7 @@ struct ExpressionParserError : public ExpressionParserDiagnostic {
* \brief The base node, from which all nodes in the tree of
* an expression inherits from.
*/
struct ExpressionNode {
struct GD_CORE_API ExpressionNode {
ExpressionNode(const gd::String &type_) : type(type_){};
virtual ~ExpressionNode(){};
virtual void Visit(ExpressionParser2NodeWorker &worker){};
@@ -104,7 +104,7 @@ struct ExpressionNode {
// gd::ParameterMetadata::IsExpression or "unknown".
};
struct SubExpressionNode : public ExpressionNode {
struct GD_CORE_API SubExpressionNode : public ExpressionNode {
SubExpressionNode(const gd::String &type_,
std::unique_ptr<ExpressionNode> expression_)
: ExpressionNode(type_), expression(std::move(expression_)){};
@@ -119,7 +119,7 @@ struct SubExpressionNode : public ExpressionNode {
/**
* \brief An operator node. For example: "lhs + rhs".
*/
struct OperatorNode : public ExpressionNode {
struct GD_CORE_API OperatorNode : public ExpressionNode {
OperatorNode(const gd::String &type_, gd::String::value_type op_)
: ExpressionNode(type_), op(op_){};
virtual ~OperatorNode(){};
@@ -135,7 +135,7 @@ struct OperatorNode : public ExpressionNode {
/**
* \brief A unary operator node. For example: "-2".
*/
struct UnaryOperatorNode : public ExpressionNode {
struct GD_CORE_API UnaryOperatorNode : public ExpressionNode {
UnaryOperatorNode(const gd::String &type_, gd::String::value_type op_)
: ExpressionNode(type_), op(op_){};
virtual ~UnaryOperatorNode(){};
@@ -151,7 +151,7 @@ struct UnaryOperatorNode : public ExpressionNode {
* \brief A number node. For example: "123".
* Its `type` is always "number".
*/
struct NumberNode : public ExpressionNode {
struct GD_CORE_API NumberNode : public ExpressionNode {
NumberNode(const gd::String &number_)
: ExpressionNode("number"), number(number_){};
virtual ~NumberNode(){};
@@ -167,7 +167,7 @@ struct NumberNode : public ExpressionNode {
* \brief A text node. For example: "Hello World".
* Its `type` is always "string".
*/
struct TextNode : public ExpressionNode {
struct GD_CORE_API TextNode : public ExpressionNode {
TextNode(const gd::String &text_) : ExpressionNode("string"), text(text_){};
virtual ~TextNode(){};
virtual void Visit(ExpressionParser2NodeWorker &worker) {
@@ -177,7 +177,7 @@ struct TextNode : public ExpressionNode {
gd::String text;
};
struct VariableAccessorOrVariableBracketAccessorNode : public ExpressionNode {
struct GD_CORE_API VariableAccessorOrVariableBracketAccessorNode : public ExpressionNode {
VariableAccessorOrVariableBracketAccessorNode() : ExpressionNode(""){};
std::unique_ptr<VariableAccessorOrVariableBracketAccessorNode> child;
@@ -191,7 +191,7 @@ struct VariableAccessorOrVariableBracketAccessorNode : public ExpressionNode {
* \see gd::VariableAccessorNode
* \see gd::VariableBracketAccessorNode
*/
struct VariableNode : public ExpressionNode {
struct GD_CORE_API VariableNode : public ExpressionNode {
VariableNode(const gd::String &type_,
const gd::String &name_,
const gd::String &objectName_)
@@ -214,7 +214,7 @@ struct VariableNode : public ExpressionNode {
* \brief A bracket accessor of a variable. Example: MyChild
* in MyVariable.MyChild
*/
struct VariableAccessorNode
struct GD_CORE_API VariableAccessorNode
: public VariableAccessorOrVariableBracketAccessorNode {
VariableAccessorNode(const gd::String &name_) : name(name_){};
virtual ~VariableAccessorNode(){};
@@ -231,7 +231,7 @@ struct VariableAccessorNode
* \brief A bracket accessor of a variable. Example: ["MyChild"]
* (in MyVariable["MyChild"]).
*/
struct VariableBracketAccessorNode
struct GD_CORE_API VariableBracketAccessorNode
: public VariableAccessorOrVariableBracketAccessorNode {
VariableBracketAccessorNode(std::unique_ptr<ExpressionNode> expression_)
: expression(std::move(expression_)){};
@@ -243,7 +243,7 @@ struct VariableBracketAccessorNode
std::unique_ptr<ExpressionNode> expression;
};
struct IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode
struct GD_CORE_API IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode
: public ExpressionNode {
IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode(
const gd::String &type)
@@ -253,7 +253,7 @@ struct IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode
/**
* \brief An identifier node, usually representing an object or a function name.
*/
struct IdentifierNode
struct GD_CORE_API IdentifierNode
: public IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode {
IdentifierNode(const gd::String &identifierName_, const gd::String &type_)
: IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode(type_),
@@ -266,7 +266,7 @@ struct IdentifierNode
gd::String identifierName;
};
struct FunctionCallOrObjectFunctionNameOrEmptyNode
struct GD_CORE_API FunctionCallOrObjectFunctionNameOrEmptyNode
: public IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode {
FunctionCallOrObjectFunctionNameOrEmptyNode(const gd::String &type)
: IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode(type){};
@@ -279,7 +279,7 @@ struct FunctionCallOrObjectFunctionNameOrEmptyNode
* For example: "MyObject.Function" or "MyObject.Physics" or
* "MyObject.Physics::LinearVelocity".
*/
struct ObjectFunctionNameNode
struct GD_CORE_API ObjectFunctionNameNode
: public FunctionCallOrObjectFunctionNameOrEmptyNode {
ObjectFunctionNameNode(const gd::String &type_,
const gd::String &objectName_,
@@ -332,7 +332,7 @@ struct ObjectFunctionNameNode
* For example: "MyExtension::MyFunction(1, 2)", "MyObject.Function()" or
* "MyObject.Physics::LinearVelocity()".
*/
struct FunctionCallNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
struct GD_CORE_API FunctionCallNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
/** \brief Construct a free function call node. */
FunctionCallNode(const gd::String &type_,
std::vector<std::unique_ptr<ExpressionNode>> parameters_,
@@ -400,7 +400,7 @@ struct FunctionCallNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
* \brief An empty node, used when parsing failed/a syntax error was
* encountered and any other node could not make sense.
*/
struct EmptyNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
struct GD_CORE_API EmptyNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
EmptyNode(const gd::String &type_, const gd::String &text_ = "")
: FunctionCallOrObjectFunctionNameOrEmptyNode(type_), text(text_){};
virtual ~EmptyNode(){};

View File

@@ -106,7 +106,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddExpressionAndConditionAndAction("number", "CenterX",
_("Center X position"),
_("the X position of the center"),
_("the X position of the center of rotation"),
_("the X position of the center"),
_("Position/Center"),
"res/actions/position24.png")
@@ -115,13 +115,67 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddExpressionAndConditionAndAction("number", "CenterY",
_("Center Y position"),
_("the Y position of the center"),
_("the Y position of the center of rotation"),
_("the Y position of the center"),
_("Position/Center"),
"res/actions/position24.png")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxLeft",
_("Bounding box left position"),
_("the bounding box (the area encapsulating the object) left position"),
_("the bounding box left position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-left.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxTop",
_("Bounding box top position"),
_("the bounding box (the area encapsulating the object) top position"),
_("the bounding box top position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-top.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxRight",
_("Bounding box right position"),
_("the bounding box (the area encapsulating the object) right position"),
_("the bounding box right position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-right.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxBottom",
_("Bounding box bottom position"),
_("the bounding box (the area encapsulating the object) bottom position"),
_("the bounding box bottom position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-bottom.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxCenterX",
_("Bounding box center X position"),
_("the bounding box (the area encapsulating the object) center X position"),
_("the bounding box center X position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-center.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxCenterY",
_("Bounding box center Y position"),
_("the bounding box (the area encapsulating the object) center Y position"),
_("the bounding box center Y position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-center.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddAction("MettreAutourPos",
_("Put around a position"),
_("Position the center of the given object around a position, "
@@ -155,7 +209,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Rotate"),
_("Rotate an object, clockwise if the speed is positive, "
"counterclockwise otherwise."),
_("Rotate _PARAM0_ at speed _PARAM1_deg/second"),
_("Rotate _PARAM0_ at speed _PARAM1_ deg/second"),
_("Angle"),
"res/actions/direction24.png",
"res/actions/direction.png")
@@ -169,7 +223,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"RotateTowardAngle",
_("Rotate toward angle"),
_("Rotate an object towards an angle with the specified speed."),
_("Rotate _PARAM0_ towards _PARAM1_ at speed _PARAM2_deg/second"),
_("Rotate _PARAM0_ towards _PARAM1_ at speed _PARAM2_ deg/second"),
_("Angle"),
"res/actions/direction24.png",
"res/actions/direction.png")
@@ -185,7 +239,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Rotate toward position"),
_("Rotate an object towards a position, with the specified speed."),
_("Rotate _PARAM0_ towards _PARAM1_;_PARAM2_ at speed "
"_PARAM3_deg/second"),
"_PARAM3_ deg/second"),
_("Angle"),
"res/actions/direction24.png",
"res/actions/direction.png")
@@ -1143,7 +1197,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.AddAction("Create",
_("Create an object"),
_("Create an object at specified position"),
_("Create object _PARAM1_ at position _PARAM2_;_PARAM3_"),
_("Create object _PARAM1_ at position _PARAM2_;_PARAM3_ (layer: _PARAM4_)"),
_("Objects"),
"res/actions/create24.png",
"res/actions/create.png")
@@ -1161,7 +1215,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Among the objects of the specified group, this action will "
"create the object with the specified name."),
_("Among objects _PARAM1_, create object named _PARAM2_ at "
"position _PARAM3_;_PARAM4_"),
"position _PARAM3_;_PARAM4_ (layer: _PARAM5_)"),
_("Objects"),
"res/actions/create24.png",
"res/actions/create.png")

View File

@@ -419,7 +419,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
_("Layer time scale"),
_("Compare the time scale applied to the objects of the layer."),
_("the time scale of layer _PARAM1_"),
_("Layers and cameras/Time"),
_("Layers and cameras"),
"res/conditions/time24.png",
"res/conditions/time.png")
.AddCodeOnlyParameter("currentScene", "")
@@ -433,8 +433,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
"ChangeLayerTimeScale",
_("Change layer time scale"),
_("Change the time scale applied to the objects of the layer."),
_("Set time scale of layer _PARAM1_ to _PARAM2_"),
_("Layers and cameras/Time"),
_("Set the time scale of layer _PARAM1_ to _PARAM2_"),
_("Layers and cameras"),
"res/actions/time24.png",
"res/actions/time.png")
.AddCodeOnlyParameter("currentScene", "")
@@ -551,8 +551,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
extension
.AddExpression("LayerTimeScale",
_("Time scale"),
_("Time scale"),
_("Layer time scale"),
_("Returns the time scale of the specified layer."),
_("Layers and cameras"),
"res/actions/time.png")
.AddCodeOnlyParameter("currentScene", "")

View File

@@ -83,29 +83,29 @@ BuiltinExtensionsImplementer::ImplementsCommonInstructionsExtension(
std::make_shared<gd::StandardEvent>());
extension.AddEvent("Link",
_("Link"),
_("Link to some external events"),
_("Link external events"),
_("Link to external events."),
"",
"res/lienaddicon.png",
std::make_shared<gd::LinkEvent>());
extension.AddEvent("Comment",
_("Comment"),
_("Event displaying a text in the events editor"),
_("Event displaying a text in the events editor."),
"",
"res/comment.png",
std::make_shared<gd::CommentEvent>());
extension.AddEvent("While",
_("While"),
_("The event is repeated while the conditions are true"),
_("Repeat the event while the conditions are true."),
"",
"res/while.png",
std::make_shared<gd::WhileEvent>());
extension.AddEvent("Repeat",
_("Repeat"),
_("Event repeated a number of times"),
_("Repeat the event for a specified number of times."),
"",
"res/repeat.png",
std::make_shared<gd::RepeatEvent>());
@@ -126,8 +126,8 @@ BuiltinExtensionsImplementer::ImplementsCommonInstructionsExtension(
std::make_shared<gd::ForEachChildVariableEvent>());
extension.AddEvent("Group",
_("Group"),
_("Group containing events"),
_("Event group"),
_("Group containing events."),
"",
"res/foreach.png",
std::make_shared<gd::GroupEvent>());

View File

@@ -78,7 +78,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
_("Direction"),
"res/actions/direction24.png",
"res/actions/direction.png")
.SetHidden() // Hide as 8 direction is not supported officially in the interface.
.AddParameter("object", _("Object"), "Sprite")
.UseStandardOperatorParameters("number")
.MarkAsAdvanced();
@@ -242,7 +242,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
_("Direction"),
"res/conditions/direction24.png",
"res/conditions/direction.png")
.SetHidden() // Hide as 8 direction is not supported officially in the interface.
.AddParameter("object", _("Object"), "Sprite")
.UseStandardRelationalOperatorParameters("number");
@@ -464,6 +464,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
_("Direction of the object"),
_("Direction"),
"res/actions/direction.png")
.SetHidden() // Hide as 8 direction is not supported officially in the interface.
.AddParameter("object", _("Object"), "Sprite");
obj.AddExpression("Anim",

View File

@@ -116,6 +116,18 @@ class GD_CORE_API SpriteObject : public gd::Object {
* animation of the object.
*/
const std::vector<Animation>& GetAllAnimations() const { return animations; }
/**
* \brief Set if the object animation should be played even if the object is hidden
* or far from the camera.
*/
void SetUpdateIfNotVisible(bool updateIfNotVisible_) { updateIfNotVisible = updateIfNotVisible_; }
/**
* \brief Check if the object animation should be played even if the object is hidden
* or far from the camera (false by default).
*/
bool GetUpdateIfNotVisible() const { return updateIfNotVisible; }
///@}
private:

View File

@@ -38,8 +38,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
extension
.AddCondition("TimeScale",
_("Time scale"),
_("Test the time scale."),
_("the time scale"),
_("Compare the time scale of the scene."),
_("the time scale of the scene"),
_("Timers and time"),
"res/conditions/time24.png",
"res/conditions/time.png")
@@ -111,8 +111,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
extension
.AddAction("ChangeTimeScale",
_("Change time scale"),
_("Change the time scale of the game."),
_("Set time scale to _PARAM1_"),
_("Change the time scale of the scene."),
_("Set the time scale of the scene to _PARAM1_"),
_("Timers and time"),
"res/actions/time24.png",
"res/actions/time.png")
@@ -175,20 +175,11 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
extension
.AddExpression("TimeScale",
_("Time scale"),
_("Time scale"),
_("Returns the time scale of the scene."),
_("Time"),
"res/actions/time.png")
.AddCodeOnlyParameter("currentScene", "");
extension
.AddExpression("TimeScale",
_("Time scale"),
_("Time scale"),
_("Time"),
"res/actions/time.png")
.SetHidden()
.AddCodeOnlyParameter("currentScene", "");
extension
.AddExpression("Time",
_("Current time"),

View File

@@ -28,7 +28,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Value of a scene variable"),
_("Compare the value of a scene variable."),
_("the scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -39,7 +39,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Text of a scene variable"),
_("Compare the text of a scene variable."),
_("the text of scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -51,7 +51,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Boolean value of a scene variable"),
_("Compare the boolean value of a scene variable."),
_("The boolean value of scene variable _PARAM0_ is _PARAM1_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -64,7 +64,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Child existence"),
_("Check if the specified child of the scene variable exists."),
_("Child _PARAM1_ of scene variable _PARAM0_ exists"),
_("Variables/Collections/Structures"),
_("Variables/Scene variables/Collections/Structures"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -89,7 +89,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
"Test if a scene variable is defined",
"Test if the scene variable exists.",
"Scene variable _PARAM0_ is defined",
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddCodeOnlyParameter("currentScene", "")
@@ -151,7 +151,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Value of a scene variable"),
_("Change the value of a scene variable."),
_("the scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -162,7 +162,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("String of a scene variable"),
_("Modify the text of a scene variable."),
_("the text of scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -174,7 +174,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Boolean value of a scene variable"),
_("Modify the boolean value of a scene variable."),
_("Set the boolean value of scene variable _PARAM0_ to _PARAM1_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -187,7 +187,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("If it was true, it will become false, and if it was "
"false it will become true."),
_("Toggle the boolean value of scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"));
@@ -245,7 +245,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Remove a child"),
_("Remove a child from a scene variable."),
_("Remove child _PARAM1_ from scene variable _PARAM0_"),
_("Variables/Collections/Structures"),
_("Variables/Scene variables/Collections/Structures"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -269,7 +269,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Clear scene variable"),
_("Remove all the children from the scene variable."),
_("Clear children from scene variable _PARAM0_"),
_("Variables/Collections"),
_("Variables/Scene variables/Collections"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -291,7 +291,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Append variable to a scene array"),
_("Appends a variable at the end of a scene array variable."),
_("Append variable _PARAM1_ to array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Array variable"))
@@ -304,7 +304,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Append a string to a scene array"),
_("Appends a string at the end of a scene array variable."),
_("Append string _PARAM1_ to array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Array variable"))
@@ -316,7 +316,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Append a number to a scene array"),
_("Appends a number at the end of a scene array variable."),
_("Append number _PARAM1_ to array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Array variable"))
@@ -328,7 +328,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Append a boolean to a scene array"),
_("Appends a boolean at the end of a scene array variable."),
_("Append boolean _PARAM1_ to array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Array variable"))
@@ -341,7 +341,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Remove variable from a scene array (by index)"),
_("Removes a variable at the specified index of a scene array variable."),
_("Remove variable at index _PARAM1_ from scene array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -414,7 +414,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddExpression("GlobalVariableChildCount",
_("Number of children of a global variable"),
_("Number of children of a global variable"),
_("Variables"),
_("Variables/Global variables"),
"res/actions/var.png")
.AddParameter("globalvar", _("Variable"));
@@ -422,7 +422,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddExpression("VariableChildCount",
_("Number of children of a scene variable"),
_("Number of children of a scene variable"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"));
@@ -430,7 +430,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddExpression("Variable",
_("Value of a scene variable"),
_("Value of a scene variable"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"));
@@ -438,7 +438,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddStrExpression("VariableString",
_("Text of a scene variable"),
_("Text of a scene variable"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"));
@@ -446,7 +446,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddExpression("GlobalVariable",
_("Value of a global variable"),
_("Value of a global variable"),
_("Variables"),
_("Variables/Global variables"),
"res/actions/var.png")
.AddParameter("globalvar", _("Name of the global variable"));
@@ -454,7 +454,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddStrExpression("GlobalVariableString",
_("Text of a global variable"),
_("Text of a global variable"),
_("Variables"),
_("Variables/Global variables"),
"res/actions/var.png")
.AddParameter("globalvar", _("Variable"));
}

View File

@@ -32,7 +32,7 @@ namespace gd {
* The IDE is responsible for actually *searching* and showing the completions -
* this is only describing what must be listed.
*/
struct ExpressionCompletionDescription {
struct GD_CORE_API ExpressionCompletionDescription {
public:
/**
* The different kind of completions that can be described.
@@ -274,7 +274,7 @@ struct ExpressionCompletionDescription {
/**
* \brief Turn an ExpressionCompletionDescription to a string.
*/
std::ostream& operator<<(std::ostream& os,
GD_CORE_API std::ostream& operator<<(std::ostream& os,
ExpressionCompletionDescription const& value);
/**

View File

@@ -95,10 +95,11 @@ namespace gdjs {
}
updateFontFamily(): void {
this._pixiObject.textStyles.default.fontFamily = this._object._runtimeScene
.getGame()
.getFontManager()
.getFontFamily(this._object._fontFamily);
this._pixiObject.textStyles.default.fontFamily =
this._object._runtimeScene
.getGame()
.getFontManager()
.getFontFamily(this._object._fontFamily);
this._pixiObject.dirty = true;
}
@@ -109,9 +110,14 @@ namespace gdjs {
}
updatePosition(): void {
this._pixiObject.position.x = this._object.x + this._pixiObject.width / 2;
this._pixiObject.position.y =
this._object.y + this._pixiObject.height / 2;
const angle = gdjs.toRad(this._object.angle);
const height = this.getHeight() / 2;
const width = this.getWidth() / 2;
const xDelta = width * Math.cos(angle) - height * Math.sin(angle);
const yDelta = width * Math.sin(angle) + height * Math.cos(angle);
this._pixiObject.position.x = this._object.x + xDelta;
this._pixiObject.position.y = this._object.y + yDelta;
}
updateAngle(): void {

View File

@@ -149,8 +149,14 @@ namespace gdjs {
}
updatePosition(): void {
this._pixiObject.position.x = this._object.x + this.getWidth() / 2;
this._pixiObject.position.y = this._object.y + this.getHeight() / 2;
const angle = gdjs.toRad(this._object.angle);
const height = this.getHeight() / 2;
const width = this.getWidth() / 2;
const xDelta = width * Math.cos(angle) - height * Math.sin(angle);
const yDelta = width * Math.sin(angle) + height * Math.cos(angle);
this._pixiObject.position.x = this._object.x + xDelta;
this._pixiObject.position.y = this._object.y + yDelta;
}
updateAngle(): void {
@@ -169,5 +175,6 @@ namespace gdjs {
return this._pixiObject.textHeight * this.getScale();
}
}
export const BitmapTextRuntimeObjectRenderer = BitmapTextRuntimeObjectPixiRenderer;
export const BitmapTextRuntimeObjectRenderer =
BitmapTextRuntimeObjectPixiRenderer;
}

View File

@@ -24,7 +24,7 @@ namespace gdjs {
type: 'info' | 'warning' | 'error',
group: string
) {
gdjs.log(group, message, type, false);
gdjs.Logger.getLoggerOutput().log(group, message, type, false);
};
/**

View File

@@ -1,5 +1,5 @@
namespace gdjs {
const logger = new gdjs.Logger('Firebase');
const logger = new gdjs.Logger('Firebase (setup)');
export namespace evtTools {
/**
* Firebase Event Tools

View File

@@ -69,3 +69,4 @@ var t=require("./bufferbuilder").BufferBuilder,e=require("./bufferbuilder").bina
},{"eventemitter3":"JJlS","./util":"BHXf","./logger":"WOs9","./socket":"wJlv","./mediaconnection":"dbHP","./dataconnection":"GBTQ","./enums":"ZRYf","./api":"in7L"}],"iTK6":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("./util"),r=require("./peer");exports.peerjs={Peer:r.Peer,util:e.util},exports.default=r.Peer,window.peerjs=exports.peerjs,window.Peer=r.Peer;
},{"./util":"BHXf","./peer":"Hxpd"}]},{},["iTK6"], null)
//# sourceMappingURL=A_peer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -375,15 +375,14 @@ namespace gdjs {
key: string,
ssl: boolean
) => {
peerConfig = {
debug: 1,
Object.assign(peerConfig, {
host,
port,
path,
secure: ssl,
// All servers have "peerjs" as default key
key: key.length === 0 ? 'peerjs' : key,
};
});
loadPeerJS();
};
@@ -394,6 +393,26 @@ namespace gdjs {
*/
export const useDefaultBrokerServer = loadPeerJS;
/**
* Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.
* @param urls The URL of the STUN/TURN server.
* @param username An optional username to send to the server.
* @param credential An optional password to send to the server.
*/
export const useCustomICECandidate = (
urls: string,
username?: string,
credential?: string
) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceServers = peerConfig.config.iceServers || [];
peerConfig.config.iceServers.push({
urls,
username,
credential,
});
};
/**
* Overrides the default peer ID. Must be called before connecting to a broker.
* Overriding the ID may have unwanted consequences. Do not use this feature

View File

@@ -154,6 +154,30 @@ module.exports = {
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setFunctionName('gdjs.evtTools.p2p.useCustomBrokerServer');
extension
.addAction(
'UseOwnICEServer',
_('Use a custom ICE server'),
_(
'Disables the default ICE (STUN or TURN) servers list and use one of your own. ' +
'Note that it is recommended to add at least 1 self-hosted STUN and TURN server ' +
'for games that are not over LAN but over the internet. ' +
'This action can be used multiple times to add multiple servers. ' +
'This action needs to be called BEFORE connecting to the broker server.'
),
_('Use ICE server _PARAM0_ (username: _PARAM1_, password: _PARAM2_)'),
_('P2P (experimental)'),
'JsPlatform/Extensions/p2picon.svg',
'JsPlatform/Extensions/p2picon.svg'
)
.addParameter('string', _('URL to the ICE server'), '', false)
.addParameter('string', _('(Optional) Username'), '', true)
.addParameter('string', _('(Optional) Password'), '', true)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setFunctionName('gdjs.evtTools.p2p.useCustomICECandidate');
extension
.addAction(
'UseDefaultBroker',

View File

@@ -20,7 +20,6 @@ This project is released under the MIT License.
void PlatformerObjectBehavior::InitializeContent(
gd::SerializerElement& behaviorContent) {
behaviorContent.SetAttribute("roundCoordinates", true);
behaviorContent.SetAttribute("gravity", 1000);
behaviorContent.SetAttribute("maxFallingSpeed", 700);
behaviorContent.SetAttribute("ladderClimbingSpeed", 150);
@@ -80,12 +79,6 @@ PlatformerObjectBehavior::GetProperties(
gd::String::From(behaviorContent.GetDoubleAttribute("yGrabOffset")));
properties[_("Grab tolerance on X axis")].SetValue(gd::String::From(
behaviorContent.GetDoubleAttribute("xGrabTolerance", 10)));
properties[_("Round coordinates")]
.SetValue(behaviorContent.GetBoolAttribute("roundCoordinates", false)
? "true"
: "false")
.SetType("Boolean");
return properties;
}
@@ -95,8 +88,6 @@ bool PlatformerObjectBehavior::UpdateProperty(
const gd::String& value) {
if (name == _("Default controls"))
behaviorContent.SetAttribute("ignoreDefaultControls", (value == "0"));
else if (name == _("Round coordinates"))
behaviorContent.SetAttribute("roundCoordinates", (value == "1"));
else if (name == _("Can grab platform ledges"))
behaviorContent.SetAttribute("canGrabPlatforms", (value == "1"));
else if (name == _("Grab offset on Y axis"))

View File

@@ -0,0 +1,57 @@
# Platformer Extension technical documentation
## Floor following
### Horizontal search
When the character walks on a platform, he must follow its slope.
The `slopeMaxAngle` property is used to calculate how much the character can move vertically to follow it.
If the platform is too high, the platform is considered to be an obstacle and the character will stop before it.
When there is no obstacle detected by the horizontal search, the movement is done in 1 step and the vertical search is done at the new `x` position.
[![RequestedDeltaX](./diagrams/SlopeFollowingRequestedDeltaX.png)](./diagrams/SlopeFollowingRequestedDeltaX.svgz)
Otherwise, when there is a junction, 2 vertical searches are done:
- one before a potential obstacle (in pink)
- one at the end of the movement
[![RequestedDeltaX](./diagrams/SlopeFollowingClimbFactor.png)](./diagrams/SlopeFollowingClimbFactor.svgz)
This allows to calculate the right slope angle. Indeed, in one step, the angle could appear lower (the dotted line).
Which means that the character could climb it during 1 frame and then stop.
[![RequestedDeltaX](./diagrams/SlopeFollowingClimbFactorMean.png)](./diagrams/SlopeFollowingClimbFactorMean.svgz)
For further details on the implementation, please take a look at the comments in:
- the function `gdjs.PlatformerObjectRuntimeBehavior._moveX`
- the function `gdjs.PlatformerObjectRuntimeBehavior.OnFloor.beforeMovingY`
### Vertical search
The aim of the vertical search is to find the highest platform where the character can land.
There are 2 constraints:
- `allowedMinDeltaY` how much the character can go upward
- `allowedMaxDeltaY` how much the character can go downward
During the search, these 2 constraints can tighten around the character.
If they become incompatible, it means that the character can't go through the hole,
it will go back to its original position and lose its speed.
There are also more obvious obstacles that cover the character in the middle and end the search directly.
[![RequestedDeltaX](./diagrams/SlopeFollowingResult.png)](./diagrams/SlopeFollowingResult.svgz)
Obstacles can eventually encompass the character. So platforms edges don't have any collision with character.
To detect such cases, 2 flags are used:
- `foundOverHead` when an edge is over `headMaxY`
- `foundUnderHead` when an edge is under `floorMinY`
[![RequestedDeltaX](./diagrams/SlopeFollowingContext.png)](./diagrams/SlopeFollowingContext.svgz)
For further details on the implementation, please take a look at the comments in:
- the function `gdjs.PlatformerObjectRuntimeBehavior._findHighestFloorAndMoveOnTop`
- the class `gdjs.PlatformerObjectRuntimeBehavior.FollowConstraintContext`

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -105,6 +105,7 @@ namespace gdjs {
_oldY: float = 0;
_oldWidth: float = 0;
_oldHeight: float = 0;
_oldAngle: float = 0;
_manager: gdjs.PlatformObjectsManager;
_registeredInManager: boolean = false;
@@ -173,7 +174,8 @@ namespace gdjs {
this._oldX !== this.owner.getX() ||
this._oldY !== this.owner.getY() ||
this._oldWidth !== this.owner.getWidth() ||
this._oldHeight !== this.owner.getHeight()
this._oldHeight !== this.owner.getHeight() ||
this._oldAngle !== this.owner.getAngle()
) {
if (this._registeredInManager) {
this._manager.removePlatform(this);
@@ -183,6 +185,7 @@ namespace gdjs {
this._oldY = this.owner.getY();
this._oldWidth = this.owner.getWidth();
this._oldHeight = this.owner.getHeight();
this._oldAngle = this.owner.getAngle();
}
}

View File

@@ -0,0 +1,604 @@
describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, function () {
const makeTestRuntimeScene = () => {
const runtimeGame = new gdjs.RuntimeGame({
variables: [],
resources: {
resources: [],
},
properties: { windowWidth: 800, windowHeight: 600 },
});
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
runtimeScene.loadFromScene({
layers: [{ name: '', visibility: true, effects: [] }],
variables: [],
behaviorsSharedData: [],
objects: [],
instances: [],
});
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / 60) * 1000;
};
return runtimeScene;
};
const addCharacter = (runtimeScene) => {
const character = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 1500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
},
],
effects: [],
});
character.setCustomWidthAndHeight(100, 100);
runtimeScene.addObject(character);
return character;
};
const addPlatform = (runtimeScene, collisionMask) => {
const platform = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'platform',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 0, y: 0 },
centerPoint: { x: 50, y: 50 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 50, y: 50 },
],
hasCustomCollisionMask: true,
customCollisionMask: collisionMask,
},
],
},
],
},
],
});
runtimeScene.addObject(platform);
platform.setUnscaledWidthAndHeight(100, 100);
platform.setCustomWidthAndHeight(100, 100);
return platform;
};
const checkMoveOn = (
characterBehavior,
platformBehavior,
upwardDeltaY,
downwardDeltaY
) => {
const result = characterBehavior._findHighestFloorAndMoveOnTop(
[platformBehavior],
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(platformBehavior);
};
const checkNoFloor = (
characterBehavior,
platformBehavior,
upwardDeltaY,
downwardDeltaY
) => {
const oldY = characterBehavior.owner.getY();
const result = characterBehavior._findHighestFloorAndMoveOnTop(
[platformBehavior],
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(null);
expect(result.isCollidingAnyPlatform).to.be(false);
expect(characterBehavior.owner.getY()).to.be(oldY);
};
const checkObstacle = (
characterBehavior,
platformBehavior,
upwardDeltaY,
downwardDeltaY
) => {
const oldY = characterBehavior.owner.getY();
const result = characterBehavior._findHighestFloorAndMoveOnTop(
[platformBehavior],
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(null);
expect(result.isCollidingAnyPlatform).to.be(true);
expect(characterBehavior.owner.getY()).to.be(oldY);
};
const noCollision = gdjs.PlatformerObjectRuntimeBehavior._noCollision;
const floorIsTooHigh = gdjs.PlatformerObjectRuntimeBehavior._floorIsTooHigh;
[false, true].forEach((swapVerticesOrder) => {
describe(`(swapVertexOrder: ${swapVerticesOrder})`, function () {
const collisionMasks = {
square: [
[
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
{ x: 0, y: 100 },
],
],
bottomLeftTriangle: [
[
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
],
],
bottomRightTriangle: [
[
{ x: 100, y: 0 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
],
],
topRightTriangle: [
[
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
],
],
topLeftTriangle: [
[
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 0, y: 100 },
],
],
topLeftTriangleWithLowEdge: [
[
{ x: -1, y: 100 },
{ x: -1, y: 0 },
{ x: 100, y: 0 },
{ x: 0, y: 100 },
],
],
bottomTriangle: [
[
{ x: 50, y: 0 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
],
],
topTriangle: [
[
{ x: 50, y: 100 },
{ x: 0, y: 0 },
{ x: 100, y: 0 },
],
],
leftTriangle: [
[
{ x: 100, y: 50 },
{ x: 0, y: 0 },
{ x: 0, y: 100 },
],
],
rightTriangle: [
[
{ x: 0, y: 50 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
],
],
horizontalTunnel: [
[
{ x: 0, y: 0 },
{ x: 0, y: 25 },
{ x: 100, y: 25 },
{ x: 100, y: 0 },
],
[
{ x: 0, y: 75 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
{ x: 100, y: 75 },
],
],
verticalTunnel: [
[
{ x: 25, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 25, y: 100 },
],
[
{ x: 75, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
{ x: 75, y: 100 },
],
],
};
if (swapVerticesOrder) {
for (const key in collisionMasks) {
if (Object.hasOwnProperty.call(collisionMasks, key)) {
collisionMasks[key] = collisionMasks[key].reverse();
}
}
}
[
{
description: '(An edge crossing from the left to the right)',
mask: collisionMasks.square,
position: [200, -100],
},
{
description: '(A vertex inside and edges crossing at the bottom)',
mask: collisionMasks.bottomTriangle,
position: [200, -100],
},
{
description: '(An edge crossing from the left to the bottom)',
mask: collisionMasks.bottomLeftTriangle,
position: [200, -200],
},
{
description:
'(An edge crossing from the left to the bottom with the vertex right on the left border)',
mask: collisionMasks.bottomLeftTriangle,
position: [300, -100],
},
{
description: '(An edge crossing from the right to the bottom)',
mask: collisionMasks.bottomRightTriangle,
position: [200, -200],
},
{
description:
'(An edge crossing from the right to the bottom with the vertex right on the right border)',
mask: collisionMasks.bottomRightTriangle,
position: [100, -100],
},
{
description: '(A vertex inside and edges crossing at the left)',
mask: collisionMasks.leftTriangle,
position: [1, -249.5],
},
{
description: '(A vertex inside and edges crossing at the right)',
mask: collisionMasks.rightTriangle,
position: [399, -249.5],
},
].forEach(({ description, mask, position }) => {
describe(description, function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(runtimeScene, mask);
platform.setCustomWidthAndHeight(300, 300);
platform.setPosition(position[0], position[1]);
const platformBehavior = platform.getBehavior('Platform');
it('can detect a platform away downward', function () {
character.setPosition(300, -210.1);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can detect a floor to follow down', function () {
character.setPosition(300, -210);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect a floor when right on it', function () {
character.setPosition(300, -200);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect a floor to follow up', function () {
character.setPosition(300, -190);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect an obstacle a bit too high to follow', function () {
character.setPosition(300, -189.9);
checkObstacle(behavior, platformBehavior, -10, 10);
});
});
});
[
{
description: '(An edge crossing from the left to the right)',
mask: collisionMasks.square,
position: [200, -100],
},
{
description: '(A vertex inside and edges crossing at the top)',
mask: collisionMasks.topTriangle,
position: [200, -100],
},
{
description: '(An edge crossing from the left to the top)',
mask: collisionMasks.topRightTriangle,
position: [200, 0],
},
{
description: '(An edge crossing from the right to the top)',
mask: collisionMasks.topLeftTriangle,
position: [200, 0],
},
{
description: '(An edge crossing from the right to the top)',
// An edge will be lower than the character (but not under).
mask: collisionMasks.topLeftTriangleWithLowEdge,
position: [180, 20],
},
].forEach(({ description, mask, position }) => {
describe(description, function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(runtimeScene, mask);
platform.setCustomWidthAndHeight(300, 300);
platform.setPosition(position[0], position[1]);
const platformBehavior = platform.getBehavior('Platform');
it('can detect an obstacle overlapping the top', function () {
// -10 because the character can follow a platform downward.
character.setPosition(300, 199.9 - 10);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect a platform away downward', function () {
character.setPosition(300, 200 - 10);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
});
});
describe('(A platform with an hitbox under and another one above)', function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(
runtimeScene,
collisionMasks.horizontalTunnel
);
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(250, -250);
const platformBehavior = platform.getBehavior('Platform');
it('can detect a tunnel ceiling', function () {
character.setPosition(300, -210.1);
checkObstacle(behavior, platformBehavior, -10, 10);
});
// The character won't collide the ceiling anymore when he follows the floor.
// So, the ceiling should not be seen as an obstacle.
// This can happen if a tunnel is going down
it('can detect a floor to follow down', function () {
character.setPosition(300, -210);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect a floor when right on it', function () {
character.setPosition(300, -200);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect a floor to follow up', function () {
character.setPosition(300, -190);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect an obstacle a bit too high to follow', function () {
character.setPosition(300, -189.9);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect a too thin horizontal tunnel', function () {
platform.setCustomWidthAndHeight(200, 199.8);
platform.setPosition(250, -250.1);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
});
describe('(A platform with an hitbox on the left and another one on the right)', function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(
runtimeScene,
collisionMasks.verticalTunnel
);
const platformBehavior = platform.getBehavior('Platform');
it('can fell inside a vertical tunnel that fit the character', function () {
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(250, -250);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can fell inside a vertical tunnel', function () {
platform.setCustomWidthAndHeight(200.2, 200);
platform.setPosition(249.9, -250);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can detect a too thin vertical tunnel', function () {
platform.setCustomWidthAndHeight(199.8, 200);
platform.setPosition(250.1, -250);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
});
describe('(A platform sharing a vertex with the character})', function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(runtimeScene, collisionMasks.square);
const platformBehavior = platform.getBehavior('Platform');
it('can detect a platform at its exact position', function () {
platform.setCustomWidthAndHeight(100, 100);
platform.setPosition(300, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassing platform sharing the top left corner', function () {
// Shared vertex at (300, -200)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(300, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassed platform sharing the top left corner', function () {
// Shared vertex at (300, -200)
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(300, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassing platform sharing the top right corner', function () {
// Shared vertex at (400, -200)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(200, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassed platform sharing the top right corner', function () {
// Shared vertex at (400, -200)
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(350, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassing platform sharing the bottom left corner', function () {
// Shared vertex at (300, -100)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(300, -300);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassed platform sharing the bottom left corner', function () {
// Shared vertex at (300, -100)
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(300, -150);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassing platform sharing the bottom right corner', function () {
// Shared vertex at (400, -100)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(200, -300);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassed platform sharing the bottom right corner', function () {
// Shared vertex at (400, -100)
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(350, -150);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can be next to a platform sharing the top left corner', function () {
// Shared vertex at (300, -200)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(100, -200);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can be next to a platform sharing the top right corner', function () {
// Shared vertex at (400, -200)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(400, -200);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can be next to a platform sharing the bottom left corner', function () {
// Shared vertex at (300, -100)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(100, -300);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can be next to a platform sharing the bottom right corner', function () {
// Shared vertex at (400, -100)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(400, -300);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
});
});
});
});

View File

@@ -348,8 +348,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
[
// less than 1 pixel per frame (50/60)
// TODO The character can't land when falling less than 1 pixel per frame.
//50,
50,
// a commonly used value
1500,
].forEach((maxFallingSpeed) => {
@@ -475,6 +474,140 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
});
it('can track platform angle changes', function () {
// The initial pltaforms AABB are put in RBush.
runtimeScene.renderAndStep(1000 / 60);
// Now change the angle to check that the AABB is updated in RBush.
platform.setAngle(90);
// Put the character above the rotated platform.
object.setPosition(
platform.getX() + platform.getWidth() / 2,
platform.getY() +
(platform.getHeight() - platform.getWidth()) / 2 -
object.getHeight() -
10
);
for (let i = 0; i < 15; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// The character should land on it.
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getX()).to.be(30);
expect(object.getY()).to.be(-44);
});
});
});
describe('Floating-point error mitigations', function () {
it('Specific coordinates with slopeMaxAngle=0 creating Y oscillations and drift on a moving floor', function () {
const runtimeScene = makeTestRuntimeScene();
// Create a Sprite object that has the origin at a specific position (see below)
// and that has a slope max angle of 0 (so it can't climb on a floor even if it's a bit higher
// than the bottom of the object).
const object = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 1300,
maxFallingSpeed: 1000,
acceleration: 500,
deceleration: 1500,
maxSpeed: 280,
jumpSpeed: 750,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 0,
jumpSustainTime: 0.2,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 5, y: 19 },
centerPoint: { x: 5, y: 46 },
points: [
{ name: 'Center', x: 5, y: 46 },
{ name: 'Origin', x: 5, y: 19 },
],
hasCustomCollisionMask: false,
},
],
},
],
},
],
});
// Set the size of the object so that it results in a specific
// Y position for the bottom of the object AABB:
object.setUnscaledWidthAndHeight(10, 92);
object.setCustomWidthAndHeight(10, 66.0008);
// Origin Y is originally 19.
// After the scaling, it is now 19*66.0008/92=13.6306.
// Set the Y position so that the object falls at a Y position on the floor
// that would generate oscillations.
object.setPosition(0, 139.3118);
runtimeScene.addObject(object);
// Put a platform at a specific Y that can cause oscillations.
const platform = addJumpThroughPlatformObject(runtimeScene);
platform.setPosition(0, 193.000000000001);
// This means that the exact Y position the object should take is:
// platform Y - height + origin Y = 193.000000000001-66.0008+13.6306 = 140.6298
// Wait for the object to fall on the floor
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isOnFloor()).to.be(false);
// Ensure it is on the floor
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
// The Y position won't be exact because of floating point errors.
// expect(object.getY()).to.be(140.6298)
expect(object.getY()).to.be.within(140.6297999, 140.6298001);
// Move the platform by 6 pixels to the right.
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,
// it won't work because the object will get repositioned back to its old X position
// whenever the floor is considered "too high" for the object to reach.
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);
expect(object.getX()).to.be(6);
});
});
@@ -643,7 +776,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
});
});
describe('(jump and jump sustain, round coordinates on)', function () {
describe('(jump and jump sustain)', function () {
let runtimeScene;
let object;
let platform;
@@ -669,7 +802,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
roundCoordinates: true,
},
],
effects: [],
@@ -724,13 +856,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
false
);
}
// The jump finishes one frame before going back to the floor
// because the gravity is not applied on the first step.
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
expect(object.getY()).to.be(-31);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
@@ -1107,7 +1232,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
roundCoordinates: true,
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
@@ -1286,12 +1410,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
// TODO The character is not right on the platform, but a bit higher
//expect(object.getY()).to.be(jumpthru.getY() - object.getHeight());
expect(object.getY()).to.be.within(
jumpthru.getY() - object.getHeight() - 1,
jumpthru.getY() - object.getHeight()
);
expect(object.getY()).to.be(jumpthru.getY() - object.getHeight());
});
it('can fall through the jumpthru from the left side', function () {
@@ -1316,7 +1435,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
});
});
describe('(rounded coordinates, moving platforms)', function () {
describe('(moving platforms)', function () {
let runtimeScene;
let object;
let platform;
@@ -1337,7 +1456,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
roundCoordinates: true,
gravity: 900,
maxFallingSpeed: maxFallingSpeed,
acceleration: 500,
@@ -1432,10 +1550,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
// This test doesn't pass because there is no collision test.
// As long as the platform is in the result of the spacial search
// for nearby platforms the object will follow it.
it.skip('must not follow a platform that is moved over its top', function () {
it('must not follow a platform that is moved over its top', function () {
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
@@ -1456,7 +1571,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
// Check that the object falls
expect(object.getBehavior('auto1').isOnFloor()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.above(-30);
});
@@ -1532,7 +1646,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
});
// The following tests doesn't pass because the object sometimes round inside the moving platform and can't move right and left.
[-10, -10.1, -9.9].forEach((platformY) => {
[
-maxDeltaY + epsilon,
@@ -1544,7 +1657,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
0,
].forEach((deltaY) => {
[-maxDeltaX, maxDeltaX, 0].forEach((deltaX) => {
it.skip(`follows the platform moving (${deltaX}; ${deltaY}) with initial Y = ${platformY}`, function () {
it(`follows the platform moving (${deltaX}; ${deltaY}) with initial Y = ${platformY}`, function () {
platform.setPosition(platform.getX(), platformY);
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
@@ -1607,7 +1720,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
roundCoordinates: false,
gravity: 900,
maxFallingSpeed: maxFallingSpeed,
acceleration: 500,
@@ -1668,10 +1780,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
expect(object.getBehavior('auto1').isMoving()).to.be(false);
});
// The following tests doesn't pass
// because the object sometimes round inside the moving platform
// so it can't move right and left
// or there is a gap between the moving platform and the object.
[-10, -10.1, -9.9].forEach((platformY) => {
[
-maxDeltaY + epsilon,
@@ -1683,7 +1791,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
0,
].forEach((deltaY) => {
[-maxDeltaX, maxDeltaX, 0].forEach((deltaX) => {
it.skip(`follows the platform moving (${deltaX}; ${deltaY}) with initial Y = ${platformY}`, function () {
it(`follows the platform moving (${deltaX}; ${deltaY}) with initial Y = ${platformY}`, function () {
platform.setPosition(platform.getX(), platformY);
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
@@ -1741,7 +1849,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'PlatformerObject',
roundCoordinates: true,
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
@@ -1771,7 +1878,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'PlatformerObject',
roundCoordinates: true,
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
@@ -1910,7 +2016,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
roundCoordinates: true,
},
],
effects: [],
@@ -2171,11 +2276,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
}
// Falling 1 frame
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
// and directly on the floor
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
@@ -2302,7 +2402,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
ignoreDefaultControls: true,
slopeMaxAngle: slopeMaxAngle,
jumpSustainTime: 0.2,
roundCoordinates: true,
},
],
effects: [],
@@ -2397,6 +2496,31 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
);
});
it('can walk on a platform and go through a jump through', function () {
// Jumpthru that are ignored had a side effects on the search context.
// It made jumpthru appear solid when a platform was tested after them.
// Add the jumptru 1st to make RBrush gives it 1st.
// There is no causality but it does in the current implementation.
const jumpThroughPlatform = addJumpThroughPlatformObject(runtimeScene);
jumpThroughPlatform.setPosition(30, -15);
jumpThroughPlatform.setCustomWidthAndHeight(60, 10);
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
object.setPosition(10, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Walk from the 1st platform to the 2nd one.
walkRight(20);
expect(object.getX()).to.be.above(jumpThroughPlatform.getX());
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
it('can walk from a platform to another one that not aligned', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
@@ -2573,7 +2697,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
roundCoordinates: true,
},
],
effects: [],
@@ -2621,18 +2744,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
}
};
const walkLeftCanStop = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.not.be.above(lastX);
}
};
const walkLeft = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
@@ -2722,13 +2833,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
fallOnPlatform(10);
// Walk from the 1st platform to the 2nd one.
// TODO: replace by walkRight(30) when the object no longer loss its velocity at the junction.
// See https://github.com/4ian/GDevelop/issues/3013.
if (slopeAngle === 45) {
walkRightCanStop(40);
} else {
walkRight(30);
}
walkRight(30);
expect(object.getX()).to.be.above(slope.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be.below(platform.getY() - object.getHeight());
@@ -2756,22 +2861,13 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
fallOnPlatform(10);
// Walk from the 1st platform to the 2nd one.
// TODO: replace by walkRight(30) when the object no longer loss its velocity at the junction.
// See https://github.com/4ian/GDevelop/issues/3013.
if (slopeAngle === 45) {
walkLeftCanStop(40);
} else {
walkLeft(30);
}
walkLeft(30);
expect(object.getX()).to.be.below(platform.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be.below(platform.getY() - object.getHeight());
});
// TODO It fells during 1 frame.
it.skip(`can go uphill from a ${slopeAngle}° slope to a 0° slope`, function () {
it(`can go uphill from a ${slopeAngle}° slope to a 0° slope`, function () {
// Put a platform.
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(
@@ -2789,15 +2885,13 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
fallOnPlatform(12);
// Walk from the 1st platform to the 2nd one.
// TODO It fells during 1 frame.
walkRight(30);
expect(object.getX()).to.be.above(platform.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
// TODO It fells during 1 frame.
it.skip(`can go uphill from a ${slopeAngle}° slope to a 0° jump through platform`, function () {
it(`can go uphill from a ${slopeAngle}° slope to a 0° jump through platform`, function () {
// Put a platform.
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(
@@ -2818,7 +2912,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
fallOnPlatform(12);
// Walk from the 1st platform to the 2nd one.
// TODO It fells during 1 frame.
walkRight(30);
expect(object.getX()).to.be.above(jumpThroughPlatform.getX());
// Gone upward following the 2nd platform.
@@ -2857,11 +2950,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
fallOnPlatform(12);
// Walk from the 1st platform to the 2nd one.
// TODO the character stops a bit at the junction.
//walkLeft(30);
walkRightCanStop(36);
walkRight(30);
expect(object.getX()).to.be.above(slope2.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be.below(slope1.getY() - object.getHeight());
@@ -2975,7 +3064,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
object.setPosition(0, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
fallOnPlatform(11);
// Walk from the 1st platform to the 2nd one.
walkRight(30);
@@ -3153,8 +3242,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
: // slopeMaxAngle === 25
[{ angle: 26, height: 25 }]
).forEach((slopesDimension) => {
// TODO It climbs the first pixel of the slope.
it.skip(`can't go uphill on a too steep slope (${slopesDimension.angle}°)`, function () {
it(`can't go uphill on a too steep slope (${slopesDimension.angle}°)`, function () {
// Put a platform.
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(50, slopesDimension.height);
@@ -3208,11 +3296,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
behavior.isOnFloor() || behavior.isFallingWithoutJumping()
).to.be(true);
expect(object.getX()).to.be.above(lastX);
// TODO The object Y doesn't change every frame because it moves pixel by pixel.
//expect(object.getY()).to.be.above(lastY);
expect(object.getY()).not.to.be.below(lastY);
expect(object.getY()).to.be.above(lastY);
// Check that the object doesn't stop
expect(behavior.getCurrentSpeed()).to.be.above(lastSpeed);
}
@@ -3237,8 +3321,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
// A rotated platform will probably result to not pixel aligned junctions.
48.9,
].forEach((slopeJunctionX) => {
// TODO It climbs the first pixel of the slope.
it.skip(`(slopeJunctionX: ${slopeJunctionX}) can't go uphill from a 0° slope to a too steep slope (${slopesDimension.angle}°) going right`, function () {
it(`(slopeJunctionX: ${slopeJunctionX}) can't go uphill from a 0° slope to a too steep slope (${slopesDimension.angle}°) going right`, function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setCustomWidthAndHeight(slopeJunctionX, 50);
@@ -3287,8 +3370,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
// A rotated platform will probably result to not pixel aligned junctions.
51.1,
].forEach((slopeJunctionX) => {
// TODO It climbs the first pixel of the slope.
it.skip(`(slopeJunctionX: ${slopeJunctionX}) can't go uphill from a 0° slope to a too steep slope (${slopesDimension.angle}°) going left`, function () {
it(`(slopeJunctionX: ${slopeJunctionX}) can't go uphill from a 0° slope to a too steep slope (${slopesDimension.angle}°) going left`, function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setCustomWidthAndHeight(100 - slopeJunctionX, 50);
@@ -3440,9 +3522,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// TODO The character is not right on the platform, but a bit higher
//expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getY()).to.be.within(-31, 30);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// walk right
for (let i = 0; i < 25; ++i) {
@@ -3452,12 +3532,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
}
// is blocked by the wall
expect(object.getX()).to.be(wall.getX() - objectWidth);
// TODO The character is not right on the platform, but a bit higher
//expect(object.getY()).to.be(platform.getY() - objectHeight);
expect(object.getY()).to.be.within(
platform.getY() - objectHeight - 1,
platform.getY() - objectHeight
);
expect(object.getY()).to.be(platform.getY() - objectHeight);
});
});
});

View File

@@ -94,8 +94,14 @@ namespace gdjs {
}
updatePosition(): void {
this._text.position.x = this._object.x + this._text.width / 2;
this._text.position.y = this._object.y + this._text.height / 2;
const angle = gdjs.toRad(this._object.angle);
const width = this._text.width / 2;
const height = this._text.height / 2;
const xDelta = width * Math.cos(angle) - height * Math.sin(angle);
const yDelta = height * Math.cos(angle) + width * Math.sin(angle);
this._text.position.x = this._object.x + xDelta;
this._text.position.y = this._object.y + yDelta;
}
updateAngle(): void {

View File

@@ -76,11 +76,19 @@ namespace gdjs {
}
updateXOffset(): void {
this._tiledSprite.tilePosition.x = -this._object._xOffset;
// Known PIXI.js issue, the coordinates should not exceed the width/height of the texture,
// otherwise the texture will be pixelated over time.
// See https://github.com/pixijs/pixijs/issues/7891#issuecomment-947549553
this._tiledSprite.tilePosition.x =
-this._object._xOffset % this._tiledSprite.texture.width;
}
updateYOffset(): void {
this._tiledSprite.tilePosition.y = -this._object._yOffset;
// Known PIXI.js issue, the coordinates should not exceed the width/height of the texture,
// otherwise the texture will be pixelated over time.
// See https://github.com/pixijs/pixijs/issues/7891#issuecomment-947549553
this._tiledSprite.tilePosition.y =
-this._object._yOffset % this._tiledSprite.texture.height;
}
setColor(rgbColor): void {

View File

@@ -1,22 +0,0 @@
Copyright (c) 2013 Jeremy Kahn
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

571
Extensions/TweenBehavior/shifty.d.ts vendored Normal file
View File

@@ -0,0 +1,571 @@
// Shifty.js 2.16.0 type definitions by arthuro555
declare namespace shifty {
// index.js
type easingFunction = (position: number) => number;
type startFunction = (state: any, data?: any) => any;
type finishFunction = (promisedData: shifty.promisedData) => any;
/**
* Gets called for every tick of the tween. This function is not called on the
* final tick of the animation.
*/
type renderFunction = (
state: any,
data: any | undefined,
timeElapsed: number
) => any;
type scheduleFunction = (callback: Function, timeout: number) => any;
interface tweenConfig {
/**
* Starting position. If omitted, {@link * shifty.Tweenable#get} is used.
*/
from?: any;
/**
* Ending position. The keys of this Object should
* match those of `to`.
*/
to?: any;
/**
* How many milliseconds to animate for.
*/
duration?: number;
/**
* How many milliseconds to wait before starting the
* tween.
*/
delay?: number;
/**
* Executes when the tween begins.
*/
start?: shifty.startFunction;
/**
* Executes when the tween
* completes. This will get overridden by {@link shifty.Tweenablethen } if that
* is called, and it will not fire if {@link shifty.Tweenablecancel } is
* called.
*/
finish?: shifty.finishFunction;
/**
* Executes on every tick. Shifty
* assumes a [retained mode](https://en.wikipedia.org/wiki/Retained_mode)
* rendering environment, which in practice means that `render` only gets
* called when the tween state changes. Importantly, this means that `render`
* is _not_ called when a tween is not animating (for instance, when it is
* paused or waiting to start via the `delay` option). This works naturally
* with DOM environments, but you may need to account for this design in more
* custom environments such as `<canvas>`.
*
* Legacy property name: `step`.
*/
render?: shifty.renderFunction;
/**
* Easing curve name(s) or {@link shifty.easingFunction }(s) to apply
* to the properties of the tween. If this is an Object, the keys should
* correspond to `to`/`from`. You can learn more about this in the {@tutorial
* easing-function-in-depth} tutorial.
*/
easing?: Record<string, easingFunction> | string | easingFunction;
/**
* Data that is passed to {@link * shifty.startFunction}, {@link shifty.renderFunction }, and {@link * shifty.promisedData}. Legacy property name: `attachment`.
*/
data?: any;
/**
* Promise constructor for when you want
* to use Promise library or polyfill Promises in unsupported environments.
*/
promise?: Function;
}
type promisedData = {
/**
* The current state of the tween.
*/
state: any;
/**
* The `data` Object that the tween was configured with.
*/
data: any;
/**
* The {@link shifty.Tweenable } instance to
* which the tween belonged.
*/
tweenable: Tweenable;
};
/**
* Is called when a tween is created to determine if a filter is needed.
* Filters are only added to a tween when it is created so that they are not
* unnecessarily processed if they don't apply during an update tick.
*/
type doesApplyFilter = (tweenable: any) => boolean;
/**
* Is called when a tween is created. This should perform any setup needed by
* subsequent per-tick calls to {@link shifty.beforeTween } and {@link * shifty.afterTween}.
*/
type tweenCreatedFilter = (tweenable: any) => any;
/**
* Is called right before a tween is processed in a tick.
*/
type beforeTweenFilter = (tweenable: any) => any;
/**
* Is called right after a tween is processed in a tick.
*/
type afterTweenFilter = (tweenable: any) => any;
/**
* An Object that contains functions that are called at key points in a tween's
* lifecycle. Shifty can only process `Number`s internally, but filters can
* expand support for any type of data. This is the mechanism that powers
* [string interpolation]{@tutorial string-interpolation}.
*/
type filter = {
/**
* Is called when a tween is
* created.
*/
doesApply: shifty.doesApplyFilter;
/**
* Is called when a tween is
* created.
*/
tweenCreated: shifty.tweenCreatedFilter;
/**
* Is called right before a
* tween starts.
*/
beforeTween: shifty.beforeTweenFilter;
/**
* Is called right after a tween
* ends.
*/
afterTween: shifty.afterTweenFilter;
};
// easing-functions.js
export function linear(pos: number): number;
export function easeInQuad(pos: any): number;
export function easeOutQuad(pos: any): number;
export function easeInOutQuad(pos: any): number;
export function easeInCubic(pos: any): number;
export function easeOutCubic(pos: any): number;
export function easeInOutCubic(pos: any): number;
export function easeInQuart(pos: any): number;
export function easeOutQuart(pos: any): number;
export function easeInOutQuart(pos: any): number;
export function easeInQuint(pos: any): number;
export function easeOutQuint(pos: any): number;
export function easeInOutQuint(pos: any): number;
export function easeInSine(pos: any): number;
export function easeOutSine(pos: any): number;
export function easeInOutSine(pos: any): number;
export function easeInExpo(pos: any): number;
export function easeOutExpo(pos: any): number;
export function easeInOutExpo(pos: any): number;
export function easeInCirc(pos: any): number;
export function easeOutCirc(pos: any): number;
export function easeInOutCirc(pos: any): number;
export function easeOutBounce(pos: any): number;
export function easeInBack(pos: any): number;
export function easeOutBack(pos: any): number;
export function easeInOutBack(pos: any): number;
export function elastic(pos: any): number;
export function swingFromTo(pos: any): number;
export function swingFrom(pos: any): number;
export function swingTo(pos: any): number;
export function bounce(pos: any): number;
export function bouncePast(pos: any): number;
export function easeFromTo(pos: any): number;
export function easeFrom(pos: any): number;
export function easeTo(pos: any): number;
// bezier.js
export function setBezierFunction(
name: string,
x1: number,
y1: number,
x2: number,
y2: number
): any;
export function unsetBezierFunction(name: string): boolean;
// interpolate.js
export function interpolate<T extends Object>(
from: T,
to: T,
position: number,
easing: Record<string, easingFunction> | string | easingFunction,
delay?: number
): T;
// scene.js
export class Scene {
/**
* The {@link shifty.Scene} class provides a way to control groups of {@link
* shifty.Tweenable}s. It is lightweight, minimalistic, and meant to provide
* performant {@link shifty.Tweenable} batch control that users of Shifty
* might otherwise have to implement themselves. It is **not** a robust
* timeline solution, and it does **not** provide utilities for sophisticated
* animation sequencing or orchestration. If that is what you need for your
* project, consider using a more robust tool such as
* [Rekapi](http://jeremyckahn.github.io/rekapi/doc/) (a timeline layer built
* on top of Shifty).
*
* Please be aware that {@link shifty.Scene} does **not** perform any
* automatic cleanup. If you want to remove a {@link shifty.Tweenable} from a
* {@link shifty.Scene}, you must do so explicitly with either {@link
* shifty.Scene#remove} or {@link shifty.Scene#empty}.
*
* <p class="codepen" data-height="677" data-theme-id="0" data-default-tab="js,result" data-user="jeremyckahn" data-slug-hash="qvZKbe" style="height: 677px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="Shifty Scene Demo">
* <span>See the Pen <a href="https://codepen.io/jeremyckahn/pen/qvZKbe/">
* Shifty Scene Demo</a> by Jeremy Kahn (<a href="https://codepen.io/jeremyckahn">@jeremyckahn</a>)
* on <a href="https://codepen.io">CodePen</a>.</span>
* </p>
* <script async src="https://static.codepen.io/assets/embed/ei.js"></script>
* @param {...shifty.Tweenable} tweenables
* @see https://codepen.io/jeremyckahn/pen/qvZKbe
* @constructs shifty.Scene
*/
constructor(...tweenables: Tweenable[]);
/**
* A copy of the internal {@link shifty.Tweenable}s array.
* @member shifty.Scene#tweenables
* @type {Array.<shifty.Tweenable>}
* @readonly
*/
get tweenables(): Tweenable[];
/**
* The {@link external:Promise}s for all {@link shifty.Tweenable}s in this
* {@link shifty.Scene} that have been configured with {@link
* shifty.Tweenable#setConfig}. Note that each call of {@link
* shifty.Scene#play} or {@link shifty.Scene#pause} creates new {@link
* external:Promise}s:
*
* const scene = new Scene(new Tweenable());
* scene.play();
*
* Promise.all(scene.promises).then(() =>
* // Plays the scene again upon completion, but a new promise is
* // created so this line only runs once.
* scene.play()
* );
*
* @member shifty.Scene#promises
* @type {Array.<external:Promise>}
* @readonly
*/
get promises(): Promise<Object>[];
/**
* Add a {@link shifty.Tweenable} to be controlled by this {@link
* shifty.Scene}.
* @method shifty.Scene#add
* @param {shifty.Tweenable} tweenable
* @return {shifty.Tweenable} The {@link shifty.Tweenable} that was added.
*/
add(tweenable: Tweenable): Tweenable;
/**
* Remove a {@link shifty.Tweenable} that is controlled by this {@link
* shifty.Scene}.
* @method shifty.Scene#remove
* @param {shifty.Tweenable} tweenable
* @return {shifty.Tweenable} The {@link shifty.Tweenable} that was removed.
*/
remove(tweenable: Tweenable): Tweenable;
/**
* [Remove]{@link shifty.Scene#remove} all {@link shifty.Tweenable}s in this {@link
* shifty.Scene}.
* @method shifty.Scene#empty
* @return {Array.<shifty.Tweenable>} The {@link shifty.Tweenable}s that were
* removed.
*/
empty(): Array<Tweenable>;
/**
* Is `true` if any {@link shifty.Tweenable} in this {@link shifty.Scene} is
* playing.
* @method shifty.Scene#isPlaying
* @return {boolean}
*/
isPlaying(): boolean;
/**
* Play all {@link shifty.Tweenable}s from their beginning.
* @method shifty.Scene#play
* @return {shifty.Scene}
*/
play(): Scene;
/**
* {@link shifty.Tweenable#pause} all {@link shifty.Tweenable}s in this
* {@link shifty.Scene}.
* @method shifty.Scene#pause
* @return {shifty.Scene}
*/
pause(): Scene;
/**
* {@link shifty.Tweenable#resume} all paused {@link shifty.Tweenable}s.
* @method shifty.Scene#resume
* @return {shifty.Scene}
*/
resume(): Scene;
/**
* {@link shifty.Tweenable#stop} all {@link shifty.Tweenable}s in this {@link
* shifty.Scene}.
* @method shifty.Scene#stop
* @param {boolean} [gotoEnd]
* @return {shifty.Scene}
*/
stop(gotoEnd?: boolean): Scene;
}
// tweenable.js
/**
* @method shifty.tween
* @param {shifty.tweenConfig} [config={}]
* @description Standalone convenience method that functions identically to
* {@link shifty.Tweenable#tween}. You can use this to create tweens without
* needing to set up a {@link shifty.Tweenable} instance.
*
* ```
* import { tween } from 'shifty';
*
* tween({ from: { x: 0 }, to: { x: 10 } }).then(
* () => console.log('All done!')
* );
* ```
*
* @returns {shifty.Tweenable} A new {@link shifty.Tweenable} instance.
*/
export function tween(config?: tweenConfig): Tweenable;
export function tweenProps(
forPosition: number,
currentState: any,
originalState: any,
targetState: any,
duration: number,
timestamp: number,
easing: Record<any, string | Function>
): Object;
export function processTweens(): void;
export function scheduleUpdate(): void;
export function composeEasingObject(
fromTweenParams: any,
easing?: any | string | Function,
composedEasing?: any
): any | Function;
export class Tweenable {
/**
* @method shifty.Tweenable.now
* @static
* @returns {number} The current timestamp.
*/
static now: () => number;
/**
* @param {Object} [initialState={}] The values that the initial tween should
* start at if a `from` value is not provided to {@link
* shifty.Tweenable#tween} or {@link shifty.Tweenable#setConfig}.
* @param {shifty.tweenConfig} [config] Configuration object to be passed to
* {@link shifty.Tweenable#setConfig}.
* @constructs shifty.Tweenable
*/
constructor(initialState?: Object, config?: tweenConfig);
private _config: tweenConfig;
private _data: Object;
private _delay: number;
private _filters: filter[];
private _next: any;
private _previous: any;
private _timestamp: number;
private _resolve: any;
private _reject: (reason?: any) => void;
private _currentState: any;
private _originalState: Object;
private _targetState: Object;
private _start: () => void;
private _render: () => void;
private _promiseCtor: PromiseConstructor;
/**
* Applies a filter to Tweenable instance.
* @param {string} filterName The name of the filter to apply.
* @private
*/
private _applyFilter;
private _isPlaying: boolean;
private _pausedAtTime: number;
private _duration: any;
private _scheduleId: any;
private _easing: any;
/**
* Configure and start a tween. If this {@link shifty.Tweenable}'s instance
* is already running, then it will stop playing the old tween and
* immediately play the new one.
* @method shifty.Tweenable#tween
* @param {shifty.tweenConfig} [config] Gets passed to {@link
* shifty.Tweenable#setConfig}.
* @return {shifty.Tweenable}
*/
tween(config?: tweenConfig): this;
/**
* Configure a tween that will start at some point in the future. Aside from
* `delay`, `from`, and `to`, each configuration option will automatically
* default to the same option used in the preceding tween of this {@link
* shifty.Tweenable} instance.
* @method shifty.Tweenable#setConfig
* @param {shifty.tweenConfig} [config={}]
* @return {shifty.Tweenable}
*/
setConfig(config?: tweenConfig): this;
/**
* Overrides any `finish` function passed via a {@link shifty.tweenConfig}.
* @method shifty.Tweenable#then
* @param {function} onFulfilled Receives {@link shifty.promisedData} as the
* first parameter.
* @param {function} onRejected Receives {@link shifty.promisedData} as the
* first parameter.
* @return {external:Promise}
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
*/
then(onFulfilled: Function, onRejected?: Function): Promise<any>;
private _promise: Promise<any>;
/**
* @method shifty.Tweenable#catch
* @param {function} onRejected Receives {@link shifty.promisedData} as the
* first parameter.
* @return {external:Promise}
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
*/
catch(onRejected: Function): Promise<any>;
/**
* @method shifty.Tweenable#get
* @return {Object} The current state.
*/
get(): Object;
/**
* Set the current state.
* @method shifty.Tweenable#set
* @param {Object} state The state to set.
*/
set(state: Object): void;
/**
* Pause a tween. Paused tweens can be resumed from the point at which they
* were paused. If a tween is not running, this is a no-op.
* @method shifty.Tweenable#pause
* @return {shifty.Tweenable}
*/
pause(): this;
/**
* Resume a paused tween.
* @method shifty.Tweenable#resume
* @return {shifty.Tweenable}
*/
resume(): this;
private _resume(currentTime?: number): any;
/**
* Move the state of the animation to a specific point in the tween's
* timeline. If the animation is not running, this will cause {@link
* shifty.renderFunction} handlers to be called.
* @method shifty.Tweenable#seek
* @param {millisecond} millisecond The millisecond of the animation to seek
* to. This must not be less than `0`.
* @return {shifty.Tweenable}
*/
seek(millisecond: number): this;
/**
* Stops a tween. If a tween is not running, this is a no-op. This method
* does not cancel the tween {@link external:Promise}. For that, use {@link
* shifty.Tweenable#cancel}.
* @param {boolean} [gotoEnd] If `false`, the tween just stops at its current
* state. If `true`, the tweened object's values are instantly set to the
* target values.
* @method shifty.Tweenable#stop
* @return {shifty.Tweenable}
*/
stop(gotoEnd?: boolean): this;
/**
* {@link shifty.Tweenable#stop}s a tween and also `reject`s its {@link
* external:Promise}. If a tween is not running, this is a no-op. Prevents
* calling any provided `finish` function.
* @param {boolean} [gotoEnd] Is propagated to {@link shifty.Tweenable#stop}.
* @method shifty.Tweenable#cancel
* @return {shifty.Tweenable}
* @see https://github.com/jeremyckahn/shifty/issues/122
*/
cancel(gotoEnd?: boolean): this;
/**
* Whether or not a tween is running.
* @method shifty.Tweenable#isPlaying
* @return {boolean}
*/
isPlaying(): boolean;
/**
* @method shifty.Tweenable#setScheduleFunction
* @param {shifty.scheduleFunction} scheduleFunction
* @deprecated Will be removed in favor of {@link shifty.Tweenable.setScheduleFunction} in 3.0.
*/
setScheduleFunction(scheduleFunction: scheduleFunction): void;
/**
* Get and optionally set the data that gets passed as `data` to {@link
* shifty.promisedData}, {@link shifty.startFunction} and {@link
* shifty.renderFunction}.
* @param {Object} [data]
* @method shifty.Tweenable#data
* @return {Object} The internally stored `data`.
*/
data(data?: any): any;
/**
* `delete` all "own" properties. Call this when the {@link
* shifty.Tweenable} instance is no longer needed to free memory.
* @method shifty.Tweenable#dispose
*/
dispose(): void;
}
export namespace Tweenable {
/**
* Set a custom schedule function.
*
* By default,
* [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame)
* is used if available, otherwise
* [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout)
* is used.
* @method shifty.Tweenable.setScheduleFunction
* @param {shifty.scheduleFunction} fn The function to be
* used to schedule the next frame to be rendered.
* @return {shifty.scheduleFunction} The function that was set.
*/
export function setScheduleFunction(fn: scheduleFunction): scheduleFunction;
export const filters: any;
}
// token.js
export function tweenCreated(tweenable: any): void;
export function beforeTween(tweenable: any): void;
export function afterTween(tweenable: any): void;
export function doesApply(tweenable: any): boolean;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
/*!
* TERMS OF USE - EASING EQUATIONS
* Open source under the BSD License.
* Easing Equations (c) 2003 Robert Penner, all rights reserved.
*/
/*!
* All equations are adapted from Thomas Fuchs'
* [Scripty2](https://github.com/madrobby/scripty2/blob/master/src/effects/transitions/penner.js).
*
* Based on Easing Equations (c) 2003 [Robert
* Penner](http://www.robertpenner.com/), all rights reserved. This work is
* [subject to terms](http://www.robertpenner.com/easing_terms_of_use.html).
*/
/*! Shifty 2.16.0 - https://github.com/jeremyckahn/shifty */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -54,6 +54,12 @@ BaseObjectExtension::BaseObjectExtension() {
objectConditions["Angle"]
.SetFunctionName("getAngle")
.SetIncludeFile("runtimeobject.js");
objectConditions["BoundingBoxLeft"].SetFunctionName("getAABBLeft");
objectConditions["BoundingBoxTop"].SetFunctionName("getAABBTop");
objectConditions["BoundingBoxRight"].SetFunctionName("getAABBRight");
objectConditions["BoundingBoxBottom"].SetFunctionName("getAABBBottom");
objectConditions["BoundingBoxCenterX"].SetFunctionName("getAABBCenterX");
objectConditions["BoundingBoxCenterY"].SetFunctionName("getAABBCenterY");
objectActions["Rotate"].SetFunctionName("rotate").SetIncludeFile(
"runtimeobject.js");
objectActions["RotateTowardAngle"]
@@ -193,6 +199,12 @@ BaseObjectExtension::BaseObjectExtension() {
objectExpressions["Y"].SetFunctionName("getY");
objectExpressions["CenterX"].SetFunctionName("getCenterXInScene");
objectExpressions["CenterY"].SetFunctionName("getCenterYInScene");
objectExpressions["BoundingBoxLeft"].SetFunctionName("getAABBLeft");
objectExpressions["BoundingBoxTop"].SetFunctionName("getAABBTop");
objectExpressions["BoundingBoxRight"].SetFunctionName("getAABBRight");
objectExpressions["BoundingBoxBottom"].SetFunctionName("getAABBBottom");
objectExpressions["BoundingBoxCenterX"].SetFunctionName("getAABBCenterX");
objectExpressions["BoundingBoxCenterY"].SetFunctionName("getAABBCenterY");
objectExpressions["ZOrder"].SetFunctionName("getZOrder");
objectExpressions["Plan"].SetFunctionName("getZOrder"); // Deprecated
objectExpressions["Width"].SetFunctionName("getWidth");

View File

@@ -82,7 +82,8 @@ bool Exporter::ExportWholePixiProject(
// Export engine libraries
helper.AddLibsInclude(
/*pixiRenderers=*/true,
/*websocketDebuggerClient=*/false,
/*includeWebsocketDebuggerClient=*/false,
/*includeWindowMessageDebuggerClient=*/false,
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
@@ -119,7 +120,7 @@ bool Exporter::ExportWholePixiProject(
helper.ExportProjectData(
fs, exportedProject, codeOutputDir + "/data.js", noRuntimeGameOptions);
includesFiles.push_back(codeOutputDir + "/data.js");
helper.ExportIncludesAndLibs(includesFiles, exportDir, false);
gd::String source = gdjsRoot + "/Runtime/index.html";

View File

@@ -101,7 +101,10 @@ bool ExporterHelper::ExportProjectForPixiPreview(
// Export engine libraries
AddLibsInclude(/*pixiRenderers=*/true,
/*websocketDebuggerClient=*/true,
/*includeWebsocketDebuggerClient=*/
!options.websocketDebuggerServerAddress.empty(),
/*includeWindowMessageDebuggerClient=*/
options.useWindowMessageDebuggerClient,
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
@@ -147,10 +150,10 @@ bool ExporterHelper::ExportProjectForPixiPreview(
}
runtimeGameOptions.AddChild("projectDataOnlyExport")
.SetBoolValue(options.projectDataOnlyExport);
runtimeGameOptions.AddChild("debuggerServerAddress")
.SetStringValue(options.debuggerServerAddress);
runtimeGameOptions.AddChild("debuggerServerPort")
.SetStringValue(options.debuggerServerPort);
runtimeGameOptions.AddChild("websocketDebuggerServerAddress")
.SetStringValue(options.websocketDebuggerServerAddress);
runtimeGameOptions.AddChild("websocketDebuggerServerPort")
.SetStringValue(options.websocketDebuggerServerPort);
// Pass in the options the list of scripts files - useful for hot-reloading.
auto &scriptFilesElement = runtimeGameOptions.AddChild("scriptFiles");
@@ -521,7 +524,8 @@ bool ExporterHelper::CompleteIndexFile(
}
void ExporterHelper::AddLibsInclude(bool pixiRenderers,
bool websocketDebuggerClient,
bool includeWebsocketDebuggerClient,
bool includeWindowMessageDebuggerClient,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles) {
// First, do not forget common includes (they must be included before events
@@ -571,10 +575,16 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "splash/gd-logo-light.js");
}
if (websocketDebuggerClient) {
InsertUnique(includesFiles, "websocket-debugger-client/hot-reloader.js");
if (includeWebsocketDebuggerClient || includeWindowMessageDebuggerClient) {
InsertUnique(includesFiles, "debugger-client/hot-reloader.js");
InsertUnique(includesFiles, "debugger-client/abstract-debugger-client.js");
}
if (includeWebsocketDebuggerClient) {
InsertUnique(includesFiles, "debugger-client/websocket-debugger-client.js");
}
if (includeWindowMessageDebuggerClient) {
InsertUnique(includesFiles,
"websocket-debugger-client/websocket-debugger-client.js");
"debugger-client/window-message-debugger-client.js");
}
if (pixiRenderers) {

View File

@@ -34,18 +34,28 @@ struct PreviewExportOptions {
PreviewExportOptions(gd::Project &project_, const gd::String &exportPath_)
: project(project_),
exportPath(exportPath_),
useWindowMessageDebuggerClient(false),
projectDataOnlyExport(false),
fullLoadingScreen(false),
nonRuntimeScriptsCacheBurst(0){};
/**
* \brief Set the address of the debugger server that the game should reach
* out to, using WebSockets.
* out to, using the "WebSockets" debugger client.
*/
PreviewExportOptions &SetDebuggerServerAddress(const gd::String &address,
const gd::String &port) {
debuggerServerAddress = address;
debuggerServerPort = port;
PreviewExportOptions &UseWebsocketDebuggerClientWithServerAddress(
const gd::String &address, const gd::String &port) {
websocketDebuggerServerAddress = address;
websocketDebuggerServerPort = port;
return *this;
}
/**
* \brief Set that the game should connect to the debugger server using
* the "Window Message " debugger client.
*/
PreviewExportOptions &UseWindowMessageDebuggerClient() {
useWindowMessageDebuggerClient = true;
return *this;
}
@@ -107,8 +117,9 @@ struct PreviewExportOptions {
gd::Project &project;
gd::String exportPath;
gd::String debuggerServerAddress;
gd::String debuggerServerPort;
gd::String websocketDebuggerServerAddress;
gd::String websocketDebuggerServerPort;
bool useWindowMessageDebuggerClient;
gd::String layoutName;
gd::String externalLayoutName;
std::map<gd::String, int> includeFileHashes;
@@ -164,10 +175,11 @@ class ExporterHelper {
gd::String exportDir);
/**
* \brief Add libraries files from Pixi.js to the list of includes.
* \brief Add libraries files to the list of includes.
*/
void AddLibsInclude(bool pixiRenderers,
bool websocketDebuggerClient,
bool includeWebsocketDebuggerClient,
bool includeWindowMessageDebuggerClient,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles);

View File

@@ -0,0 +1,431 @@
namespace gdjs {
const logger = new gdjs.Logger('Debugger client');
const originalConsole = {
log: console.log,
info: console.info,
debug: console.debug,
warn: console.warn,
error: console.error,
};
/**
* A function used to replace circular references with a new value.
* @param key - The key corresponding to the value.
* @param value - The value.
* @returns The new value.
*/
type DebuggerClientCycleReplacer = (key: string, value: any) => any;
/**
* Generates a JSON serializer that prevent circular references and stop if maxDepth is reached.
* @param [replacer] - A function called for each property on the object or array being stringified, with the property key and its value, and that returns the new value. If not specified, values are not altered.
* @param [cycleReplacer] - Function used to replace circular references with a new value.
* @param [maxDepth] - The maximum depth, after which values are replaced by a string ("[Max depth reached]"). If not specified, there is no maximum depth.
*/
const depthLimitedSerializer = (
replacer?: DebuggerClientCycleReplacer,
cycleReplacer?: DebuggerClientCycleReplacer,
maxDepth?: number
): DebuggerClientCycleReplacer => {
const stack: Array<string> = [],
keys: Array<string> = [];
if (cycleReplacer === undefined || cycleReplacer === null) {
cycleReplacer = function (key, value) {
if (stack[0] === value) {
return '[Circular ~]';
}
return (
'[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'
);
};
}
return function (key: string, value: any): any {
if (stack.length > 0) {
const thisPos = stack.indexOf(this);
~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
if (maxDepth != null && thisPos > maxDepth) {
return '[Max depth reached]';
} else {
if (~stack.indexOf(value)) {
value = (cycleReplacer as DebuggerClientCycleReplacer).call(
this,
key,
value
);
}
}
} else {
stack.push(value);
}
return replacer == null ? value : replacer.call(this, key, value);
};
};
/**
* This is an alternative to JSON.stringify that ensure that circular references
* are replaced by a placeholder.
*
* @param obj - The object to serialize.
* @param [replacer] - A function called for each property on the object or array being stringified, with the property key and its value, and that returns the new value. If not specified, values are not altered.
* @param [maxDepth] - The maximum depth, after which values are replaced by a string ("[Max depth reached]"). If not specified, there is no maximum depth.
* @param [spaces] - The number of spaces for indentation.
* @param [cycleReplacer] - Function used to replace circular references with a new value.
*/
const circularSafeStringify = (
obj: any,
replacer?: DebuggerClientCycleReplacer,
maxDepth?: number,
spaces?: number,
cycleReplacer?: DebuggerClientCycleReplacer
) => {
return JSON.stringify(
obj,
depthLimitedSerializer(replacer, cycleReplacer, maxDepth),
spaces
);
};
/**
* The base class describing a debugger client, that can be used to inspect
* a runtime game (dump its state) or alter it.
*/
export abstract class AbstractDebuggerClient {
_runtimegame: gdjs.RuntimeGame;
_hotReloader: gdjs.HotReloader;
_originalConsole = originalConsole;
constructor(runtimeGame: RuntimeGame) {
this._runtimegame = runtimeGame;
this._hotReloader = new gdjs.HotReloader(runtimeGame);
const redirectJsLog = (
type: 'info' | 'warning' | 'error',
...messages: any[]
) => {
this.log(
'JavaScript',
messages.reduce((accumulator, value) => accumulator + value, ''),
type,
false
);
};
// Hook the console logging functions to log to the Debugger as well
console.log = (...messages: any[]) => {
originalConsole.log(...messages);
redirectJsLog('info', ...messages);
};
console.debug = (...messages: any[]) => {
originalConsole.debug(...messages);
redirectJsLog('info', ...messages);
};
console.info = (...messages: any[]) => {
originalConsole.info(...messages);
redirectJsLog('info', ...messages);
};
console.warn = (...messages: any[]) => {
originalConsole.warn(...messages);
redirectJsLog('warning', ...messages);
};
console.error = (...messages: any[]) => {
originalConsole.error(...messages);
redirectJsLog('error', ...messages);
};
// Overwrite the default GDJS log outputs so that they
// both go to the console (or wherever they were configured to go)
// and sent to the remote debugger.
const existingLoggerOutput = gdjs.Logger.getLoggerOutput();
gdjs.Logger.setLoggerOutput({
log: (
group: string,
message: string,
type: 'info' | 'warning' | 'error' = 'info',
internal = true
) => {
existingLoggerOutput.log(group, message, type, internal);
this.log(group, message, type, internal);
},
});
}
/**
* Should be called by derived class to handle a command
* received from the debugger server.
*
* @param data An object containing the command to do.
*/
protected handleCommand(data: any) {
const that = this;
const runtimeGame = this._runtimegame;
if (data && data.command) {
if (data.command === 'play') {
runtimeGame.pause(false);
} else if (data.command === 'pause') {
runtimeGame.pause(true);
that.sendRuntimeGameDump();
} else if (data.command === 'refresh') {
that.sendRuntimeGameDump();
} else if (data.command === 'set') {
that.set(data.path, data.newValue);
} else if (data.command === 'call') {
that.call(data.path, data.args);
} else if (data.command === 'profiler.start') {
runtimeGame.startCurrentSceneProfiler(function (stoppedProfiler) {
that.sendProfilerOutput(
stoppedProfiler.getFramesAverageMeasures(),
stoppedProfiler.getStats()
);
that.sendProfilerStopped();
});
that.sendProfilerStarted();
} else if (data.command === 'profiler.stop') {
runtimeGame.stopCurrentSceneProfiler();
} else if (data.command === 'hotReload') {
that._hotReloader.hotReload().then((logs) => {
that.sendHotReloaderLogs(logs);
});
} else {
logger.info(
'Unknown command "' + data.command + '" received by the debugger.'
);
}
} else {
logger.info('Debugger received a message with badly formatted data.');
}
}
/**
* Should be re-implemented by derived class to send a stringified message object
* to the debugger server.
* @param message
*/
protected abstract _sendMessage(message: string): void;
/**
* Send a message (a log) to debugger server.
*/
log(
group: string,
message: string,
type: 'info' | 'warning' | 'error',
internal: boolean
) {
this._sendMessage(
JSON.stringify({
command: 'console.log',
payload: {
message,
type,
group,
internal,
timestamp: performance.now(),
},
})
);
}
/**
* Update a value, specified by a path starting from the {@link RuntimeGame} instance.
* @param path - The path to the variable, starting from {@link RuntimeGame}.
* @param newValue - The new value.
* @return Was the operation successful?
*/
set(path: string[], newValue: any): boolean {
if (!path || !path.length) {
logger.warn('No path specified, set operation from debugger aborted');
return false;
}
let object = this._runtimegame;
let currentIndex = 0;
while (currentIndex < path.length - 1) {
const key = path[currentIndex];
if (!object || !object[key]) {
logger.error('Incorrect path specified. No ' + key + ' in ', object);
return false;
}
object = object[key];
currentIndex++;
}
// Ensure the newValue is properly typed to avoid breaking anything in
// the game engine.
const currentValue = object[path[currentIndex]];
if (typeof currentValue === 'number') {
newValue = parseFloat(newValue);
} else {
if (typeof currentValue === 'string') {
newValue = '' + newValue;
}
}
logger.log('Updating', path, 'to', newValue);
object[path[currentIndex]] = newValue;
return true;
}
/**
* Call a method, specified by a path starting from the {@link RuntimeGame} instance.
* @param path - The path to the method, starting from {@link RuntimeGame}.
* @param args - The arguments to pass the method.
* @return Was the operation successful?
*/
call(path: string[], args: any[]): boolean {
if (!path || !path.length) {
logger.warn('No path specified, call operation from debugger aborted');
return false;
}
let object = this._runtimegame;
let currentIndex = 0;
while (currentIndex < path.length - 1) {
const key = path[currentIndex];
if (!object || !object[key]) {
logger.error('Incorrect path specified. No ' + key + ' in ', object);
return false;
}
object = object[key];
currentIndex++;
}
if (!object[path[currentIndex]]) {
logger.error('Unable to call', path);
return false;
}
logger.log('Calling', path, 'with', args);
object[path[currentIndex]].apply(object, args);
return true;
}
/**
* Dump all the relevant data from the {@link RuntimeGame} instance and send it to the server.
*/
sendRuntimeGameDump(): void {
const that = this;
const message = { command: 'dump', payload: this._runtimegame };
const serializationStartTime = Date.now();
// Stringify the message, excluding some known data that are big and/or not
// useful for the debugger.
const excludedValues = [that._runtimegame.getGameData()];
const excludedKeys = [
// Exclude reference to the debugger
'_debuggerClient',
// Exclude some RuntimeScene fields:
'_allInstancesList',
// Exclude circular references to parent runtimeGame or runtimeScene:
'_runtimeGame',
'_runtimeScene',
// Exclude some runtimeObject duplicated data:
'_behaviorsTable',
// Exclude some objects data:
'_animations',
'_animationFrame',
// Exclude linked objects to avoid too much repetitions:
'linkedObjectsManager',
// Could be improved by using private fields and excluding these (_)
// Exclude some behaviors data:
'_platformRBush',
// PlatformBehavior
'HSHG',
// Pathfinding
'_obstaclesHSHG',
// Pathfinding
'owner',
// Avoid circular reference from behavior to parent runtimeObject
// Exclude rendering related objects:
'_renderer',
'_imageManager',
// Exclude PIXI textures:
'baseTexture',
'_baseTexture',
'_invalidTexture',
];
const stringifiedMessage = circularSafeStringify(
message,
function (key, value) {
if (
excludedValues.indexOf(value) !== -1 ||
excludedKeys.indexOf(key) !== -1
) {
return '[Removed from the debugger]';
}
return value;
},
/* Limit maximum depth to prevent any crashes */
18
);
const serializationDuration = Date.now() - serializationStartTime;
logger.log(
'RuntimeGame serialization took ' + serializationDuration + 'ms'
);
if (serializationDuration > 500) {
logger.warn(
'Serialization took a long time: please check if there is a need to remove some objects from serialization'
);
}
this._sendMessage(stringifiedMessage);
}
/**
* Send logs from the hot reloader to the server.
* @param logs The hot reloader logs.
*/
sendHotReloaderLogs(logs: HotReloaderLog[]): void {
this._sendMessage(
circularSafeStringify({
command: 'hotReloader.logs',
payload: logs,
})
);
}
/**
* Callback called when profiling is starting.
*/
sendProfilerStarted(): void {
this._sendMessage(
circularSafeStringify({
command: 'profiler.started',
payload: null,
})
);
}
/**
* Callback called when profiling is ending.
*/
sendProfilerStopped(): void {
this._sendMessage(
circularSafeStringify({
command: 'profiler.stopped',
payload: null,
})
);
}
/**
* Send profiling results.
* @param framesAverageMeasures The measures made for each frames.
* @param stats Other measures done during the profiler run.
*/
sendProfilerOutput(
framesAverageMeasures: FrameMeasure,
stats: ProfilerStats
): void {
this._sendMessage(
circularSafeStringify({
command: 'profiler.output',
payload: {
framesAverageMeasures: framesAverageMeasures,
stats: stats,
},
})
);
}
}
}

View File

@@ -0,0 +1,75 @@
namespace gdjs {
const logger = new gdjs.Logger('Debugger client (websocket)');
/**
* This debugger client connects to a websocket server, exchanging
* and receiving messages with this server.
*/
export class WebsocketDebuggerClient extends gdjs.AbstractDebuggerClient {
_ws: WebSocket | null;
/**
* @param path - The path of the property to modify, starting from the RuntimeGame.
*/
constructor(runtimeGame: RuntimeGame) {
super(runtimeGame);
this._ws = null;
if (typeof WebSocket === 'undefined') {
logger.log("WebSocket is not defined, the debugger won't work.");
return;
}
const that = this;
try {
// Find the WebSocket server to connect to using the address that was stored
// in the options by the editor. If not, try the default address, though it's unlikely
// to work - which is ok, the game can run without a debugger server.
const runtimeGameOptions = runtimeGame.getAdditionalOptions();
const address =
(runtimeGameOptions &&
runtimeGameOptions.websocketDebuggerServerAddress) ||
'127.0.0.1';
const port =
(runtimeGameOptions &&
runtimeGameOptions.websocketDebuggerServerPort) ||
'3030';
this._ws = new WebSocket('ws://' + address + ':' + port + '/');
} catch (e) {
logger.log(
"WebSocket could not initialize, debugger/hot-reload won't work."
);
return;
}
this._ws.onopen = function open() {
logger.info('Debugger connection open');
};
this._ws.onclose = function close() {
logger.info('Debugger connection closed');
};
this._ws.onerror = function errored(error) {
logger.warn('Debugger client error:', error);
};
this._ws.onmessage = function incoming(message) {
let data: any = null;
try {
data = JSON.parse(message.data);
} catch (error) {
logger.info('Debugger received a badly formatted message:', error);
}
that.handleCommand(data);
};
}
protected _sendMessage(message: string) {
if (!this._ws) {
logger.warn('No connection to debugger opened to send a message.');
return;
}
if (this._ws.readyState === 1) this._ws.send(message);
}
}
//Register the class to let the engine use it.
// @ts-ignore
export const DebuggerClient = WebsocketDebuggerClient;
}

View File

@@ -0,0 +1,46 @@
namespace gdjs {
const logger = new gdjs.Logger('Debugger client (window message)');
/**
* This debugger client connects to the parent window, exchanging
* and receiving messages using `postMessage` and the `message` event listener.
*/
export class WindowMessageDebuggerClient extends gdjs.AbstractDebuggerClient {
_opener: Window | null = null;
/**
* @param path - The path of the property to modify, starting from the RuntimeGame.
*/
constructor(runtimeGame: RuntimeGame) {
super(runtimeGame);
this._opener = window.opener || null;
if (!this._opener) {
logger.info("`window.opener` not existing, the debugger won't work.");
return;
}
window.addEventListener('message', (event) => {
const data = event.data;
this.handleCommand(data);
});
}
protected _sendMessage(message: string) {
if (!this._opener) return;
try {
this._opener.postMessage(message, '*');
} catch (error) {
this._originalConsole.warn(
'Error while sending a message to the debugger:',
error
);
}
}
}
//Register the class to let the engine use it.
// @ts-ignore
export const DebuggerClient = WindowMessageDebuggerClient;
}

View File

@@ -15,12 +15,24 @@ namespace gdjs {
logger.error('Error while loading an audio file: ' + error),
};
/**
* Ensure the volume is between 0 and 1.
*/
const clampVolume = (volume: float): float => {
if (volume > 1.0) {
return 1.0;
}
if (volume < 0) {
return 0;
}
return volume;
};
/**
* A thin wrapper around a Howl object with:
* * Handling of callbacks when the sound is not yet loaded.
* * Automatic clamping when calling `setRate` to ensure a valid value is passed to Howler.js.
*
* See https://github.com/goldfire/howler.js#methods for the full documentation.
* * Automatic clamping when calling `setVolume` so that the volume is always between 0 and 1.
*
* @memberof gdjs
* @class HowlerSound
@@ -37,6 +49,25 @@ namespace gdjs {
*/
private _howl: Howl;
/**
* The volume at which the sound is being played.
* This value is clamped between 0 and 1.
*/
private _volume: float;
/**
* Whether the sound is being played in a loop or not.
*/
private _loop: boolean;
/**
* The rate (speed) the sound is being played at.
* This value is not clamped, though technically Howler.js will only
* accepts values between a specific range (so we clamp this when
* passing it to Howler.js, but keep the original value here).
*/
private _rate: float;
/**
* An array of callbacks to call once the sound starts to play.
*/
@@ -47,8 +78,11 @@ namespace gdjs {
*/
private _onPlay: Array<HowlCallback> = [];
constructor(howl: Howl) {
constructor(howl: Howl, volume: float, loop: boolean, rate: float) {
this._howl = howl;
this._volume = clampVolume(volume);
this._loop = loop;
this._rate = rate;
}
/**
@@ -69,6 +103,13 @@ namespace gdjs {
);
this._id = newID;
// Set the howl properties as soon as the sound is played and we have its ID.
this._howl.volume(this._volume, newID); // this._volume is already clamped between 0 and 1.
this._howl.loop(this._loop, newID);
// this._rate is not clamped, but we need to clamp it when passing it to Howler.js as it
// only supports a specific range.
this._howl.rate(gdjs.HowlerSoundManager.clampRate(this._rate), newID);
// Manually handle the play event before we have an ID.
// Before loading, howler won't register events as without an ID we cannot set a listener.
// Once we have an ID, we can transfer control of the events to howler.
@@ -106,6 +147,9 @@ namespace gdjs {
/**
* Check if the sound is currently playing.
* Note that a loading sound is considered as playing (as it will be
* played as soon as it's loaded). To avoid loading at runtime, prefer
* to preload the sounds.
*/
playing(): boolean {
return (
@@ -129,18 +173,25 @@ namespace gdjs {
}
/**
* Get the sound playback rate.
* Get the sound playback rate. This 1 for the default speed.
* This value is not clamped (any value greater than 0 is valid),
* but the underlying audio system might not play the sound at the required
* rate if it's very low or very high.
*/
getRate(): float {
if (this._id === null) return 0;
return this._howl.rate(this._id);
return this._rate;
}
/**
* Set the playback rate.
* This value is not clamped (any value greater than 0 is valid),
* but the underlying audio system might not play the sound at the required
* rate if it's very low or very high.
* @returns The current instance for chaining.
*/
setRate(rate: float): this {
this._rate = rate;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) {
rate = gdjs.HowlerSoundManager.clampRate(rate);
this._howl.rate(rate, this._id);
@@ -152,8 +203,7 @@ namespace gdjs {
* Get if the sound is looping.
*/
getLoop(): boolean {
if (this._id === null) return false;
return this._howl.loop(this._id);
return this._loop;
}
/**
@@ -161,6 +211,8 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setLoop(loop: boolean): this {
this._loop = loop;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.loop(loop, this._id);
return this;
}
@@ -171,17 +223,19 @@ namespace gdjs {
* @returns A float from 0 to 1.
*/
getVolume(): float {
if (this._id === null) return 100;
return this._howl.volume(this._id);
return this._volume;
}
/**
* Set the sound volume.
* @param volume A float from 0 to 1.
* @param volume A float from 0 to 1. The value is clamped if too high or too low.
* @returns The current instance for chaining.
*/
setVolume(volume: float): this {
if (this._id !== null) this._howl.volume(volume, this._id);
this._volume = clampVolume(volume);
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.volume(this._volume, this._id);
return this;
}
@@ -422,7 +476,7 @@ namespace gdjs {
sound: HowlerSound
): HowlerSound {
// Try to recycle an old sound.
for (var i = 0, len = arr.length; i < len; ++i) {
for (let i = 0, len = arr.length; i < len; ++i) {
if (arr[i] !== null && arr[i].stopped()) {
arr[i] = sound;
return sound;
@@ -437,8 +491,17 @@ namespace gdjs {
* Creates a new gdjs.HowlerSound using preloaded/cached Howl instances.
* @param soundName The name of the file or resource to play.
* @param isMusic True if a music, false if a sound.
* @param volume Between 0 and 1.
* @param loop True if it should be played looping.
* @param rate speed at which it is played.
*/
createHowlerSound(soundName: string, isMusic: boolean): HowlerSound {
createHowlerSound(
soundName: string,
isMusic: boolean,
volume: float,
loop: boolean,
rate: float
): HowlerSound {
const soundFile = this._getFileFromSoundName(soundName);
const cacheContainer = isMusic ? this._loadedMusics : this._loadedSounds;
@@ -448,12 +511,21 @@ namespace gdjs {
{
src: [soundFile],
html5: isMusic,
// Cache the sound with no volume. This avoids a bug where it plays at full volume
// for a split second before setting its correct volume.
volume: 0,
},
HowlParameters
)
);
}
return new gdjs.HowlerSound(cacheContainer[soundFile]);
return new gdjs.HowlerSound(
cacheContainer[soundFile],
volume,
loop,
rate
);
}
/**
@@ -473,6 +545,9 @@ namespace gdjs {
{
src: [soundFile],
html5: isMusic,
// Cache the sound with no volume. This avoids a bug where it plays at full volume
// for a split second before setting its correct volume.
volume: 0,
},
HowlParameters
)
@@ -536,19 +611,21 @@ namespace gdjs {
}
playSound(soundName: string, loop: boolean, volume: float, pitch: float) {
var sound = this.createHowlerSound(soundName, /* isMusic= */ false);
this._storeSoundInArray(this._freeSounds, sound).play();
const sound = this.createHowlerSound(
soundName,
/* isMusic= */ false,
volume / 100,
loop,
pitch
);
this._storeSoundInArray(this._freeSounds, sound);
sound.once('play', () => {
sound
.setLoop(loop)
.setVolume(volume / 100)
.setRate(pitch);
if (this._paused) {
sound.pause();
this._pausedSounds.push(sound);
}
});
sound.play();
}
playSoundOnChannel(
@@ -560,22 +637,21 @@ namespace gdjs {
) {
if (this._sounds[channel]) this._sounds[channel].stop();
var sound = this.createHowlerSound(
const sound = this.createHowlerSound(
soundName,
/* isMusic= */ false
).play();
/* isMusic= */ false,
volume / 100,
loop,
pitch
);
this._sounds[channel] = sound;
sound.once('play', () => {
sound
.setLoop(loop)
.setVolume(volume / 100)
.setRate(pitch);
if (this._paused) {
sound.pause();
this._pausedSounds.push(sound);
}
});
sound.play();
}
getSoundOnChannel(channel: integer): HowlerSound {
@@ -583,19 +659,21 @@ namespace gdjs {
}
playMusic(soundName: string, loop: boolean, volume: float, pitch: float) {
var music = this.createHowlerSound(soundName, /* isMusic= */ true);
this._storeSoundInArray(this._freeMusics, music).play();
const music = this.createHowlerSound(
soundName,
/* isMusic= */ true,
volume / 100,
loop,
pitch
);
this._storeSoundInArray(this._freeMusics, music);
music.once('play', () => {
music
.setLoop(loop)
.setVolume(volume / 100)
.setRate(pitch);
if (this._paused) {
music.pause();
this._pausedSounds.push(music);
}
});
music.play();
}
playMusicOnChannel(
@@ -609,20 +687,19 @@ namespace gdjs {
const music = this.createHowlerSound(
soundName,
/* isMusic= */ true
).play();
/* isMusic= */ true,
volume / 100,
loop,
pitch
);
this._musics[channel] = music;
music.once('play', () => {
music
.setLoop(loop)
.setVolume(volume / 100)
.setRate(pitch);
if (this._paused) {
music.pause();
this._pausedSounds.push(music);
}
});
music.play();
}
getMusicOnChannel(channel: integer): HowlerSound {
@@ -665,7 +742,7 @@ namespace gdjs {
// For one loaded file, it can have one or more resources
// that use it.
const files = {};
for (var i = 0, len = resources.length; i < len; ++i) {
for (let i = 0, len = resources.length; i < len; ++i) {
let res = resources[i];
if (res.file && res.kind === 'audio') {
if (!!this._availableResources[res.name]) {
@@ -706,6 +783,9 @@ namespace gdjs {
onload: onLoadCallback,
onloaderror: onLoadCallback,
html5: isMusic,
// Cache the sound with no volume. This avoids a bug where it plays at full volume
// for a split second before setting its correct volume.
volume: 0,
})
);
};

View File

@@ -29,6 +29,7 @@ namespace gdjs {
_resources: ResourceData[];
_loadedJsons: { [key: string]: Object } = {};
_callbacks: { [key: string]: Array<JsonManagerRequestCallback> } = {};
/**
* @param resources The resources data of the game.
@@ -113,28 +114,64 @@ namespace gdjs {
callback(null, this._loadedJsons[resourceName]);
return;
}
// Don't fetch again an object that is already being fetched.
{
const callbacks = this._callbacks[resourceName];
if (callbacks) {
callbacks.push(callback);
return;
} else {
this._callbacks[resourceName] = [callback];
}
}
const that = this;
const xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open('GET', resource.file);
xhr.onload = function () {
const callbacks = that._callbacks[resourceName];
if (!callbacks) {
return;
}
if (xhr.status !== 200) {
callback(
new Error('HTTP error: ' + xhr.status + '(' + xhr.statusText + ')'),
null
);
for (const callback of callbacks) {
callback(
new Error(
'HTTP error: ' + xhr.status + '(' + xhr.statusText + ')'
),
null
);
}
delete that._callbacks[resourceName];
return;
}
// Cache the result
that._loadedJsons[resourceName] = xhr.response;
callback(null, xhr.response);
for (const callback of callbacks) {
callback(null, xhr.response);
}
delete that._callbacks[resourceName];
};
xhr.onerror = function () {
callback(new Error('Network error'), null);
const callbacks = that._callbacks[resourceName];
if (!callbacks) {
return;
}
for (const callback of callbacks) {
callback(new Error('Network error'), null);
}
delete that._callbacks[resourceName];
};
xhr.onabort = function () {
callback(new Error('Request aborted'), null);
const callbacks = that._callbacks[resourceName];
if (!callbacks) {
return;
}
for (const callback of callbacks) {
callback(new Error('Request aborted'), null);
}
delete that._callbacks[resourceName];
};
xhr.send();
}

View File

@@ -1,23 +1,58 @@
namespace gdjs {
const _console = {
/**
* A LoggerOutput specifies a single method to be called to display
* or register a log.
*/
export interface LoggerOutput {
log(
group: string,
message: string,
type: 'info' | 'warning' | 'error',
internal?: boolean
): void;
}
const supportedConsoleFunctions = {
info: console.log,
warning: console.warn,
error: console.error,
};
/**
* Internal method for logging messages to the JS console or the Debugger if available.
* Should be used in engine code or extensions, console.log is fine for JS events.
* The default logging output: uses the JavaScript console.
*/
export let log = (
group: string,
message: string,
type: 'info' | 'warning' | 'error' = 'info',
internal = true
): void => {
const logger = _console[type] || _console.info;
logger(`[${group}] ${message}`);
};
class ConsoleLoggerOutput implements LoggerOutput {
private readonly discardedConsoleGroups = new Set<string>();
discardGroup(groupName: string) {
this.discardedConsoleGroups.add(groupName);
}
enableGroup(groupName: string) {
this.discardedConsoleGroups.delete(groupName);
}
log(
group: string,
message: string,
type: 'info' | 'warning' | 'error' = 'info',
internal = true
): void {
if (this.discardedConsoleGroups.has(group)) return;
const logger =
supportedConsoleFunctions[type] || supportedConsoleFunctions.info;
logger(`[${group}] ${message}`);
}
}
const consoleLoggerOutput = new ConsoleLoggerOutput();
/**
* The current output method - can be changed at runtime.
* By default, output to the JavaScript console.
*/
let loggerOutput: LoggerOutput = consoleLoggerOutput;
function objectsToString(objects: any[]): string {
return objects.reduce(
@@ -27,28 +62,56 @@ namespace gdjs {
}
/**
* A Console API like class for logging using GDevelop's logger.
* A Console API like class for logging in a GDevelop game.
*/
export class Logger {
private readonly group: string;
/**
* Create a new logger with the given group name.
* You can then use log, info, warn and error on this object.
*/
constructor(group: string) {
this.group = group;
}
log(...messages: any[]): void {
this.info(...messages);
loggerOutput.log(this.group, objectsToString(messages), 'info');
}
info(...messages: any[]): void {
log(this.group, objectsToString(messages), 'info');
loggerOutput.log(this.group, objectsToString(messages), 'info');
}
warn(...messages: any[]): void {
log(this.group, objectsToString(messages), 'warning');
loggerOutput.log(this.group, objectsToString(messages), 'warning');
}
error(...messages: any[]): void {
log(this.group, objectsToString(messages), 'error');
loggerOutput.log(this.group, objectsToString(messages), 'error');
}
/**
* Give access to the console output used by default by the logger.
* This can be useful to restore the default log method if you overrode it
* or to disable some logging in the console.
*/
static getDefaultConsoleLoggerOutput() {
return consoleLoggerOutput;
}
/**
* Return the current logger output (common to all gdjs.Logger instances).
*/
static getLoggerOutput(): LoggerOutput {
return loggerOutput;
}
/**
* Change the logger output (common to all gdjs.Logger instances).
*/
static setLoggerOutput(newLoggerOutput: LoggerOutput) {
loggerOutput = newLoggerOutput;
}
}
}

View File

@@ -8,11 +8,11 @@ namespace gdjs {
* called. This is used to avoid scrolling in a webpage when these keys are pressed
* in the game.
*/
const defaultPreventedKeys = [
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
const defaultPreventedKeyCodes = [
37, // ArrowLeft
38, // ArrowUp
39, // ArrowRight
40, // ArrowDown
];
/**
@@ -431,14 +431,14 @@ namespace gdjs {
//Keyboard
document.onkeydown = function (e) {
if (defaultPreventedKeys.includes(e.keyCode)) {
if (defaultPreventedKeyCodes.includes(e.keyCode)) {
e.preventDefault();
}
manager.onKeyPressed(e.keyCode, e.location);
};
document.onkeyup = function (e) {
if (defaultPreventedKeys.includes(e.keyCode)) {
if (defaultPreventedKeyCodes.includes(e.keyCode)) {
e.preventDefault();
}

View File

@@ -27,9 +27,9 @@ namespace gdjs {
/** if true, export is a partial preview without events. */
projectDataOnlyExport?: boolean;
/** The address of the debugger server, to reach out using WebSocket. */
debuggerServerAddress?: string;
websocketDebuggerServerAddress?: string;
/** The port of the debugger server, to reach out using WebSocket. */
debuggerServerPort?: string;
websocketDebuggerServerPort?: string;
};
/**
@@ -71,9 +71,9 @@ namespace gdjs {
_options: RuntimeGameOptions;
/**
* Optional client to connect to a debugger
* Optional client to connect to a debugger server.
*/
_debuggerClient: gdjs.IDebuggerClient | null;
_debuggerClient: gdjs.AbstractDebuggerClient | null;
_sessionMetricsInitialized: boolean = false;
_disableMetrics: boolean = false;
_isPreview: boolean;

View File

@@ -27,6 +27,112 @@ namespace gdjs {
return Math.pow(radiusX, 2) + Math.pow(radiusY, 2);
};
/**
* Arrays and data structure that are (re)used by
* {@link RuntimeObject.separateFromObjects} to avoid any allocation.
*/
const separateFromObjectsStatics: {
moveXArray: Array<float>;
moveYArray: Array<float>;
} = {
moveXArray: [],
moveYArray: [],
};
/**
* Move the object using the results from collisionTest call.
* This moves the object according to the direction of the longest vector,
* and projects the others on the orthogonal vector.
*
* See {@link RuntimeObject.separateFromObjects}
*
* @param object The object to move.
* @param moveXArray The X coordinates of the vectors to move the object.
* @param moveYArray The Y coordinates of the vectors to move the object.
* @return true if the object was moved.
*/
const moveFollowingSeparatingVectors = (
object: gdjs.RuntimeObject,
moveXArray: Array<float>,
moveYArray: Array<float>
): boolean => {
if (moveXArray.length === 0) {
moveXArray.length = 0;
moveYArray.length = 0;
return false;
}
if (moveXArray.length === 1) {
// Move according to the results returned by the collision algorithm.
object.setPosition(
object.getX() + moveXArray[0],
object.getY() + moveYArray[0]
);
moveXArray.length = 0;
moveYArray.length = 0;
return true;
}
// Find the longest vector
let squaredDistanceMax = 0;
let distanceMaxIndex = 0;
for (let index = 0; index < moveXArray.length; index++) {
const moveX = moveXArray[index];
const moveY = moveYArray[index];
const squaredDistance = moveX * moveX + moveY * moveY;
if (squaredDistance > squaredDistanceMax) {
squaredDistanceMax = squaredDistance;
distanceMaxIndex = index;
}
}
const distanceMax = Math.sqrt(squaredDistanceMax);
// unit vector of the longest vector
const uX = moveXArray[distanceMaxIndex] / distanceMax;
const uY = moveYArray[distanceMaxIndex] / distanceMax;
// normal vector of the longest vector
const vX = -uY;
const vY = uX;
// Project other vectors on the normal
let scalarProductMin = 0;
let scalarProductMax = 0;
for (let index = 0; index < moveXArray.length; index++) {
const moveX = moveXArray[index];
const moveY = moveYArray[index];
const scalarProduct = moveX * vX + moveY * vY;
scalarProductMin = Math.min(scalarProductMin, scalarProduct);
scalarProductMax = Math.max(scalarProductMax, scalarProduct);
}
// Apply the longest vector
let deltaX = moveXArray[distanceMaxIndex];
let deltaY = moveYArray[distanceMaxIndex];
// Apply the longest projected vector if they all are in the same direction
// Some projections could have rounding errors,
// they are considered negligible under a 1 for 1,000,000 ratio.
const scalarProductMinIsNegligible =
-scalarProductMin < scalarProductMax / 1048576;
const scalarProductMaxIsNegligible =
scalarProductMax < -scalarProductMin / 1048576;
if (scalarProductMinIsNegligible !== scalarProductMaxIsNegligible) {
if (scalarProductMaxIsNegligible) {
deltaX += scalarProductMin * vX;
deltaY += scalarProductMin * vY;
} else {
deltaX += scalarProductMax * vX;
deltaY += scalarProductMax * vY;
}
}
object.setPosition(object.getX() + deltaX, object.getY() + deltaY);
moveXArray.length = 0;
moveYArray.length = 0;
return true;
};
/**
* RuntimeObject represents an object being used on a RuntimeScene.
*
@@ -996,7 +1102,12 @@ namespace gdjs {
* @return the X position of the object center, relative to `getDrawableX()`.
*/
getCenterX(): float {
return this.getWidth() / 2;
const halfWidth = this.getWidth() / 2;
const halfHeight = this.getHeight() / 2;
return (
Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight) *
Math.cos(gdjs.toRad(this.getAngle()))
);
}
/**
@@ -1006,7 +1117,12 @@ namespace gdjs {
* @return the Y position of the object center, relative to `getDrawableY()`.
*/
getCenterY(): float {
return this.getHeight() / 2;
const halfWidth = this.getWidth() / 2;
const halfHeight = this.getHeight() / 2;
return (
Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight) *
Math.sin(gdjs.toRad(this.getAngle()))
);
}
/**
@@ -1288,10 +1404,13 @@ namespace gdjs {
this.hitBoxes[0].vertices[3][0] = 0 - centerX;
this.hitBoxes[0].vertices[3][1] = height - centerY;
}
this.hitBoxes[0].rotate(gdjs.toRad(this.getAngle()));
const angle = gdjs.toRad(this.getAngle());
this.hitBoxes[0].rotate(angle);
const xDelta = centerX * Math.cos(angle) - centerY * Math.sin(angle);
const yDelta = centerY * Math.cos(angle) + centerX * Math.sin(angle);
this.hitBoxes[0].move(
this.getDrawableX() + centerX,
this.getDrawableY() + centerY
this.getDrawableX() + xDelta,
this.getDrawableY() + yDelta
);
}
@@ -1373,6 +1492,54 @@ namespace gdjs {
}
}
/**
* Shortcut for `getAABB().min[0]`.
* See {@link getAABB}.
*/
getAABBLeft(): float {
return this.getAABB().min[0];
}
/**
* Shortcut for `getAABB().min[1]`.
* See {@link getAABB}.
*/
getAABBTop(): float {
return this.getAABB().min[1];
}
/**
* Shortcut for `getAABB().max[0]`.
* See {@link getAABB}.
*/
getAABBRight(): float {
return this.getAABB().max[0];
}
/**
* Shortcut for `getAABB().max[1]`.
* See {@link getAABB}.
*/
getAABBBottom(): float {
return this.getAABB().max[1];
}
/**
* Shortcut for getting the center on the X coordinates of the object AABB.
* See {@link getAABB}.
*/
getAABBCenterX(): float {
return this.getAABB().min[0] / 2 + this.getAABB().max[0] / 2;
}
/**
* Shortcut for getting the center on the Y coordinates of the object AABB.
* See {@link getAABB}.
*/
getAABBCenterY(): float {
return this.getAABB().min[1] / 2 + this.getAABB().max[1] / 2;
}
//Behaviors:
/**
* Call each behavior stepPreEvents method.
@@ -1599,35 +1766,34 @@ namespace gdjs {
objects: RuntimeObject[],
ignoreTouchingEdges: boolean
): boolean {
let moved = false;
let xMove = 0;
let yMove = 0;
let moveXArray: Array<float> = separateFromObjectsStatics.moveXArray;
let moveYArray: Array<float> = separateFromObjectsStatics.moveYArray;
moveXArray.length = 0;
moveYArray.length = 0;
const hitBoxes = this.getHitBoxes();
//Check if their is a collision with each object
for (let i = 0, len = objects.length; i < len; ++i) {
if (objects[i].id != this.id) {
const otherHitBoxes = objects[i].getHitBoxes();
for (let k = 0, lenk = hitBoxes.length; k < lenk; ++k) {
for (let l = 0, lenl = otherHitBoxes.length; l < lenl; ++l) {
const result = gdjs.Polygon.collisionTest(
hitBoxes[k],
otherHitBoxes[l],
ignoreTouchingEdges
);
if (result.collision) {
xMove += result.move_axis[0];
yMove += result.move_axis[1];
moved = true;
}
// Check if their is a collision with each object
for (const otherObject of objects) {
if (otherObject.id === this.id) {
continue;
}
const otherHitBoxes = otherObject.getHitBoxes();
for (const hitBox of hitBoxes) {
for (const otherHitBox of otherHitBoxes) {
const result = gdjs.Polygon.collisionTest(
hitBox,
otherHitBox,
ignoreTouchingEdges
);
if (result.collision) {
moveXArray.push(result.move_axis[0]);
moveYArray.push(result.move_axis[1]);
}
}
}
}
//Move according to the results returned by the collision algorithm.
this.setPosition(this.getX() + xMove, this.getY() + yMove);
return moved;
return moveFollowingSeparatingVectors(this, moveXArray, moveYArray);
}
/**
@@ -1640,40 +1806,40 @@ namespace gdjs {
objectsLists: ObjectsLists,
ignoreTouchingEdges: boolean
): boolean {
let moved = false;
let xMove = 0;
let yMove = 0;
let moveXArray: Array<float> = separateFromObjectsStatics.moveXArray;
let moveYArray: Array<float> = separateFromObjectsStatics.moveYArray;
moveXArray.length = 0;
moveYArray.length = 0;
const hitBoxes = this.getHitBoxes();
for (const name in objectsLists.items) {
if (objectsLists.items.hasOwnProperty(name)) {
const objects = objectsLists.items[name];
const otherObjects = objectsLists.items[name];
//Check if their is a collision with each object
for (let i = 0, len = objects.length; i < len; ++i) {
if (objects[i].id != this.id) {
const otherHitBoxes = objects[i].getHitBoxes();
for (let k = 0, lenk = hitBoxes.length; k < lenk; ++k) {
for (let l = 0, lenl = otherHitBoxes.length; l < lenl; ++l) {
const result = gdjs.Polygon.collisionTest(
hitBoxes[k],
otherHitBoxes[l],
ignoreTouchingEdges
);
if (result.collision) {
xMove += result.move_axis[0];
yMove += result.move_axis[1];
moved = true;
}
// Check if their is a collision with each object
for (const otherObject of otherObjects) {
if (otherObject.id === this.id) {
continue;
}
const otherHitBoxes = otherObject.getHitBoxes();
for (const hitBox of hitBoxes) {
for (const otherHitBox of otherHitBoxes) {
const result = gdjs.Polygon.collisionTest(
hitBox,
otherHitBox,
ignoreTouchingEdges
);
if (result.collision) {
moveXArray.push(result.move_axis[0]);
moveYArray.push(result.move_axis[1]);
}
}
}
}
}
}
//Move according to the results returned by the collision algorithm.
this.setPosition(this.getX() + xMove, this.getY() + yMove);
return moved;
return moveFollowingSeparatingVectors(this, moveXArray, moveYArray);
}
/**

View File

@@ -4,7 +4,8 @@
* This project is released under the MIT License.
*/
namespace gdjs {
const logger = new gdjs.Logger('Scene manager');
const logger = new gdjs.Logger('RuntimeScene');
const setupWarningLogger = new gdjs.Logger('RuntimeScene (setup warnings)');
/**
* A scene being played, containing instances of objects rendered on screen.
@@ -437,7 +438,7 @@ namespace gdjs {
if (module && module.func) {
this._eventsFunction = module.func;
} else {
logger.warn(
setupWarningLogger.warn(
'No function found for running logic of scene ' + this._name
);
this._eventsFunction = function () {};

View File

@@ -1,517 +0,0 @@
namespace gdjs {
const logger = new gdjs.Logger('Debugger client');
/**
* An client side implementation of the Debugger
*/
export interface IDebuggerClient {
/**
* Logs a value in the debugger console.
* @param message - The value to log inside the console.
* @param additionalData - Additional data about the log.
*/
log(
group: string,
message: string,
type: 'info' | 'warning' | 'error',
internal: boolean
): void;
/**
* Update a value, specified by a path starting from the {@link RuntimeGame} instance.
* @param path - The path to the variable, starting from {@link RuntimeGame}.
* @param newValue - The new value.
* @return Was the operation successful?
*/
set(path: string[], newValue: any): boolean;
/**
* Call a method, specified by a path starting from the {@link RuntimeGame} instance.
* @param path - The path to the method, starting from {@link RuntimeGame}.
* @param args - The arguments to pass the method.
* @return Was the operation successful?
*/
call(path: string[], args: any[]): boolean;
/**
* Dump all the relevant data from the {@link RuntimeGame} instance and send it to the server.
*/
sendRuntimeGameDump(): void;
/**
* Send logs from the hot reloader to the server.
* @param logs The hot reloader logs.
*/
sendHotReloaderLogs(logs: HotReloaderLog[]): void;
/**
* Callback called when profiling is starting.
*/
sendProfilerStarted(): void;
/**
* Callback called when profiling is ending.
*/
sendProfilerStopped(): void;
/**
* Send profiling results.
* @param framesAverageMeasures The measures made for each frames.
* @param stats Other measures done during the profiler run.
*/
sendProfilerOutput(
framesAverageMeasures: FrameMeasure,
stats: ProfilerStats
): void;
}
/**
* A function used to replace circular references with a new value.
* @param key - The key corresponding to the value.
* @param value - The value.
* @returns The new value.
*/
type DebuggerClientCycleReplacer = (key: string, value: any) => any;
/**
* This {@link IDebuggerClient} connects to a websocket server, can dump
* the data of the current game, and receive message to change a field or
* call a function, specified by a path from the {@link RuntimeGame}.
*
* @param runtimeGame - The `gdjs.RuntimeGame` to be debugged
*/
export class WebsocketDebuggerClient implements IDebuggerClient {
_runtimegame: gdjs.RuntimeGame;
_hotReloader: gdjs.HotReloader;
_ws: WebSocket | null;
/**
* @param path - The path of the property to modify, starting from the RuntimeGame.
*/
constructor(runtimeGame: RuntimeGame) {
this._runtimegame = runtimeGame;
this._hotReloader = new gdjs.HotReloader(runtimeGame);
this._ws = null;
if (typeof WebSocket === 'undefined') {
logger.log("WebSocket is not defined, debugger won't work");
return;
}
const that = this;
let ws = null;
try {
// Find the WebSocket server to connect to using the address that was stored
// in the options by the editor. If not, try the default address, though it's unlikely
// to work - which is ok, the game can run without a debugger server.
const runtimeGameOptions = this._runtimegame.getAdditionalOptions();
const address =
(runtimeGameOptions && runtimeGameOptions.debuggerServerAddress) ||
'127.0.0.1';
const port =
(runtimeGameOptions && runtimeGameOptions.debuggerServerPort) ||
'3030';
this._ws = new WebSocket('ws://' + address + ':' + port + '/');
} catch (e) {
logger.log(
"WebSocket could not initialize, debugger/hot-reload won't work (might be because of preview inside web browser)."
);
return;
}
this._ws.onopen = function open() {
logger.info('Debugger connection open');
};
this._ws.onclose = function close() {
logger.info('Debugger connection closed');
};
this._ws.onerror = function errored(error) {
logger.warn('Debugger client error:', error);
};
this._ws.onmessage = function incoming(message) {
let data: any = null;
try {
data = JSON.parse(message.data);
} catch (e) {
logger.info('Debugger received a badly formatted message');
}
if (data && data.command) {
if (data.command === 'play') {
runtimeGame.pause(false);
} else if (data.command === 'pause') {
runtimeGame.pause(true);
that.sendRuntimeGameDump();
} else if (data.command === 'refresh') {
that.sendRuntimeGameDump();
} else if (data.command === 'set') {
that.set(data.path, data.newValue);
} else if (data.command === 'call') {
that.call(data.path, data.args);
} else if (data.command === 'profiler.start') {
runtimeGame.startCurrentSceneProfiler(function (stoppedProfiler) {
that.sendProfilerOutput(
stoppedProfiler.getFramesAverageMeasures(),
stoppedProfiler.getStats()
);
that.sendProfilerStopped();
});
that.sendProfilerStarted();
} else if (data.command === 'profiler.stop') {
runtimeGame.stopCurrentSceneProfiler();
} else if (data.command === 'hotReload') {
that._hotReloader.hotReload().then((logs) => {
that.sendHotReloaderLogs(logs);
});
} else {
logger.info(
'Unknown command "' + data.command + '" received by the debugger.'
);
}
} else {
logger.info('Debugger received a message with badly formatted data.');
}
};
((log, info, debug, warn, error, gdjsLog) => {
// Hook the console logging functions to log to the Debugger as well
console.log = (...messages) => {
log(...messages);
this._consoleLogHook('info', ...messages);
};
console.debug = (...messages) => {
debug(...messages);
this._consoleLogHook('info', ...messages);
};
console.info = (...messages) => {
info(...messages);
this._consoleLogHook('info', ...messages);
};
console.warn = (...messages) => {
warn(...messages);
this._consoleLogHook('warning', ...messages);
};
console.error = (...messages) => {
error(...messages);
this._consoleLogHook('error', ...messages);
};
gdjs.log = (
group: string,
message: string,
type: 'info' | 'warning' | 'error' = 'info',
internal = true
) => {
gdjsLog(group, message, type);
this.log(group, message, type, internal);
};
})(
console.log,
console.info,
console.debug,
console.warn,
console.error,
gdjs.log
);
}
_consoleLogHook(type: 'info' | 'warning' | 'error', ...messages) {
this.log(
'JavaScript',
messages.reduce(
(accumulator, value) => accumulator + value.toString(),
''
),
type,
false
);
}
log(
group: string,
message: string,
type: 'info' | 'warning' | 'error',
internal: boolean
) {
if (!this._ws) {
logger.warn('No connection to debugger opened to send logs');
return;
}
if (this._ws.readyState === 1)
this._ws.send(
JSON.stringify({
command: 'console.log',
payload: {
message,
type,
group,
internal,
timestamp: performance.now(),
},
})
);
}
set(path: string[], newValue: any): boolean {
if (!path || !path.length) {
logger.warn('No path specified, set operation from debugger aborted');
return false;
}
let object = this._runtimegame;
let currentIndex = 0;
while (currentIndex < path.length - 1) {
const key = path[currentIndex];
if (!object || !object[key]) {
logger.error('Incorrect path specified. No ' + key + ' in ', object);
return false;
}
object = object[key];
currentIndex++;
}
// Ensure the newValue is properly typed to avoid breaking anything in
// the game engine.
const currentValue = object[path[currentIndex]];
if (typeof currentValue === 'number') {
newValue = parseFloat(newValue);
} else {
if (typeof currentValue === 'string') {
newValue = '' + newValue;
}
}
logger.log('Updating', path, 'to', newValue);
object[path[currentIndex]] = newValue;
return true;
}
call(path: string[], args: any[]): boolean {
if (!path || !path.length) {
logger.warn('No path specified, call operation from debugger aborted');
return false;
}
let object = this._runtimegame;
let currentIndex = 0;
while (currentIndex < path.length - 1) {
const key = path[currentIndex];
if (!object || !object[key]) {
logger.error('Incorrect path specified. No ' + key + ' in ', object);
return false;
}
object = object[key];
currentIndex++;
}
if (!object[path[currentIndex]]) {
logger.error('Unable to call', path);
return false;
}
logger.log('Calling', path, 'with', args);
object[path[currentIndex]].apply(object, args);
return true;
}
sendRuntimeGameDump(): void {
if (!this._ws) {
logger.warn(
'No connection to debugger opened to send RuntimeGame dump'
);
return;
}
const that = this;
const message = { command: 'dump', payload: this._runtimegame };
const serializationStartTime = Date.now();
// Stringify the message, excluding some known data that are big and/or not
// useful for the debugger.
const excludedValues = [that._runtimegame.getGameData()];
const excludedKeys = [
// Exclude reference to the debugger
'_debuggerClient',
// Exclude some RuntimeScene fields:
'_allInstancesList',
// Exclude circular references to parent runtimeGame or runtimeScene:
'_runtimeGame',
'_runtimeScene',
// Exclude some runtimeObject duplicated data:
'_behaviorsTable',
// Exclude some objects data:
'_animations',
'_animationFrame',
// Exclude linked objects to avoid too much repetitions:
'linkedObjectsManager',
// Could be improved by using private fields and excluding these (_)
// Exclude some behaviors data:
'_platformRBush',
// PlatformBehavior
'HSHG',
// Pathfinding
'_obstaclesHSHG',
// Pathfinding
'owner',
// Avoid circular reference from behavior to parent runtimeObject
// Exclude rendering related objects:
'_renderer',
'_imageManager',
// Exclude PIXI textures:
'baseTexture',
'_baseTexture',
'_invalidTexture',
];
const stringifiedMessage = this._circularSafeStringify(
message,
function (key, value) {
if (
excludedValues.indexOf(value) !== -1 ||
excludedKeys.indexOf(key) !== -1
) {
return '[Removed from the debugger]';
}
return value;
},
/* Limit maximum depth to prevent any crashes */
18
);
const serializationDuration = Date.now() - serializationStartTime;
logger.log(
'RuntimeGame serialization took ' + serializationDuration + 'ms'
);
if (serializationDuration > 500) {
logger.warn(
'Serialization took a long time: please check if there is a need to remove some objects from serialization'
);
}
this._ws.send(stringifiedMessage);
}
sendHotReloaderLogs(logs: HotReloaderLog[]): void {
if (!this._ws) {
logger.warn('No connection to debugger opened');
return;
}
this._ws.send(
this._circularSafeStringify({
command: 'hotReloader.logs',
payload: logs,
})
);
}
sendProfilerStarted(): void {
if (!this._ws) {
logger.warn('No connection to debugger opened');
return;
}
this._ws.send(
this._circularSafeStringify({
command: 'profiler.started',
payload: null,
})
);
}
sendProfilerStopped(): void {
if (!this._ws) {
logger.warn('No connection to debugger opened');
return;
}
this._ws.send(
this._circularSafeStringify({
command: 'profiler.stopped',
payload: null,
})
);
}
sendProfilerOutput(
framesAverageMeasures: FrameMeasure,
stats: ProfilerStats
): void {
if (!this._ws) {
logger.warn(
'No connection to debugger opened to send profiler measures'
);
return;
}
this._ws.send(
this._circularSafeStringify({
command: 'profiler.output',
payload: {
framesAverageMeasures: framesAverageMeasures,
stats: stats,
},
})
);
}
/**
* This is an alternative to JSON.stringify that ensure that circular references
* are replaced by a placeholder.
* @param obj - The object to serialize.
* @param [replacer] - A function called for each property on the object or array being stringified, with the property key and its value, and that returns the new value. If not specified, values are not altered.
* @param [maxDepth] - The maximum depth, after which values are replaced by a string ("[Max depth reached]"). If not specified, there is no maximum depth.
* @param [spaces] - The number of spaces for indentation.
* @param [cycleReplacer] - Function used to replace circular references with a new value.
*/
_circularSafeStringify(
obj: any,
replacer?: DebuggerClientCycleReplacer,
maxDepth?: number,
spaces?: number,
cycleReplacer?: DebuggerClientCycleReplacer
) {
return JSON.stringify(
obj,
this._depthLimitedSerializer(replacer, cycleReplacer, maxDepth),
spaces
);
}
/**
* Generates a JSON serializer that prevent circular references and stop if maxDepth is reached.
* @param [replacer] - A function called for each property on the object or array being stringified, with the property key and its value, and that returns the new value. If not specified, values are not altered.
* @param [cycleReplacer] - Function used to replace circular references with a new value.
* @param [maxDepth] - The maximum depth, after which values are replaced by a string ("[Max depth reached]"). If not specified, there is no maximum depth.
*/
_depthLimitedSerializer(
replacer?: DebuggerClientCycleReplacer,
cycleReplacer?: DebuggerClientCycleReplacer,
maxDepth?: number
): DebuggerClientCycleReplacer {
const stack: Array<string> = [],
keys: Array<string> = [];
if (cycleReplacer === undefined || cycleReplacer === null) {
cycleReplacer = function (key, value) {
if (stack[0] === value) {
return '[Circular ~]';
}
return (
'[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'
);
};
}
return function (key: string, value: any): any {
if (stack.length > 0) {
const thisPos = stack.indexOf(this);
~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
if (maxDepth != null && thisPos > maxDepth) {
return '[Max depth reached]';
} else {
if (~stack.indexOf(value)) {
value = (cycleReplacer as DebuggerClientCycleReplacer).call(
this,
key,
value
);
}
}
} else {
stack.push(value);
}
return replacer == null ? value : replacer.call(this, key, value);
};
}
}
//Register the class to let the engine use it.
export const DebuggerClient = WebsocketDebuggerClient;
}

146
GDJS/package-lock.json generated
View File

@@ -1216,10 +1216,148 @@
}
},
"esbuild": {
"version": "0.8.26",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.8.26.tgz",
"integrity": "sha512-u3MMHOOumdWoAKF+073GHPpzvVB2cM+y9VD4ZwYs1FAQ6atRPISya35dbrbOu/mM68mQ42P+nwPzQVBTfQhkvQ==",
"dev": true
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.12.tgz",
"integrity": "sha512-vTKKUt+yoz61U/BbrnmlG9XIjwpdIxmHB8DlPR0AAW6OdS+nBQBci6LUHU2q9WbBobMEIQxxDpKbkmOGYvxsow==",
"dev": true,
"requires": {
"esbuild-android-arm64": "0.13.12",
"esbuild-darwin-64": "0.13.12",
"esbuild-darwin-arm64": "0.13.12",
"esbuild-freebsd-64": "0.13.12",
"esbuild-freebsd-arm64": "0.13.12",
"esbuild-linux-32": "0.13.12",
"esbuild-linux-64": "0.13.12",
"esbuild-linux-arm": "0.13.12",
"esbuild-linux-arm64": "0.13.12",
"esbuild-linux-mips64le": "0.13.12",
"esbuild-linux-ppc64le": "0.13.12",
"esbuild-netbsd-64": "0.13.12",
"esbuild-openbsd-64": "0.13.12",
"esbuild-sunos-64": "0.13.12",
"esbuild-windows-32": "0.13.12",
"esbuild-windows-64": "0.13.12",
"esbuild-windows-arm64": "0.13.12"
}
},
"esbuild-android-arm64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.12.tgz",
"integrity": "sha512-TSVZVrb4EIXz6KaYjXfTzPyyRpXV5zgYIADXtQsIenjZ78myvDGaPi11o4ZSaHIwFHsuwkB6ne5SZRBwAQ7maw==",
"dev": true,
"optional": true
},
"esbuild-darwin-64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.12.tgz",
"integrity": "sha512-c51C+N+UHySoV2lgfWSwwmlnLnL0JWj/LzuZt9Ltk9ub1s2Y8cr6SQV5W3mqVH1egUceew6KZ8GyI4nwu+fhsw==",
"dev": true,
"optional": true
},
"esbuild-darwin-arm64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.12.tgz",
"integrity": "sha512-JvAMtshP45Hd8A8wOzjkY1xAnTKTYuP/QUaKp5eUQGX+76GIie3fCdUUr2ZEKdvpSImNqxiZSIMziEiGB5oUmQ==",
"dev": true,
"optional": true
},
"esbuild-freebsd-64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.12.tgz",
"integrity": "sha512-r6On/Skv9f0ZjTu6PW5o7pdXr8aOgtFOEURJZYf1XAJs0IQ+gW+o1DzXjVkIoT+n1cm3N/t1KRJfX71MPg/ZUA==",
"dev": true,
"optional": true
},
"esbuild-freebsd-arm64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.12.tgz",
"integrity": "sha512-F6LmI2Q1gii073kmBE3NOTt/6zLL5zvZsxNLF8PMAwdHc+iBhD1vzfI8uQZMJA1IgXa3ocr3L3DJH9fLGXy6Yw==",
"dev": true,
"optional": true
},
"esbuild-linux-32": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.12.tgz",
"integrity": "sha512-U1UZwG3UIwF7/V4tCVAo/nkBV9ag5KJiJTt+gaCmLVWH3bPLX7y+fNlhIWZy8raTMnXhMKfaTvWZ9TtmXzvkuQ==",
"dev": true,
"optional": true
},
"esbuild-linux-64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.12.tgz",
"integrity": "sha512-YpXSwtu2NxN3N4ifJxEdsgd6Q5d8LYqskrAwjmoCT6yQnEHJSF5uWcxv783HWN7lnGpJi9KUtDvYsnMdyGw71Q==",
"dev": true,
"optional": true
},
"esbuild-linux-arm": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.12.tgz",
"integrity": "sha512-SyiT/JKxU6J+DY2qUiSLZJqCAftIt3uoGejZ0HDnUM2MGJqEGSGh7p1ecVL2gna3PxS4P+j6WAehCwgkBPXNIw==",
"dev": true,
"optional": true
},
"esbuild-linux-arm64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.12.tgz",
"integrity": "sha512-sgDNb8kb3BVodtAlcFGgwk+43KFCYjnFOaOfJibXnnIojNWuJHpL6aQJ4mumzNWw8Rt1xEtDQyuGK9f+Y24jGA==",
"dev": true,
"optional": true
},
"esbuild-linux-mips64le": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.12.tgz",
"integrity": "sha512-qQJHlZBG+QwVIA8AbTEtbvF084QgDi4DaUsUnA+EolY1bxrG+UyOuGflM2ZritGhfS/k7THFjJbjH2wIeoKA2g==",
"dev": true,
"optional": true
},
"esbuild-linux-ppc64le": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.12.tgz",
"integrity": "sha512-2dSnm1ldL7Lppwlo04CGQUpwNn5hGqXI38OzaoPOkRsBRWFBozyGxTFSee/zHFS+Pdh3b28JJbRK3owrrRgWNw==",
"dev": true,
"optional": true
},
"esbuild-netbsd-64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.12.tgz",
"integrity": "sha512-D4raxr02dcRiQNbxOLzpqBzcJNFAdsDNxjUbKkDMZBkL54Z0vZh4LRndycdZAMcIdizC/l/Yp/ZsBdAFxc5nbA==",
"dev": true,
"optional": true
},
"esbuild-openbsd-64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.12.tgz",
"integrity": "sha512-KuLCmYMb2kh05QuPJ+va60bKIH5wHL8ypDkmpy47lzwmdxNsuySeCMHuTv5o2Af1RUn5KLO5ZxaZeq4GEY7DaQ==",
"dev": true,
"optional": true
},
"esbuild-sunos-64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.12.tgz",
"integrity": "sha512-jBsF+e0woK3miKI8ufGWKG3o3rY9DpHvCVRn5eburMIIE+2c+y3IZ1srsthKyKI6kkXLvV4Cf/E7w56kLipMXw==",
"dev": true,
"optional": true
},
"esbuild-windows-32": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.12.tgz",
"integrity": "sha512-L9m4lLFQrFeR7F+eLZXG82SbXZfUhyfu6CexZEil6vm+lc7GDCE0Q8DiNutkpzjv1+RAbIGVva9muItQ7HVTkQ==",
"dev": true,
"optional": true
},
"esbuild-windows-64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.12.tgz",
"integrity": "sha512-k4tX4uJlSbSkfs78W5d9+I9gpd+7N95W7H2bgOMFPsYREVJs31+Q2gLLHlsnlY95zBoPQMIzHooUIsixQIBjaQ==",
"dev": true,
"optional": true
},
"esbuild-windows-arm64": {
"version": "0.13.12",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.12.tgz",
"integrity": "sha512-2tTv/BpYRIvuwHpp2M960nG7uvL+d78LFW/ikPItO+2GfK51CswIKSetSpDii+cjz8e9iSPgs+BU4o8nWICBwQ==",
"dev": true,
"optional": true
},
"escape-string-regexp": {
"version": "2.0.0",

View File

@@ -11,7 +11,7 @@
"@types/mocha": "^5.2.7",
"@types/node": "^14.11.1",
"better-docs": "^2.3.2",
"esbuild": "^0.8.26",
"esbuild": "^0.13.12",
"jsdoc": "^3.6.6",
"jsdoc-autoprivate": "0.0.1",
"jsdoc-fresh": "^1.0.5",

View File

@@ -1,4 +1,4 @@
const esbuild = require('esbuild');
const { build } = require('esbuild');
const path = require('path');
const shell = require('shelljs');
const {
@@ -26,8 +26,6 @@ if (!args.out) {
shell.mkdir('-p', bundledOutPath);
(async () => {
const esbuildService = await esbuild.startService();
// Generate the output file paths
const {
allGDJSInOutFilePaths,
@@ -51,21 +49,19 @@ shell.mkdir('-p', bundledOutPath);
return;
}
return esbuildService
.build({
sourcemap: true,
entryPoints: [inPath],
outfile: renameBuiltFile(outPath),
})
.catch(() => {
// Error is already logged by esbuild.
errored = true;
});
return build({
sourcemap: true,
entryPoints: [inPath],
minify: true,
outfile: renameBuiltFile(outPath),
}).catch(() => {
// Error is already logged by esbuild.
errored = true;
});
}
)
);
esbuildService.stop();
const buildDuration = Date.now() - startTime;
if (!errored) shell.echo(`✅ GDJS built in ${buildDuration}ms`);
if (errored) shell.exit(1);

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -10,6 +10,13 @@ module.exports = function (config) {
config.set({
frameworks: ['mocha'],
browserNoActivityTimeout: 400000,
browsers: ['ChromeHeadless', 'EdgeHeadless', 'Chrome', 'Edge', 'Firefox'],
plugins: [
require('karma-chrome-launcher'),
require('@chiragrupani/karma-chromium-edge-launcher'),
require('karma-firefox-launcher'),
require('karma-mocha'),
],
client: {
mocha: {
reporter: 'html',
@@ -59,7 +66,7 @@ module.exports = function (config) {
'../../newIDE/app/resources/GDJS/Runtime/events-tools/storagetools.js',
'../../newIDE/app/resources/GDJS/Runtime/events-tools/stringtools.js',
'../../newIDE/app/resources/GDJS/Runtime/events-tools/windowtools.js',
'../../newIDE/app/resources/GDJS/Runtime/websocket-debugger-client/hot-reloader.js',
'../../newIDE/app/resources/GDJS/Runtime/debugger-client/hot-reloader.js',
//Extensions:
'../../newIDE/app/resources/GDJS/Runtime/Extensions/DraggableBehavior/draggableruntimebehavior.js',
@@ -83,7 +90,8 @@ module.exports = function (config) {
// Test extensions:
'./tests/Extensions/**.js',
//All tests files:
// Other test initialization files:
'./tests-utils/init.js',
'./tests-utils/init.pixiruntimegamewithassets.js',
// Assets

View File

@@ -4,6 +4,12 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@chiragrupani/karma-chromium-edge-launcher": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@chiragrupani/karma-chromium-edge-launcher/-/karma-chromium-edge-launcher-2.1.1.tgz",
"integrity": "sha512-QK6M+1CYMbndWRaEAOpMW9kwF+JKcqyaEY2lqe8Nnm1hyOzSZaxTqXnLTZpk7RqQHhWCOduSU/XDvceWs4H74g==",
"dev": true
},
"accepts": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",

View File

@@ -10,7 +10,9 @@
"test-benchmark": "karma start --browsers ChromeHeadless --single-run --enableBenchmarks",
"test-benchmark:watch": "karma start --browsers ChromeHeadless --enableBenchmarks",
"test:firefox": "karma start --browsers Firefox --single-run",
"test:firefox:watch": "karma start --browsers Firefox"
"test:firefox:watch": "karma start --browsers Firefox",
"test:edge": "karma start --browsers EdgeHeadless --single-run",
"test:edge:watch": "karma start --browsers EdgeHeadless"
},
"keywords": [
"HTML5",
@@ -28,6 +30,7 @@
"devDependencies": {
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"@chiragrupani/karma-chromium-edge-launcher": "2.1.1",
"karma-firefox-launcher": "^1.1.0",
"karma-mocha": "^1.3.0"
}

View File

@@ -0,0 +1,6 @@
// @ts-check
// This file is called before all tests (but after GDJS is loaded).
// Disable a few logs that are too verbose:
gdjs.Logger.getDefaultConsoleLoggerOutput().discardGroup('Firebase (setup)');
gdjs.Logger.getDefaultConsoleLoggerOutput().discardGroup('RuntimeScene (setup warnings)');

View File

@@ -54,6 +54,14 @@
max: [10, 20],
});
// Verify the shortcut functions too:
expect(object.getAABBLeft()).to.be(0);
expect(object.getAABBTop()).to.be(0);
expect(object.getAABBRight()).to.be(10);
expect(object.getAABBBottom()).to.be(20);
expect(object.getAABBCenterX()).to.be(5);
expect(object.getAABBCenterY()).to.be(10);
object.setPosition(15, 20);
expect(object.getAABB()).to.eql({
min: [15, 20],
@@ -79,6 +87,14 @@
min: [-5, 20],
max: [15, 30],
});
// Verify the shortcut functions again too:
expect(object.getAABBLeft()).to.be(-5);
expect(object.getAABBTop()).to.be(20);
expect(object.getAABBRight()).to.be(15);
expect(object.getAABBBottom()).to.be(30);
expect(object.getAABBCenterX()).to.be(5);
expect(object.getAABBCenterY()).to.be(25);
});
it('handles collision', () => {

View File

@@ -0,0 +1,117 @@
// @ts-check
/**
* Common tests for gdjs game engine.
* See README.md for more information.
*/
describe('gdjs.RuntimeObject.separateFromObjects', () => {
const runtimeGame = new gdjs.RuntimeGame({
variables: [],
// @ts-ignore TODO: make a function to create an empty game and use it across tests.
properties: { windowWidth: 800, windowHeight: 600 },
resources: { resources: [] },
});
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
const object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
variables: [],
behaviors: [],
effects: [],
});
object.setCustomWidthAndHeight(100, 100);
object.setCustomCenter(0, 0);
const obstacle1 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
variables: [],
behaviors: [],
effects: [],
});
obstacle1.setCustomWidthAndHeight(100, 100);
obstacle1.setCustomCenter(0, 0);
const obstacle2 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
variables: [],
behaviors: [],
effects: [],
});
obstacle2.setCustomWidthAndHeight(100, 100);
obstacle2.setCustomCenter(0, 0);
it('can be separated from 2 aligned objects', () => {
object.setPosition(200, 300);
// 2 obstacles on the left
obstacle1.setPosition(290, 250);
obstacle2.setPosition(290, 350);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be(190);
expect(object.getY()).to.be(300);
});
it('can be separated from 2 not exactly aligned objects', () => {
object.setPosition(200, 300);
// 2 obstacles on the left
obstacle1.setPosition(290, 250);
obstacle2.setPosition(295, 350);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be(190);
expect(object.getY()).to.be(300);
});
it('can be separated from 2 objects that form a corner', () => {
object.setPosition(200, 300);
// 1 obstacle on the top
obstacle1.setPosition(250, 220);
// 1 obstacle on the left
obstacle2.setPosition(290, 250);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be(190);
expect(object.getY()).to.be(320);
});
it('can be separated from 2 rotated objects', () => {
object.setPosition(200, 300);
// 1 obstacle on the top left corner
obstacle1.setPosition(250, 280);
obstacle1.setAngle(-45);
// 1 obstacle on the bottom left corner
obstacle2.setPosition(250, 420);
obstacle2.setAngle(-45);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be(170);
expect(object.getY()).to.be(300);
});
it('can be separated from 2 aligned objects when everything is rotated', () => {
object.setPosition(240, 350);
object.setAngle(-45);
// 2 obstacles on the top left
obstacle1.setPosition(250, 250);
obstacle1.setAngle(-45);
obstacle2.setPosition(330, 330);
obstacle1.setAngle(-45);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be.within(224, 225);
expect(object.getY()).to.be.within(365, 366);
});
});

View File

@@ -2462,6 +2462,9 @@ interface SpriteObject {
boolean HasNoAnimations();
void SwapAnimations(unsigned long first, unsigned long second);
void MoveAnimation(unsigned long oldIndex, unsigned long newIndex);
void SetUpdateIfNotVisible(boolean updateIfNotVisible);
boolean GetUpdateIfNotVisible();
};
SpriteObject implements gdObject;
@@ -2706,7 +2709,8 @@ interface EventsFunctionsExtensionCodeGenerator {
[Prefix="gdjs::"]
interface PreviewExportOptions {
void PreviewExportOptions([Ref] Project project, [Const] DOMString outputPath);
[Ref] PreviewExportOptions SetDebuggerServerAddress([Const] DOMString address, [Const] DOMString port);
[Ref] PreviewExportOptions UseWebsocketDebuggerClientWithServerAddress([Const] DOMString address, [Const] DOMString port);
[Ref] PreviewExportOptions UseWindowMessageDebuggerClient();
[Ref] PreviewExportOptions SetLayoutName([Const] DOMString layoutName);
[Ref] PreviewExportOptions SetExternalLayoutName([Const] DOMString externalLayoutName);
[Ref] PreviewExportOptions SetIncludeFileHash([Const] DOMString includeFile, long hash);

View File

@@ -1,7 +1,8 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdPreviewExportOptions {
constructor(project: gdProject, outputPath: string): void;
setDebuggerServerAddress(address: string, port: string): gdPreviewExportOptions;
useWebsocketDebuggerClientWithServerAddress(address: string, port: string): gdPreviewExportOptions;
useWindowMessageDebuggerClient(): gdPreviewExportOptions;
setLayoutName(layoutName: string): gdPreviewExportOptions;
setExternalLayoutName(externalLayoutName: string): gdPreviewExportOptions;
setIncludeFileHash(includeFile: string, hash: number): gdPreviewExportOptions;

View File

@@ -9,6 +9,8 @@ declare class gdSpriteObject extends gdObject {
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
setUpdateIfNotVisible(updateIfNotVisible: boolean): void;
getUpdateIfNotVisible(): boolean;
delete(): void;
ptr: number;
};

View File

@@ -1,9 +1,12 @@
module.exports = {
stories: ['../src/stories/index.js', '../src/stories/**/*.stories.js'],
addons: [{
name: '@storybook/addon-essentials',
options: {
docs: false,
}
}],
addons: [
{
name: '@storybook/addon-essentials',
options: {
docs: false,
},
},
'storybook-addon-mock/register',
],
};

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@
"devDependencies": {
"@lingui/cli": "^2.7.3",
"@lingui/macro": "^2.7.3",
"@storybook/addon-essentials": "6.3.8",
"@storybook/react": "6.3.8",
"@storybook/addon-essentials": "6.3.12",
"@storybook/react": "6.3.12",
"axios-mock-adapter": "^1.19.0",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "8.0.6",
@@ -23,6 +23,7 @@
"recursive-copy": "^2.0.10",
"recursive-readdir": "^2.2.2",
"shelljs": "0.8.4",
"storybook-addon-mock": "^1.7.0",
"style-dictionary": "^2.10.2",
"typescript": "^4.1.3",
"unzipper": "^0.9.11",

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24" fill="#222"><path d="M5,15H3V17H5M3,21H21V19H3M5,11H3V13H5M19,9H21V7H19M19,5H21V3H19M5,7H3V9H5M19,17H21V15H19M19,13H21V11H19M17,3H15V5H17M13,3H11V5H13M5,3H3V5H5M9,3H7V5H9" /></svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24" fill="#222"><path d="M19,17H21V15H19M19,21H21V19H19M13,3H11V11H3V13H11V21H13V13H21V11H13M15,21H17V19H15M19,5H21V3H19M19,9H21V7H19M17,3H15V5H17M5,3H3V5H5M9,3H7V5H9M3,17H5V15H3M5,7H3V9H5M7,21H9V19H7M3,21H5V19H3V21Z" /></svg>

After

Width:  |  Height:  |  Size: 498 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24" fill="#222"><path d="M15,5H17V3H15M19,21H21V19H19M19,13H21V11H19M19,5H21V3H19M19,17H21V15H19M15,21H17V19H15M19,9H21V7H19M3,21H5V3H3M7,5H9V3H7M7,21H9V19H7M11,5H13V3H11M11,21H13V19H11V21Z" /></svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24" fill="#222"><path d="M11,5H13V3H11M15,5H17V3H15M15,21H17V19H15M19,21H21V3H19M3,9H5V7H3M3,17H5V15H3M3,13H5V11H3M11,21H13V19H11M3,21H5V19H3M7,5H9V3H7M3,5H5V3H3M7,21H9V19H7V21Z" /></svg>

After

Width:  |  Height:  |  Size: 459 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24" fill="#222"><path d="M19,21H21V19H19M15,21H17V19H15M19,17H21V15H19M3,5H21V3H3M19,13H21V11H19M19,9H21V7H19M3,9H5V7H3M3,13H5V11H3M3,21H5V19H3M3,17H5V15H3M11,21H13V19H11M7,21H9V19H7V21Z" /></svg>

After

Width:  |  Height:  |  Size: 468 B

View File

@@ -11,6 +11,7 @@
"folderProject": false,
"orientation": "landscape",
"packageName": "com.example.platformer",
"pixelsRounding": false,
"projectUuid": "c82d449a-9975-4c9d-a003-8683951a8a09",
"scaleMode": "linear",
"sizeOnStartupMode": "adaptWidth",
@@ -26,77 +27,23 @@
"verticalSync": false,
"platformSpecificAssets": {},
"loadingScreen": {
"showGDevelopSplash": true
"backgroundColor": 0,
"backgroundFadeInDuration": 0.2,
"backgroundImageResourceName": "",
"gdevelopLogoStyle": "light",
"logoAndProgressFadeInDuration": 0.2,
"logoAndProgressLogoFadeInDelay": 0.2,
"minDuration": 1.5,
"progressBarColor": 16777215,
"progressBarHeight": 20,
"progressBarMaxWidth": 200,
"progressBarMinWidth": 40,
"progressBarWidthPercent": 30,
"showGDevelopSplash": true,
"showProgressBar": true
},
"authorIds": [],
"extensionProperties": [],
"extensions": [
{
"name": "BuiltinObject"
},
{
"name": "BuiltinAudio"
},
{
"name": "BuiltinVariables"
},
{
"name": "BuiltinTime"
},
{
"name": "BuiltinMouse"
},
{
"name": "BuiltinKeyboard"
},
{
"name": "BuiltinJoystick"
},
{
"name": "BuiltinCamera"
},
{
"name": "BuiltinWindow"
},
{
"name": "BuiltinFile"
},
{
"name": "BuiltinNetwork"
},
{
"name": "BuiltinScene"
},
{
"name": "BuiltinAdvanced"
},
{
"name": "Sprite"
},
{
"name": "BuiltinCommonInstructions"
},
{
"name": "BuiltinCommonConversions"
},
{
"name": "BuiltinStringInstructions"
},
{
"name": "BuiltinMathematicalTools"
},
{
"name": "BuiltinExternalLayouts"
},
{
"name": "TiledSpriteObject"
},
{
"name": "PlatformBehavior"
},
{
"name": "TextObject"
}
],
"platforms": [
{
"name": "GDevelop JS platform"
@@ -506,17 +453,16 @@
"v": 244,
"uiSettings": {
"grid": false,
"gridB": 255,
"gridG": 180,
"gridType": "rectangular",
"gridWidth": 70,
"gridHeight": 70,
"gridOffsetX": 0,
"gridOffsetY": 0,
"gridR": 158,
"gridType": "rectangular",
"gridWidth": 70,
"gridColor": 10401023,
"gridAlpha": 0.8,
"snap": true,
"windowMask": true,
"zoomFactor": 0.4606
"zoomFactor": 0.4606,
"windowMask": true
},
"objectsGroups": [
{
@@ -1946,6 +1892,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -2244,6 +2191,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Platform",
@@ -2358,6 +2306,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Platform",
@@ -2426,6 +2375,7 @@
"type": "TiledSpriteObject::TiledSprite",
"width": 70,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Platform",
@@ -2444,6 +2394,7 @@
"type": "TiledSpriteObject::TiledSprite",
"width": 70,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Platform",
@@ -2460,6 +2411,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Platform",
@@ -2526,6 +2478,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -2584,6 +2537,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -2644,6 +2598,7 @@
"type": "TiledSpriteObject::TiledSprite",
"width": 70,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Platform",
@@ -2660,9 +2615,11 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "PlatformerObject",
"slopeMaxAngle": 60,
"type": "PlatformBehavior::PlatformerObjectBehavior",
"jumpSpeed": 800,
"jumpSustainTime": 0.4,
@@ -2674,7 +2631,6 @@
"maxFallingSpeed": 1000,
"maxSpeed": 250,
"roundCoordinates": true,
"slopeMaxAngle": 0,
"xGrabTolerance": 10,
"yGrabOffset": 0
}
@@ -2742,6 +2698,7 @@
"value": "0"
}
],
"effects": [],
"behaviors": [
{
"name": "PlatformerObject",
@@ -2905,6 +2862,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "PlatformerObject",
@@ -3068,6 +3026,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -3188,6 +3147,7 @@
"type": "TextObject::Text",
"underlined": false,
"variables": [],
"effects": [],
"behaviors": [],
"string": "x 0",
"font": "Bimbo_JVE.ttf",
@@ -3204,6 +3164,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Tween",
@@ -3267,6 +3228,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -3325,6 +3287,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -3383,6 +3346,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -3441,6 +3405,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Anchor",
@@ -3509,6 +3474,7 @@
"type": "Sprite",
"updateIfNotVisible": true,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -3569,6 +3535,7 @@
"type": "TiledSpriteObject::TiledSprite",
"width": 128,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "HorizontalTiledSpriteParallax",
@@ -3582,6 +3549,7 @@
"type": "Sprite",
"updateIfNotVisible": false,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -3621,6 +3589,7 @@
"type": "Sprite",
"updateIfNotVisible": false,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -3660,6 +3629,7 @@
"type": "Sprite",
"updateIfNotVisible": false,
"variables": [],
"effects": [],
"behaviors": [],
"animations": [
{
@@ -3701,6 +3671,7 @@
"type": "TiledSpriteObject::TiledSprite",
"width": 32,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Tween",
@@ -5610,6 +5581,7 @@
"parallax",
"tiled"
],
"authorIds": [],
"dependencies": [],
"eventsFunctions": [],
"eventsBasedBehaviors": [

View File

@@ -117,7 +117,7 @@ const enrichCommits = rawCommits => {
lowerCaseMessage.includes("don't show in the changelog") ||
lowerCaseMessage.includes("do not show in changelog") ||
lowerCaseMessage.includes("do not show in the changelog") ||
lowerCaseMessage === 'update translations' ||
lowerCaseMessage.startsWith('update translations') ||
lowerCaseMessage === 'prettier' ||
lowerCaseMessage === 'update jsextension.js' ||
lowerCaseMessage.includes('run code formatting') ||

View File

@@ -15,7 +15,8 @@ const {
} = require('./lib/WikiHelpLink');
const { convertMarkdownToDokuWikiMarkdown } = require('./lib/DokuwikiHelpers');
const extensionsUrl = 'https://api.gdevelop-app.com/asset/extension';
const extensionShortHeadersUrl =
'https://api.gdevelop-app.com/asset/extension-short-header';
const gdRootPath = path.join(__dirname, '..', '..', '..');
const outputRootPath = path.join(gdRootPath, 'docs-wiki');
const extensionsRootPath = path.join(outputRootPath, 'extensions');
@@ -38,19 +39,22 @@ const generateExtensionFooterText = fullName => {
);
};
const getAllExtensions = async () => {
const response = await axios.get(extensionsUrl);
if (!response.data.databaseUrl) {
/**
* Return the list of all extensions and their associated short headers
* (useful as containing author public profiles information).
*/
const getAllExtensionAndExtensionShortHeaders = async () => {
const response = await axios.get(extensionShortHeadersUrl);
const extensionShortHeaders = response.data;
if (!extensionShortHeaders.length) {
throw new Error('Unexpected response from the extension endpoint.');
}
const databaseResponse = await axios.get(response.data.databaseUrl);
const extensionsDatabase = databaseResponse.data;
const extensions = await Promise.all(
extensionsDatabase.extensionShortHeaders.map(async extensionShortHeader => {
extensionShortHeaders.map(async extensionShortHeader => {
const response = await axios.get(extensionShortHeader.url);
const extensionHeader = response.data;
if (!extensionHeader) {
const extension = response.data;
if (!extension) {
throw new Error(
`Unexpected response when fetching an extension (${
extensionShortHeader.url
@@ -58,7 +62,7 @@ const getAllExtensions = async () => {
);
}
return extensionHeader;
return { extensionShortHeader, extension };
})
);
@@ -68,7 +72,7 @@ const getAllExtensions = async () => {
(async () => {
try {
console.info(` Loading all community extensions...`);
const extensions = await getAllExtensions();
const extensionsAndExtensionShortHeaders = await getAllExtensionAndExtensionShortHeaders();
let indexPageContent = `# Extensions
@@ -78,10 +82,16 @@ GDevelop is built in a flexible way. In addition to [[gdevelop5:all-features|cor
`;
for (const extension of extensions) {
for (const {
extension,
extensionShortHeader,
} of extensionsAndExtensionShortHeaders) {
const folderName = getExtensionFolderName(extension.name);
const referencePageUrl = `${gdevelopWikiUrlRoot}/extensions/${folderName}/reference`;
const helpPageUrl = getHelpLink(extension.helpPath) || referencePageUrl;
const authorUsernames = (extensionShortHeader.authors || [])
.map(author => author.username || null)
.filter(Boolean);
const referencePageContent =
`# ${extension.fullName}` +
@@ -91,7 +101,7 @@ GDevelop is built in a flexible way. In addition to [[gdevelop5:all-features|cor
`${extension.shortDescription}\n` +
'\n' +
`**Authors and contributors** to this community extension: ${
extension.author
authorUsernames.length ? authorUsernames.join(', ') : 'not specified'
}.\n` +
'\n' +
'---\n' +

View File

@@ -19,7 +19,7 @@ import PlaceholderError from '../UI/PlaceholderError';
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../ResourcesList/ResourceSource.flow';
} from '../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
import Add from '@material-ui/icons/Add';
import { ResponsiveLineStackLayout } from '../UI/Layout';

View File

@@ -121,8 +121,7 @@ export function ExampleDialog({
<AlertMessage kind="error">
<Trans>
Unfortunately, this example requires a newer version of GDevelop
to work. Upgrade GDevelop to be able to use this extension in your
project.
to work. Update GDevelop to be able to open this example.
</Trans>
</AlertMessage>
)}

View File

@@ -128,7 +128,7 @@ export default class ExtensionInstallDialog extends Component<Props, State> {
<AlertMessage kind="error">
<Trans>
Unfortunately, this extension requires a newer version of
GDevelop to work. Upgrade GDevelop to be able to use this
GDevelop to work. Update GDevelop to be able to use this
extension in your project.
</Trans>
</AlertMessage>

View File

@@ -11,6 +11,12 @@ import HelpButton from '../../UI/HelpButton';
import { importExtension, installExtension } from './InstallExtension';
import InfoBar from '../../UI/Messages/InfoBar';
import { type ExtensionShortHeader } from '../../Utils/GDevelopServices/Extension';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import {
ACHIEVEMENT_FEATURE_FLAG,
addCreateBadgePreHookIfNotClaimed,
TRIVIAL_FIRST_EXTENSION,
} from '../../Utils/GDevelopServices/Badge';
type Props = {|
project: gdProject,
@@ -33,6 +39,16 @@ export default function ExtensionsSearchDialog({
const eventsFunctionsExtensionsState = React.useContext(
EventsFunctionsExtensionsContext
);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const installDisplayedExtension = ACHIEVEMENT_FEATURE_FLAG
? addCreateBadgePreHookIfNotClaimed(
authenticatedUser,
TRIVIAL_FIRST_EXTENSION,
installExtension
)
: installExtension;
const eventsFunctionsExtensionOpener = eventsFunctionsExtensionsState.getEventsFunctionsExtensionOpener();
return (
@@ -83,7 +99,7 @@ export default function ExtensionsSearchDialog({
onInstall={async extensionShortHeader => {
setIsInstalling(true);
onInstallExtension(extensionShortHeader);
const wasExtensionInstalled = await installExtension(
const wasExtensionInstalled = await installDisplayedExtension(
i18n,
project,
eventsFunctionsExtensionsState,

View File

@@ -25,22 +25,17 @@ import { AssetDetails } from './AssetDetails';
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../ResourcesList/ResourceSource.flow';
} from '../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
import {
sendAssetAddedToProject,
sendAssetOpened,
sendNewObjectCreated,
} from '../Utils/Analytics/EventSender';
import optionalRequire from '../Utils/OptionalRequire';
import { showErrorBox } from '../UI/Messages/MessageBox';
import { useScreenType } from '../UI/Reponsive/ScreenTypeMeasurer';
import Window from '../Utils/Window';
import { useResourceFetcher } from '../ProjectsStorage/ResourceFetcher';
import RaisedButton from '../UI/RaisedButton';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import ScrollView from '../UI/ScrollView';
const electron = optionalRequire('electron');
const ObjectListItem = ({
objectMetadata,
@@ -114,7 +109,6 @@ export default function NewObjectDialog({
project,
]);
const experimentalObjectsInformation = getExperimentalObjects();
const screenType = useScreenType();
const resourcesFetcher = useResourceFetcher();
React.useEffect(() => setNewObjectDialogDefaultTab(currentTab), [
@@ -189,18 +183,7 @@ export default function NewObjectDialog({
return (
<Dialog
title={<Trans>Add a new object</Trans>}
secondaryActions={[
<HelpButton helpPagePath="/objects" key="help" />,
!electron && screenType !== 'touch' ? (
<FlatButton
key="download-gdevelop"
label={<Trans>Download GDevelop to import your images</Trans>}
onClick={() =>
Window.openExternalURL('https://gdevelop-app.com/download')
}
/>
) : null,
]}
secondaryActions={[<HelpButton helpPagePath="/objects" key="help" />]}
actions={[
<FlatButton
key="close"
@@ -214,6 +197,7 @@ export default function NewObjectDialog({
open
flexBody
noMargin
fullHeight
>
<Column noMargin expand>
<Tabs value={currentTab} onChange={setCurrentTab}>
@@ -286,17 +270,6 @@ export default function NewObjectDialog({
/>
)}
</Line>
<Line justifyContent="center" alignItems="center">
<RaisedButton
label={
<Trans>Browse ready made objects in the Asset Store</Trans>
}
primary
onClick={() => {
setCurrentTab('asset-store');
}}
/>
</Line>
</ScrollView>
)}
</Column>

View File

@@ -2,7 +2,7 @@
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../../ResourcesList/ResourceSource.flow';
} from '../../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../../ResourcesList/ResourceExternalEditor.flow';
/**

View File

@@ -18,9 +18,8 @@ import RaisedButton from '../UI/RaisedButton';
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../ResourcesList/ResourceSource.flow';
} from '../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
import { getBehaviorTutorialHints } from '../Hints';
import DismissableTutorialMessage from '../Hints/DismissableTutorialMessage';
import { ColumnStackLayout } from '../UI/Layout';
import useForceUpdate from '../Utils/UseForceUpdate';
@@ -29,6 +28,13 @@ import EmptyBehaviorsPlaceholder from './EmptyBehaviorsPlaceholder';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import ScrollView from '../UI/ScrollView';
import { IconContainer } from '../UI/IconContainer';
import {
ACHIEVEMENT_FEATURE_FLAG,
addCreateBadgePreHookIfNotClaimed,
TRIVIAL_FIRST_BEHAVIOR,
} from '../Utils/GDevelopServices/Badge';
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import { getBehaviorTutorialIds } from '../Utils/GDevelopServices/Tutorial';
const gd: libGDevelop = global.gd;
@@ -46,6 +52,7 @@ const BehaviorsEditor = (props: Props) => {
const [newBehaviorDialogOpen, setNewBehaviorDialogOpen] = React.useState(
false
);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const { object, project } = props;
const allBehaviorNames = object.getAllBehaviorNames().toJSArray();
@@ -60,7 +67,7 @@ const BehaviorsEditor = (props: Props) => {
.filter(behaviorType => behaviorType === type).length;
};
const addBehavior = (type: string, defaultName: string) => {
const _addBehavior = (type: string, defaultName: string) => {
setNewBehaviorDialogOpen(false);
if (hasBehaviorWithType(type)) {
@@ -86,6 +93,14 @@ const BehaviorsEditor = (props: Props) => {
props.onUpdateBehaviorsSharedData();
};
const addBehavior = ACHIEVEMENT_FEATURE_FLAG
? addCreateBadgePreHookIfNotClaimed(
authenticatedUser,
TRIVIAL_FIRST_BEHAVIOR,
_addBehavior
)
: _addBehavior;
const onChangeBehaviorName = (
behaviorContent: gdBehaviorContent,
newName: string
@@ -179,9 +194,9 @@ const BehaviorsEditor = (props: Props) => {
const BehaviorComponent = BehaviorsEditorService.getEditor(
behaviorTypeName
);
const tutorialHints = getBehaviorTutorialHints(behaviorTypeName);
const enabledTutorialHints = tutorialHints.filter(
hint => !values.hiddenTutorialHints[hint.identifier]
const tutorialIds = getBehaviorTutorialIds(behaviorTypeName);
const enabledTutorialIds = tutorialIds.filter(
tutorialId => !values.hiddenTutorialHints[tutorialId]
);
const iconUrl = behaviorMetadata.getIconFilename();
@@ -233,13 +248,13 @@ const BehaviorsEditor = (props: Props) => {
// Avoid Physics2 behavior overflow on small screens
noOverflowParent
>
{enabledTutorialHints.length ? (
{enabledTutorialIds.length ? (
<Line>
<ColumnStackLayout expand>
{tutorialHints.map(tutorialHint => (
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
key={tutorialId}
tutorialId={tutorialId}
/>
))}
</ColumnStackLayout>

View File

@@ -1,7 +1,7 @@
// @flow
import { mapFor } from '../Utils/MapFor';
import { type Schema } from '../PropertiesEditor';
import { type ResourceKind } from '../ResourcesList/ResourceSource.flow';
import { type ResourceKind } from '../ResourcesList/ResourceSource';
import flatten from 'lodash/flatten';
export type EnumeratedEffectMetadata = {|

View File

@@ -30,10 +30,16 @@ import {
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../ResourcesList/ResourceSource.flow';
} from '../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
import ScrollView from '../UI/ScrollView';
import { EmptyEffectsPlaceholder } from './EmptyEffectsPlaceholder';
import {
ACHIEVEMENT_FEATURE_FLAG,
addCreateBadgePreHookIfNotClaimed,
TRIVIAL_FIRST_EFFECT,
} from '../Utils/GDevelopServices/Badge';
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
type Props = {|
project: gdProject,
@@ -63,6 +69,7 @@ export default function EffectsList(props: Props) {
const { effectsContainer, onEffectsUpdated } = props;
const preferences = React.useContext(PreferencesContext);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const showEffectParameterNames = preferences.values.showEffectParameterNames;
const setShowEffectParameterNames = preferences.setShowEffectParameterNames;
@@ -73,7 +80,7 @@ export default function EffectsList(props: Props) {
const forceUpdate = useForceUpdate();
const addEffect = () => {
const _addEffect = () => {
const newName = newNameGenerator('Effect', name =>
effectsContainer.hasEffectNamed(name)
);
@@ -86,6 +93,14 @@ export default function EffectsList(props: Props) {
onEffectsUpdated();
};
const addEffect = ACHIEVEMENT_FEATURE_FLAG
? addCreateBadgePreHookIfNotClaimed(
authenticatedUser,
TRIVIAL_FIRST_EFFECT,
_addEffect
)
: _addEffect;
const removeEffect = (name: string) => {
effectsContainer.removeEffect(name);
forceUpdate();

View File

@@ -5,7 +5,7 @@ import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import EventsSheet from '../EventsSheet';
import EventsSheet, { type EventsSheetInterface } from '../EventsSheet';
import EditorMosaic from '../UI/EditorMosaic';
import EmptyMessage from '../UI/EmptyMessage';
import EventsFunctionConfigurationEditor from './EventsFunctionConfigurationEditor';
@@ -20,7 +20,7 @@ import EventsBasedBehaviorEditorDialog from '../EventsBasedBehaviorEditor/Events
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../ResourcesList/ResourceSource.flow';
} from '../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
import BehaviorMethodSelectorDialog from './BehaviorMethodSelectorDialog';
import ExtensionFunctionSelectorDialog from './ExtensionFunctionSelectorDialog';
@@ -107,7 +107,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
extensionFunctionSelectorDialogOpen: false,
onAddEventsFunctionCb: null,
};
editor: ?EventsSheet;
editor: ?EventsSheetInterface;
_editorMosaic: ?EditorMosaic;
_editorNavigator: ?EditorNavigatorInterface;
_globalObjectsContainer: ?gdObjectsContainer;
@@ -709,6 +709,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
project={project}
scope={{
layout: null,
externalEvents: null,
eventsFunctionsExtension,
eventsBasedBehavior: selectedEventsBasedBehavior,
eventsFunction: selectedEventsFunction,

View File

@@ -8,21 +8,31 @@ import SemiControlledAutoComplete, {
type DataSource,
} from '../../../../UI/SemiControlledAutoComplete';
const getList = (project: ?gdProject): DataSource => {
if (!project) {
const getList = (
currentSceneName: ?string,
currentExternalEventName: ?string,
project: ?gdProject
): DataSource => {
if (!project || !currentSceneName) {
return [];
}
const externalEvents = enumerateExternalEvents(project).map(
externalEvents => ({
const externalEvents = enumerateExternalEvents(project)
.filter(
externalEvents => externalEvents.getName() !== currentExternalEventName
)
.map(externalEvents => ({
text: externalEvents.getName(),
value: externalEvents.getName(),
})
);
const layouts = enumerateLayouts(project).map(layout => ({
text: layout.getName(),
value: layout.getName(),
}));
}));
const layouts = enumerateLayouts(project)
.filter(layout => layout.getName() !== currentSceneName)
.map(layout => ({
text: layout.getName(),
value: layout.getName(),
}));
return [...externalEvents, { type: 'separator' }, ...layouts];
};
@@ -33,6 +43,8 @@ type Props = {|
isInline?: boolean,
onRequestClose?: () => void,
onApply?: () => void,
sceneName?: string,
externalEventsName?: string,
|};
export default class ExternalEventsAutoComplete extends React.Component<
@@ -53,6 +65,8 @@ export default class ExternalEventsAutoComplete extends React.Component<
onApply,
isInline,
project,
sceneName,
externalEventsName,
} = this.props;
return (
@@ -64,7 +78,7 @@ export default class ExternalEventsAutoComplete extends React.Component<
onChange={onChange}
onRequestClose={onRequestClose}
onApply={onApply}
dataSource={getList(project)}
dataSource={getList(sceneName, externalEventsName, project)}
openOnFocus={!isInline}
ref={field => (this._field = field)}
/>

View File

@@ -142,23 +142,29 @@ export default class LinkEvent extends React.Component<EventRendererProps, *> {
[disabledText]: this.props.disabled,
})}
>
<Trans>Include events from</Trans>{' '}
<i
className={classNames({
[selectableArea]: true,
})}
onClick={this.edit}
onKeyPress={event => {
if (shouldActivate(event)) {
this.edit(event);
}
}}
tabIndex={0}
>
{target || (
<Trans>{`<Enter the name of external events>`}</Trans>
)}
</i>
{this.props.scope.layout ? (
<>
<Trans>Include events from</Trans>{' '}
<i
className={classNames({
[selectableArea]: true,
})}
onClick={this.edit}
onKeyPress={event => {
if (shouldActivate(event)) {
this.edit(event);
}
}}
tabIndex={0}
>
{target || (
<Trans>{`<Enter the name of external events>`}</Trans>
)}
</i>{' '}
</>
) : (
<Trans>Links can't be used outside of a scene.</Trans>
)}
</span>
<IconButton
onClick={() => this.openTarget(i18n)}
@@ -175,6 +181,16 @@ export default class LinkEvent extends React.Component<EventRendererProps, *> {
<ExternalEventsAutoComplete
project={this.props.project}
value={target}
sceneName={
this.props.scope.layout
? this.props.scope.layout.getName()
: undefined
}
externalEventsName={
this.props.scope.externalEvents
? this.props.scope.externalEvents.getName()
: undefined
}
onChange={text => {
linkEvent.setTarget(text);
this.props.onUpdate();

View File

@@ -5,7 +5,7 @@ import ParameterRenderingService from './ParameterRenderingService';
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../ResourcesList/ResourceSource.flow';
} from '../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
import { type EventsScope } from '../InstructionOrExpression/EventsScope.flow';
import { setupInstructionParameters } from '../InstructionOrExpression/SetupInstructionParameters';

View File

@@ -8,7 +8,7 @@ import InstructionEditor from './index.js';
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../../ResourcesList/ResourceSource.flow';
} from '../../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../../ResourcesList/ResourceExternalEditor.flow';
import { type EventsScope } from '../../InstructionOrExpression/EventsScope.flow';

View File

@@ -13,16 +13,13 @@ import HelpButton from '../../UI/HelpButton';
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../../ResourcesList/ResourceSource.flow';
} from '../../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../../ResourcesList/ResourceExternalEditor.flow';
import { Column, Line, Spacer } from '../../UI/Grid';
import AlertMessage from '../../UI/AlertMessage';
import DismissableAlertMessage from '../../UI/DismissableAlertMessage';
import Window from '../../Utils/Window';
import {
getExtraInstructionInformation,
getInstructionTutorialHints,
} from '../../Hints';
import { getExtraInstructionInformation } from '../../Hints';
import DismissableTutorialMessage from '../../Hints/DismissableTutorialMessage';
import { isAnEventFunctionMetadata } from '../../EventsFunctionsExtensionsLoader';
import OpenInNew from '@material-ui/icons/OpenInNew';
@@ -34,6 +31,7 @@ import { getInstructionMetadata } from './NewInstructionEditor';
import { ColumnStackLayout } from '../../UI/Layout';
import { setupInstructionParameters } from '../../InstructionOrExpression/SetupInstructionParameters';
import ScrollView from '../../UI/ScrollView';
import { getInstructionTutorialIds } from '../../Utils/GDevelopServices/Tutorial';
const gd: libGDevelop = global.gd;
const styles = {
@@ -222,7 +220,7 @@ export default class InstructionParametersEditor extends React.Component<
const instructionExtraInformation = getExtraInstructionInformation(
instructionType
);
const tutorialHints = getInstructionTutorialHints(instructionType);
const tutorialIds = getInstructionTutorialIds(instructionType);
const objectParameterIndex = objectName
? getObjectParameterIndex(instructionMetadata)
: -1;
@@ -276,13 +274,13 @@ export default class InstructionParametersEditor extends React.Component<
)}
</Line>
)}
{tutorialHints.length ? (
{tutorialIds.length ? (
<Line>
<ColumnStackLayout expand>
{tutorialHints.map(tutorialHint => (
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
key={tutorialId}
tutorialId={tutorialId}
/>
))}
</ColumnStackLayout>

View File

@@ -7,7 +7,7 @@ import FlatButton from '../../UI/FlatButton';
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../../ResourcesList/ResourceSource.flow';
} from '../../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../../ResourcesList/ResourceExternalEditor.flow';
import InstructionParametersEditor from './InstructionParametersEditor';
import InstructionOrObjectSelector, {

Some files were not shown because too many files have changed in this diff Show More