Compare commits

...

47 Commits

Author SHA1 Message Date
github-actions[bot]
bb3abdb1fa Update translations [skip ci] (#3831)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2022-04-28 16:40:10 +02:00
Oxey405
cdddcafa68 Add an action to fade a sound or a music on a channel to a new volume (#3724) 2022-04-28 11:17:46 +02:00
AlexandreS
14175c334e Do not show in changelog - Trim player name when formatting it 2022-04-28 11:02:12 +02:00
D8H
6bd4dff03e Remove variable parameters from SavePlayerScore action (#3860)
Do not show in changelog
2022-04-28 11:01:36 +02:00
Clément Pasteau
ceec39f6a2 Bump version to 5.0.132
Do not show in changelog
2022-04-27 17:59:58 +02:00
AlexandreS
5abf80a0c9 Add possibility to customize leaderboard score column appearance (#3857) 2022-04-27 17:46:15 +02:00
AlexandreS
7f955d8703 Add expressions to specify decimal place where to round, floor and ceil 2022-04-27 16:47:14 +02:00
D8H
781dd42ccb Add analytics for extensions (#3848) 2022-04-26 09:16:01 +02:00
Fannie Yan
e584fa952d Make loader screen cover input fields (#3850)
Don't show in changelog
2022-04-25 09:57:21 +02:00
Florian Rival
5681667dde Add a gitpod.io configuration file (experimental)
* This is useful to edit some part of the codebase on gitpod.io

Only show in developer changelog
2022-04-24 14:29:11 +00:00
Florian Rival
d16f04f4a2 Change AlertMessage design to be flat
Avoid shadows if not really needed.

Don't show in changelog
2022-04-24 14:23:08 +00:00
Aurélien Vivet
e9b464beba Improve wording consistency (#3851) 2022-04-23 16:35:51 +02:00
Florian Rival
7597dbe0d1 Add (new) conditions and expressions to compare the number of instances picked or living on the scene (#3842)
* Add `PickedInstancesCount` and `SceneInstancesCount` expressions, to replace `Count`. These expressions don't do any "picking" of instances, so they are safe to use anywhere without "weird" side effects.
* Equivalent conditions are available for all objects. These conditions allow to check at any point in your events the number of instances living on the scene or picked by actions/conditions.
  * This is useful to check if enough objects are picked by a condition before launching an action. 
  * Because this condition does not change the already picked objects, it's safe to use anywhere without any side effect.

Only show the rest in the developer changelog:
* Allow to read events missing some fields (like `disabled`, `folded`).
* Reduce the useless information stored in project JSON files by not storing the fields if they have their default value.
2022-04-23 16:35:10 +02:00
Clément Pasteau
4777f0a824 Save project after creation on Desktop app 2022-04-22 19:11:34 +02:00
AlexandreS
5b2532f8f3 Remove unused variables
Do not show in changelog
2022-04-21 15:02:41 +02:00
AlexandreS
2c43de5120 Improve objects and groups lists
* Use same drag and drop effect for both lists
* Add possibility to duplicate group
* Add possibility to move multiple objects at once
2022-04-21 14:26:42 +02:00
Florian Rival
124ce1101d Fix leaderboards category icon 2022-04-20 14:27:38 +02:00
AlexandreS
eb3d6c2670 Send events when opening and finalizing events extraction as function
Don't show in changelog
2022-04-20 12:17:09 +02:00
Clément Pasteau
f737fa479f Don't allow picking image URLs that will fail when building the game on the web application
- targeting specifically images with failed CORS
2022-04-19 17:45:48 +02:00
Florian Rival
f93b3bc3b4 Fix crash when an empty effect is added to a layer
Fix #3835
2022-04-19 12:47:20 +02:00
Elairyx
5c6eb2dadb Polish the wording of particle emitter actions, conditions and expressions (#3829) 2022-04-15 16:24:51 +01:00
AlexandreS
3e6ca186f8 Add action to change image for particle emitter using project resources 2022-04-15 15:37:38 +02:00
github-actions[bot]
860e9d36e4 Update translations [skip ci] (#3808)
Co-authored-by: fannieyan <fannieyan@users.noreply.github.com>
2022-04-14 11:40:38 +02:00
Fannie Yan
c8b461cc5f Merge game management buttons in games dashboard (#3826)
* Merge game management button into one in Games dashboard
2022-04-14 10:51:48 +02:00
AlexandreS
36cdc5720a Bump newIDE version 2022-04-14 10:47:33 +02:00
AlexandreS
a7cd53b921 Add built-in leaderboard feature (actions, conditions, administration) (#3759) 2022-04-14 10:25:48 +02:00
D8H
01a25400ff New touch event functions that allow to get them several times (#3820)
* It allows extensions to handle touch events without conflicting with the scene event sheets.
* Existing touch functions continue to work and can be used alongside new ones without any issue.
2022-04-13 12:00:33 +02:00
D8H
2b484c0cf1 The url slug of a game can be verified and changed right before publication (#3811) 2022-04-12 20:39:34 +02:00
Fannie Yan
13204e4b53 Add a button "Access public profile" in one's profile details (#3819)
Add a button in profile details that opens the user's public profile on Liluo.io
2022-04-11 14:41:08 +02:00
D8H
c6d6466d54 Fix the input checkboxes "Playable with [...]" randomly checked when publishing a game for the first time (#3818) 2022-04-08 11:45:20 +01:00
Clément Pasteau
6cf737ac70 Remove unused imports (#3809)
Do not show in changelog
2022-04-06 15:00:04 +02:00
Arthur Pacaud
5b8cdbbace Update the button label in the dialog of an extension to show if the extension will be updated or (re)installed (#3777) 2022-04-06 13:47:36 +01:00
Clément Pasteau
a7df37a0dc Bump version to 131 (#3807) 2022-04-06 14:47:20 +02:00
D8H
7721f56dde Allow to specify a slug to be used in liluo.io games URL
* From the Game Details Dashboard, you can now select a username and a game name to create a custom unique URL for your game on Liluo.io, which can look like this: liluo.io/your-username/your-game-name
2022-04-06 14:35:50 +02:00
github-actions[bot]
08ada4f595 Update translations [skip ci] (#3764) 2022-04-06 13:21:28 +01:00
D8H
caa46a2fb0 Allow to define a default value for a "string with options" property (#3803) 2022-04-06 09:49:50 +02:00
D8H
660cdd753c [TopDownMovement] Add a condition to check which key was used or simulated and actions to change the current velocity (#3800) 2022-04-05 19:32:31 +02:00
D8H
3b9a1354ee [TopDownMovement] Fix the velocity value that wasn't exactly 0 when moving horizontally or vertically (#3799) 2022-04-04 23:47:17 +02:00
D8H
02fce4132a Add a condition to check if the mouse is inside the window. (#3787) 2022-04-01 12:01:53 +02:00
D8H
41241d75e1 Fix the camera rounding to work when following objects with even dimensions and pixel perfect mode is enabled (#3789) 2022-04-01 12:00:47 +02:00
Clément Pasteau
fc3e73f360 Rework the Game publishing on Liluo
* Display a summary of the Game that will be published when pushing a new build
* Add a helper message to change the game thumbnail
* Add option to save project by default when publishing to Liluo.io
* Allow defining if your game should be discoverable on Liluo.io or not
2022-04-01 10:50:05 +02:00
Florian Rival
230493039c Display an icon for the Home tab (instead of the label "Home") (#3792) 2022-04-01 09:40:22 +01:00
Florian Rival
50326e4d21 Add missing Text Input to the web-app (#3785)
Don't show in changelog
2022-03-29 13:17:44 +02:00
Florian Rival
cec7960a7d Add slug to ExampleShortHeader and Example type
Don't show in changelog
2022-03-28 16:22:49 +02:00
Florian Rival
cec901d4c2 Fix npm not installing git:// dependencies (#3780)
Don't show in changelog
2022-03-28 14:55:01 +02:00
Florian Rival
29f0567140 Add Linux zip and deb target to the packaged desktop app (#3776)
Only show in developer changelog
2022-03-27 12:55:50 +02:00
Clément Pasteau
94af33c815 Switch game metrics from live ones to cached ones for performance
Do not show in changelog
2022-03-25 14:19:14 +01:00
231 changed files with 10492 additions and 2287 deletions

View File

@@ -135,7 +135,7 @@ jobs:
# Build GDevelop IDE (seems like we need to allow Node.js to use more space than usual)
- run:
name: Build GDevelop IDE
command: export NODE_OPTIONS="--max-old-space-size=7168" && cd newIDE/electron-app && npm run build -- --linux AppImage --publish=never
command: export NODE_OPTIONS="--max-old-space-size=7168" && cd newIDE/electron-app && npm run build -- --linux AppImage zip deb --publish=never
- run:
name: Clean dist folder to keep only installers/binaries.

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@
/Binaries/.embuild*
/Binaries/build*
/Binaries/embuild*
/emsdk
*.dll
*.exe
*.a

25
.gitpod.yml Normal file
View File

@@ -0,0 +1,25 @@
# This is a configuration file allowing to quickly set up a development environment
# on GitPod (https://www.gitpod.io/).
# Also check GitHub codespaces if you're interested in working
# on a remote development server.
# This works well for:
# - The editor web-app, including the C++ classes.
# This is not yet adapted for:
# - Working on the game engine or extensions, as they can't be easily tested on the web-app.
# - Working on the desktop app (Electron).
tasks:
- name: Install dependencies for Emscripten and build GDevelop.js
init: |
sudo apt-get update
sudo apt install cmake python-is-python3 python3-distutils -y
git clone https://github.com/juj/emsdk.git && cd emsdk && ./emsdk install 1.39.6 && ./emsdk activate 1.39.6 && cd ..
cd GDevelop.js
npm install
source ../emsdk/emsdk_env.sh && npm run build -- --dev
cd ..
- name: Install GDevelop IDE dependencies
init: cd newIDE/app && npm install && cd ../electron-app && npm install

View File

@@ -63,8 +63,12 @@ void StandardEvent::UnserializeFrom(gd::Project& project,
project, conditions, element.GetChild("conditions", 0, "Conditions"));
gd::EventsListSerialization::UnserializeInstructionsFrom(
project, actions, element.GetChild("actions", 0, "Actions"));
gd::EventsListSerialization::UnserializeEventsFrom(
project, events, element.GetChild("events", 0, "Events"));
events.Clear();
if (element.HasChild("events", "Events")) {
gd::EventsListSerialization::UnserializeEventsFrom(
project, events, element.GetChild("events", 0, "Events"));
}
}
} // namespace gd

View File

@@ -24,8 +24,8 @@ void EventsCodeGenerationContext::InheritsFrom(
parent_.objectsListsToBeDeclared.end(),
std::inserter(alreadyDeclaredObjectsLists,
alreadyDeclaredObjectsLists.begin()));
std::copy(parent_.objectsListsWithoutPickingToBeDeclared.begin(),
parent_.objectsListsWithoutPickingToBeDeclared.end(),
std::copy(parent_.objectsListsOrEmptyToBeDeclared.begin(),
parent_.objectsListsOrEmptyToBeDeclared.end(),
std::inserter(alreadyDeclaredObjectsLists,
alreadyDeclaredObjectsLists.begin()));
std::copy(parent_.emptyObjectsListsToBeDeclared.begin(),
@@ -57,10 +57,10 @@ void EventsCodeGenerationContext::ObjectsListNeeded(
depthOfLastUse[objectName] = GetContextDepth();
}
void EventsCodeGenerationContext::ObjectsListWithoutPickingNeeded(
void EventsCodeGenerationContext::ObjectsListNeededOrEmptyIfJustDeclared(
const gd::String& objectName) {
if (!IsToBeDeclared(objectName))
objectsListsWithoutPickingToBeDeclared.insert(objectName);
objectsListsOrEmptyToBeDeclared.insert(objectName);
depthOfLastUse[objectName] = GetContextDepth();
}
@@ -77,8 +77,8 @@ std::set<gd::String> EventsCodeGenerationContext::GetAllObjectsToBeDeclared()
const {
std::set<gd::String> allObjectListsToBeDeclared(
objectsListsToBeDeclared.begin(), objectsListsToBeDeclared.end());
allObjectListsToBeDeclared.insert(objectsListsWithoutPickingToBeDeclared.begin(),
objectsListsWithoutPickingToBeDeclared.end());
allObjectListsToBeDeclared.insert(objectsListsOrEmptyToBeDeclared.begin(),
objectsListsOrEmptyToBeDeclared.end());
allObjectListsToBeDeclared.insert(emptyObjectsListsToBeDeclared.begin(),
emptyObjectsListsToBeDeclared.end());

View File

@@ -88,19 +88,19 @@ class GD_CORE_API EventsCodeGenerationContext {
const EventsCodeGenerationContext* GetParentContext() const { return parent; }
/**
* Mark the object has being the object being handled by the instruction
* Mark the object as being the object being handled by the instruction.
*/
void SetCurrentObject(const gd::String& objectName) {
currentObject = objectName;
};
/**
* Set that no particular object is being handled by an instruction
* Set that no particular object is being handled by an instruction.
*/
void SetNoCurrentObject() { currentObject = ""; };
/**
* Get the object being handled by the instruction
* Get the object being handled by the instruction.
*/
const gd::String& GetCurrentObject() const { return currentObject; };
@@ -109,7 +109,7 @@ class GD_CORE_API EventsCodeGenerationContext {
*
* The list will be filled with objects from the scene if it is the first time
* it is requested, unless there is already an object list with this name
* (i.e. `ObjectAlreadyDeclared(objectName)` returns true).
* (i.e. `ObjectAlreadyDeclaredByParents(objectName)` returns true).
*/
void ObjectsListNeeded(const gd::String& objectName);
@@ -121,7 +121,7 @@ class GD_CORE_API EventsCodeGenerationContext {
* from the scene. If there is already an objects list with this name, no new
* list will be declared again.
*/
void ObjectsListWithoutPickingNeeded(const gd::String& objectName);
void ObjectsListNeededOrEmptyIfJustDeclared(const gd::String& objectName);
/**
* Call this when an instruction in the event needs an empty object list,
@@ -134,10 +134,9 @@ class GD_CORE_API EventsCodeGenerationContext {
void EmptyObjectsListNeeded(const gd::String& objectName);
/**
* Return true if an object list has already been declared (or is going to be
* declared).
* Return true if an object list has already been declared by the parent contexts.
*/
bool ObjectAlreadyDeclared(const gd::String& objectName) const {
bool ObjectAlreadyDeclaredByParents(const gd::String& objectName) const {
return (alreadyDeclaredObjectsLists.find(objectName) !=
alreadyDeclaredObjectsLists.end());
};
@@ -166,9 +165,9 @@ class GD_CORE_API EventsCodeGenerationContext {
* Return the objects lists which will be will be declared, without filling
* them with objects from the scene.
*/
const std::set<gd::String>& GetObjectsListsToBeDeclaredWithoutPicking()
const std::set<gd::String>& GetObjectsListsToBeEmptyIfJustDeclared()
const {
return objectsListsWithoutPickingToBeDeclared;
return objectsListsOrEmptyToBeDeclared;
};
/**
@@ -184,7 +183,7 @@ class GD_CORE_API EventsCodeGenerationContext {
* Return the objects lists which are already declared and can be used in the
* current context without declaration.
*/
const std::set<gd::String>& GetObjectsListsAlreadyDeclared() const {
const std::set<gd::String>& GetObjectsListsAlreadyDeclaredByParents() const {
return alreadyDeclaredObjectsLists;
};
@@ -227,22 +226,20 @@ class GD_CORE_API EventsCodeGenerationContext {
*/
size_t GetCurrentConditionDepth() const { return customConditionDepth; }
private:
/**
* \brief Returns true if the given object is already going to be declared
* (either as a traditional objects list, or one without picking, or one
* empty).
*
* (either as a traditional objects list, or an empty one).
*/
bool IsToBeDeclared(const gd::String& objectName) {
return objectsListsToBeDeclared.find(objectName) !=
objectsListsToBeDeclared.end() ||
objectsListsWithoutPickingToBeDeclared.find(objectName) !=
objectsListsWithoutPickingToBeDeclared.end() ||
objectsListsOrEmptyToBeDeclared.find(objectName) !=
objectsListsOrEmptyToBeDeclared.end() ||
emptyObjectsListsToBeDeclared.find(objectName) !=
emptyObjectsListsToBeDeclared.end();
};
private:
std::set<gd::String>
alreadyDeclaredObjectsLists; ///< Objects lists already needed in a
///< parent context.
@@ -250,7 +247,7 @@ class GD_CORE_API EventsCodeGenerationContext {
objectsListsToBeDeclared; ///< Objects lists that will be declared in
///< this context.
std::set<gd::String>
objectsListsWithoutPickingToBeDeclared; ///< Objects lists that will be
objectsListsOrEmptyToBeDeclared; ///< Objects lists that will be
///< declared in this context,
///< but not filled with scene's
///< objects.

View File

@@ -743,7 +743,7 @@ gd::String EventsCodeGenerator::GenerateObjectsDeclarationCode(
gd::String declarationsCode;
for (auto object : context.GetObjectsListsToBeDeclared()) {
gd::String objectListDeclaration = "";
if (!context.ObjectAlreadyDeclared(object)) {
if (!context.ObjectAlreadyDeclaredByParents(object)) {
objectListDeclaration = "std::vector<RuntimeObject*> " +
GetObjectListName(object, context) +
" = runtimeContext->GetObjectsRawPointers(\"" +
@@ -754,9 +754,9 @@ gd::String EventsCodeGenerator::GenerateObjectsDeclarationCode(
declarationsCode += objectListDeclaration + "\n";
}
for (auto object : context.GetObjectsListsToBeDeclaredWithoutPicking()) {
for (auto object : context.GetObjectsListsToBeEmptyIfJustDeclared()) {
gd::String objectListDeclaration = "";
if (!context.ObjectAlreadyDeclared(object)) {
if (!context.ObjectAlreadyDeclaredByParents(object)) {
objectListDeclaration = "std::vector<RuntimeObject*> " +
GetObjectListName(object, context) + ";\n";
context.SetObjectDeclared(object);
@@ -767,7 +767,7 @@ gd::String EventsCodeGenerator::GenerateObjectsDeclarationCode(
}
for (auto object : context.GetObjectsListsToBeDeclaredEmpty()) {
gd::String objectListDeclaration = "";
if (!context.ObjectAlreadyDeclared(object)) {
if (!context.ObjectAlreadyDeclaredByParents(object)) {
objectListDeclaration = "std::vector<RuntimeObject*> " +
GetObjectListName(object, context) + ";\n";
context.SetObjectDeclared(object);

View File

@@ -462,17 +462,10 @@ class GD_CORE_API EventsCodeGenerator {
* Other standard parameters type that should be implemented by platforms:
* - currentScene: Reference to the current runtime scene.
* - objectList : a map containing lists of objects which are specified by the
object name in another parameter. Example:
* \code
AddExpression("Count", _("Object count"), _("Count the number of picked
objects"), _("Objects"), "res/conditions/nbObjet.png")
.AddParameter("objectList", _("Object"))
.SetFunctionName("getPickedObjectsCount");
* \endcode
* - objectListWithoutPicking : Same as objectList but do not pick object if
object name in another parameter.
* - objectListOrEmptyIfJustDeclared : Same as `objectList` but do not pick object if
they are not already picked.
* - objectPtr : Return a reference to the object specified by the object name in
* - objectPtr: Return a reference to the object specified by the object name in
another parameter. Example:
* \code
.AddParameter("object", _("Object"))

View File

@@ -218,8 +218,8 @@ void EventsListSerialization::UnserializeEventsFrom(
event = std::make_shared<EmptyEvent>();
}
event->SetDisabled(eventElem.GetBoolAttribute("disabled"));
event->SetFolded(eventElem.GetBoolAttribute("folded"));
event->SetDisabled(eventElem.GetBoolAttribute("disabled", false));
event->SetFolded(eventElem.GetBoolAttribute("folded", false));
list.InsertEvent(event, list.GetEventsCount());
}
@@ -232,8 +232,8 @@ void EventsListSerialization::SerializeEventsTo(const EventsList& list,
const gd::BaseEvent& event = list.GetEvent(j);
SerializerElement& eventElem = events.AddChild("event");
eventElem.SetAttribute("disabled", event.IsDisabled());
eventElem.SetAttribute("folded", event.IsFolded());
if (event.IsDisabled()) eventElem.SetAttribute("disabled", event.IsDisabled());
if (event.IsFolded()) eventElem.SetAttribute("folded", event.IsFolded());
eventElem.AddChild("type").SetValue(event.GetType());
event.SerializeTo(eventElem);

View File

@@ -343,6 +343,34 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAudioExtension(
"res/actions/music.png")
.AddCodeOnlyParameter("currentScene", "")
.MarkAsComplex();
extension
.AddAction(
"FadeSoundVolume",
_("Fade the volume of a sound played on a channel."),
_("Fade the volume of a sound played on a channel to the specified volume within the specified duration."),
_("Fade the sound on channel _PARAM1_ to volume _PARAM2_ within _PARAM3_ seconds"),
_("Sounds on channels"),
"res/actions/son24.png",
"res/actions/son.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("expression", _("Channel identifier"))
.AddParameter("expression", _("Final volume (0-100)"))
.AddParameter("expression", _("Fading time in seconds"))
.MarkAsAdvanced();
extension
.AddAction(
"FadeMusicVolume",
_("Fade the volume of a music played on a channel."),
_("Fade the volume of a music played on a channel to the specified volume within the specified duration."),
_("Fade the music on channel _PARAM1_ to volume _PARAM2_ within _PARAM3_ seconds"),
_("Music on channels"),
"res/actions/music24.png",
"res/actions/music.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("expression", _("Channel identifier"))
.AddParameter("expression", _("Final volume (0-100)"))
.AddParameter("expression", _("Fading time in seconds"))
.MarkAsAdvanced();
extension
.AddCondition("MusicPlaying",

View File

@@ -1252,7 +1252,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"res/actions/create24.png",
"res/actions/create24.png")
.AddCodeOnlyParameter("objectsContext", "")
.AddParameter("objectListWithoutPicking", _("Object to create"))
.AddParameter("objectListOrEmptyIfJustDeclared", _("Object to create"))
.AddParameter("expression", _("X position"))
.AddParameter("expression", _("Y position"))
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
@@ -1270,7 +1270,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"res/actions/create24.png",
"res/actions/create24.png")
.AddCodeOnlyParameter("objectsContext", "")
.AddParameter("objectListWithoutPicking", _("Group of potential objects"))
.AddParameter("objectListOrEmptyIfJustDeclared", _("Group of potential objects"))
.SetParameterLongDescription(
_("Group containing objects that can be created by the action."))
.AddParameter("string", _("Name of the object to create"))
@@ -1418,7 +1418,33 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"res/conditions/nbObjet.png")
.AddParameter("objectList", _("Object"))
.UseStandardRelationalOperatorParameters("number")
.MarkAsSimple();
.MarkAsSimple()
.SetHidden();
extension.AddExpressionAndCondition(
"number",
"SceneInstancesCount",
_("Number of object instances on the scene"),
_("the number of instances of the specified objects living on the scene"),
_("the number of _PARAM1_ living on the scene"),
_("Objects"),
"res/conditions/nbObjet24.png")
.AddCodeOnlyParameter("objectsContext", "")
.AddParameter("objectListOrEmptyWithoutPicking", _("Object"))
.UseStandardParameters("number")
.MarkAsSimple();
extension.AddExpressionAndCondition(
"number",
"PickedInstancesCount",
_("Number of object instances currently picked"),
_("the number of instances picked by the previous conditions (or actions)"),
_("the number of _PARAM0_ currently picked"),
_("Objects"),
"res/conditions/nbObjet24.png")
.AddParameter("objectListOrEmptyWithoutPicking", _("Object"))
.UseStandardParameters("number")
.MarkAsSimple();
extension
.AddCondition(
@@ -1526,7 +1552,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"currently picked in the event"),
"",
"res/conditions/nbObjet.png")
.AddParameter("objectList", _("Object"));
.AddParameter("objectList", _("Object"))
.SetHidden(); // Deprecated
obj.AddStrExpression("ObjectName",
_("Object name"),

View File

@@ -229,6 +229,15 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
"res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
.AddExpression("ceilTo",
_("Ceil (round up) to a decimal point"),
_("Round number up to the Nth decimal place"),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Expression"))
.AddParameter("expression", _("Expression"), "", true);
extension
.AddExpression("floor",
_("Floor (round down)"),
@@ -237,6 +246,15 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
"res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
.AddExpression("floorTo",
_("Floor (round down) to a decimal point"),
_("Round number down to the Nth decimal place"),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Expression"))
.AddParameter("expression", _("Expression"), "", true);
extension
.AddExpression("cos",
_("Cosine"),
@@ -295,6 +313,15 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
"res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
.AddExpression("roundTo",
_("Round to a decimal point"),
_("Round a number to the Nth decimal place"),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Expression"))
.AddParameter("expression", _("Expression"), "", true);
extension
.AddExpression("exp",
_("Exponential"),

View File

@@ -190,6 +190,17 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
extension.AddDuplicatedCondition("SourisY", "MouseY").SetHidden();
extension.AddDuplicatedExpression("SourisY", "MouseY").SetHidden();
extension
.AddCondition("IsMouseInsideCanvas",
_("Mouse cursor is inside the window"),
_("Check if the mouse cursor is inside the window."),
_("The mouse cursor is inside the window"),
"",
"res/conditions/mouse24.png",
"res/conditions/mouse.png")
.AddCodeOnlyParameter("currentScene", "")
.MarkAsAdvanced();
extension
.AddCondition("MouseButtonPressed",
_("Mouse button pressed or touch held"),
@@ -300,7 +311,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
_("Multitouch"),
"res/conditions/touch24.png",
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "");
.AddCodeOnlyParameter("currentScene", "")
.SetHidden();
extension
.AddCondition(
@@ -315,8 +327,54 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
_("Multitouch"),
"res/conditions/touch24.png",
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "")
.SetHidden();
extension
.AddCondition(
"HasAnyTouchStarted",
_("A new touch has started"),
_("Check if a touch has just started on this frame. The touch identifiers can be "
"accessed using StartedTouchId() and StartedTouchCount()."),
_("A new touch has started"),
_("Multitouch"),
"res/conditions/touch24.png",
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "");
extension
.AddExpression(
"StartedTouchCount",
_("Started touch count"),
_("The number of touches that have just started on this frame. The touch identifiers can be "
"accessed using StartedTouchId()."),
_("Multitouch"),
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "");
extension
.AddExpression(
"StartedTouchId",
_("Started touch identifier"),
_("The identifier of the touch that has just started on this frame. The touch number of touches can be "
"accessed using StartedTouchCount()."),
_("Multitouch"),
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("expression", _("Touch index"));
extension
.AddCondition(
"HasTouchEnded",
_("A touch has ended"),
_("Check if a touch has ended."),
_("The touch with identifier _PARAM1_ has ended"),
_("Multitouch"),
"res/conditions/touch24.png",
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("expression", _("Touch identifier"));
extension
.AddExpression("MouseWheelDelta",
_("Mouse wheel: Displacement"),
@@ -331,7 +389,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
_("Identifier of the last touch"),
_("Multitouch"),
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "");
.AddCodeOnlyParameter("currentScene", "")
.SetHidden();
extension
.AddExpression("LastEndedTouchId",
@@ -339,7 +398,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
_("Identifier of the last ended touch"),
_("Multitouch"),
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "");
.AddCodeOnlyParameter("currentScene", "")
.SetHidden();
}
} // namespace gd

View File

@@ -9,6 +9,7 @@
#if defined(GD_IDE_ONLY)
#include <map>
#include <memory>
#include "GDCore/String.h"
namespace gd {
class Project;
@@ -151,15 +152,16 @@ class GD_CORE_API ParameterMetadata {
}
/**
* \brief Return true if the type of the parameter is "object", "objectPtr" or
* "objectList".
* \brief Return true if the type of the parameter is representing one object
* (or more, i.e: an object group).
*
* \see gd::ParameterMetadata::GetType
*/
static bool IsObject(const gd::String &parameterType) {
return parameterType == "object" || parameterType == "objectPtr" ||
parameterType == "objectList" ||
parameterType == "objectListWithoutPicking";
parameterType == "objectListOrEmptyIfJustDeclared" ||
parameterType == "objectListOrEmptyWithoutPicking";
}
/**
@@ -196,7 +198,8 @@ class GD_CORE_API ParameterMetadata {
parameterType == "objectPointName" ||
parameterType == "objectAnimationName" ||
parameterType == "functionParameterName" ||
parameterType == "externalLayoutName";
parameterType == "externalLayoutName" ||
parameterType == "leaderboardId";
} else if (type == "variable") {
return parameterType == "objectvar" || parameterType == "globalvar" ||
parameterType == "scenevar";

View File

@@ -66,6 +66,9 @@ Project::Project()
projectUuid(""),
useDeprecatedZeroAsDefaultZOrder(false),
useExternalSourceFiles(false),
isPlayableWithKeyboard(false),
isPlayableWithGamepad(false),
isPlayableWithMobile(false),
currentPlatform(NULL),
gdMajorVersion(gd::VersionWrapper::Major()),
gdMinorVersion(gd::VersionWrapper::Minor()),

View File

@@ -77,8 +77,6 @@ bool SerializerElement::GetBoolAttribute(const gd::String& name,
}
}
std::cout << "Bool attribute \"" << name << "\" not found, returning "
<< defaultValue;
return defaultValue;
}

View File

@@ -31,7 +31,7 @@ TEST_CASE("EventsCodeGenerationContext", "[common][events]") {
gd::EventsCodeGenerationContext c1(&maxDepth);
c1.ObjectsListNeeded("c1.object1");
c1.ObjectsListNeeded("c1.object2");
c1.ObjectsListWithoutPickingNeeded("c1.noPicking1");
c1.ObjectsListNeededOrEmptyIfJustDeclared("c1.noPicking1");
gd::EventsCodeGenerationContext c2;
c2.InheritsFrom(c1);
@@ -47,7 +47,7 @@ TEST_CASE("EventsCodeGenerationContext", "[common][events]") {
gd::EventsCodeGenerationContext c5;
c5.InheritsFrom(c2);
c5.ObjectsListWithoutPickingNeeded("c5.noPicking1");
c5.ObjectsListNeededOrEmptyIfJustDeclared("c5.noPicking1");
c5.ObjectsListNeeded("c5.object1");
c5.ObjectsListNeeded("c1.object2");
c5.EmptyObjectsListNeeded("c5.empty1");
@@ -70,36 +70,36 @@ TEST_CASE("EventsCodeGenerationContext", "[common][events]") {
}
SECTION("Object list needed") {
REQUIRE(c1.GetObjectsListsAlreadyDeclared() == std::set<gd::String>());
REQUIRE(c1.GetObjectsListsAlreadyDeclaredByParents() == std::set<gd::String>());
REQUIRE(c1.GetObjectsListsToBeDeclared() ==
std::set<gd::String>({"c1.object1", "c1.object2"}));
REQUIRE(c1.GetObjectsListsToBeDeclaredWithoutPicking() ==
REQUIRE(c1.GetObjectsListsToBeEmptyIfJustDeclared() ==
std::set<gd::String>({"c1.noPicking1"}));
REQUIRE(c1.GetAllObjectsToBeDeclared() ==
std::set<gd::String>({"c1.object1", "c1.object2", "c1.noPicking1"}));
REQUIRE(c2.GetObjectsListsAlreadyDeclared() ==
REQUIRE(c2.GetObjectsListsAlreadyDeclaredByParents() ==
std::set<gd::String>({"c1.object1", "c1.object2", "c1.noPicking1"}));
REQUIRE(c2.GetObjectsListsToBeDeclared() ==
std::set<gd::String>({"c2.object1"}));
REQUIRE(c2.GetObjectsListsToBeDeclaredWithoutPicking() == std::set<gd::String>());
REQUIRE(c2.GetObjectsListsToBeEmptyIfJustDeclared() == std::set<gd::String>());
REQUIRE(c2.GetAllObjectsToBeDeclared() ==
std::set<gd::String>({"c2.object1"}));
REQUIRE(c3.GetObjectsListsAlreadyDeclared() ==
REQUIRE(c3.GetObjectsListsAlreadyDeclaredByParents() ==
std::set<gd::String>({"c1.object1", "c1.object2", "c1.noPicking1"}));
REQUIRE(c3.GetObjectsListsToBeDeclared() ==
std::set<gd::String>({"c3.object1", "c1.object2"}));
REQUIRE(c3.GetObjectsListsToBeDeclaredWithoutPicking() == std::set<gd::String>());
REQUIRE(c3.GetObjectsListsToBeEmptyIfJustDeclared() == std::set<gd::String>());
REQUIRE(c3.GetAllObjectsToBeDeclared() ==
std::set<gd::String>({"c3.object1", "c1.object2"}));
REQUIRE(c5.GetObjectsListsAlreadyDeclared() ==
REQUIRE(c5.GetObjectsListsAlreadyDeclaredByParents() ==
std::set<gd::String>(
{"c1.object1", "c1.object2", "c1.noPicking1", "c2.object1"}));
REQUIRE(c5.GetObjectsListsToBeDeclared() ==
std::set<gd::String>({"c5.object1", "c1.object2"}));
REQUIRE(c5.GetObjectsListsToBeDeclaredWithoutPicking() ==
REQUIRE(c5.GetObjectsListsToBeEmptyIfJustDeclared() ==
std::set<gd::String>({"c5.noPicking1"}));
REQUIRE(c5.GetObjectsListsToBeDeclaredEmpty() ==
std::set<gd::String>({"c5.empty1"}));
@@ -107,22 +107,22 @@ TEST_CASE("EventsCodeGenerationContext", "[common][events]") {
std::set<gd::String>({"c5.object1", "c5.noPicking1", "c1.object2", "c5.empty1"}));
}
SECTION("ObjectAlreadyDeclared") {
REQUIRE(c1.ObjectAlreadyDeclared("c1.object1") == false);
REQUIRE(c2.ObjectAlreadyDeclared("c1.object1") == true);
REQUIRE(c3.ObjectAlreadyDeclared("c1.object1") == true);
REQUIRE(c4.ObjectAlreadyDeclared("c1.object1") == true);
REQUIRE(c5.ObjectAlreadyDeclared("c1.object1") == true);
SECTION("ObjectAlreadyDeclaredByParents") {
REQUIRE(c1.ObjectAlreadyDeclaredByParents("c1.object1") == false);
REQUIRE(c2.ObjectAlreadyDeclaredByParents("c1.object1") == true);
REQUIRE(c3.ObjectAlreadyDeclaredByParents("c1.object1") == true);
REQUIRE(c4.ObjectAlreadyDeclaredByParents("c1.object1") == true);
REQUIRE(c5.ObjectAlreadyDeclaredByParents("c1.object1") == true);
REQUIRE(c2.ObjectAlreadyDeclared("c2.object1") == false);
REQUIRE(c1.ObjectAlreadyDeclared("c2.object1") == false);
REQUIRE(c3.ObjectAlreadyDeclared("c2.object1") == false);
REQUIRE(c4.ObjectAlreadyDeclared("c2.object1") == true);
REQUIRE(c5.ObjectAlreadyDeclared("c2.object1") == true);
REQUIRE(c2.ObjectAlreadyDeclaredByParents("c2.object1") == false);
REQUIRE(c1.ObjectAlreadyDeclaredByParents("c2.object1") == false);
REQUIRE(c3.ObjectAlreadyDeclaredByParents("c2.object1") == false);
REQUIRE(c4.ObjectAlreadyDeclaredByParents("c2.object1") == true);
REQUIRE(c5.ObjectAlreadyDeclaredByParents("c2.object1") == true);
REQUIRE(c3.ObjectAlreadyDeclared("some object") == false);
REQUIRE(c3.ObjectAlreadyDeclaredByParents("some object") == false);
c3.SetObjectDeclared("some object");
REQUIRE(c3.ObjectAlreadyDeclared("some object") == true);
REQUIRE(c3.ObjectAlreadyDeclaredByParents("some object") == true);
}
SECTION("Object list last depth") {

View File

@@ -1,39 +1,40 @@
describe('Inventory', function () {
var runtimeGame = new gdjs.RuntimeGame({
variables: [],
properties: { windowWidth: 800, windowHeight: 600 },
resources: { resources: [] },
it('Inventories can be serialized then unserialized with no data loss', () => {
var runtimeGame = new gdjs.RuntimeGame({
variables: [],
properties: { windowWidth: 800, windowHeight: 600 },
resources: { resources: [] },
});
var runtimeScene = new gdjs.RuntimeScene(runtimeGame);
gdjs.evtTools.inventory.add(runtimeScene, 'MyInventory', 'sword');
gdjs.evtTools.inventory.add(runtimeScene, 'MyInventory', 'sword');
gdjs.evtTools.inventory.equip(runtimeScene, 'MyInventory', 'sword', true);
gdjs.evtTools.inventory.add(runtimeScene, 'MyInventory', 'armor');
gdjs.evtTools.inventory.setMaximum(runtimeScene, 'MyInventory', 'armor', 1);
var variable = new gdjs.Variable();
gdjs.evtTools.inventory.serializeToVariable(
runtimeScene,
'MyInventory',
variable
);
gdjs.evtTools.inventory.unserializeFromVariable(
runtimeScene,
'MyInventory2',
variable
);
expect(
gdjs.evtTools.inventory.count(runtimeScene, 'MyInventory2', 'sword')
).to.be(2);
expect(
gdjs.evtTools.inventory.isEquipped(runtimeScene, 'MyInventory2', 'sword')
).to.be(true);
expect(
gdjs.evtTools.inventory.count(runtimeScene, 'MyInventory2', 'armor')
).to.be(1);
expect(
gdjs.evtTools.inventory.add(runtimeScene, 'MyInventory2', 'armor')
).to.be(false);
});
var runtimeScene = new gdjs.RuntimeScene(runtimeGame);
gdjs.evtTools.inventory.add(runtimeScene, 'MyInventory', 'sword');
gdjs.evtTools.inventory.add(runtimeScene, 'MyInventory', 'sword');
gdjs.evtTools.inventory.equip(runtimeScene, 'MyInventory', 'sword', true);
gdjs.evtTools.inventory.add(runtimeScene, 'MyInventory', 'armor');
gdjs.evtTools.inventory.setMaximum(runtimeScene, 'MyInventory', 'armor', 1);
var variable = new gdjs.Variable();
gdjs.evtTools.inventory.serializeToVariable(
runtimeScene,
'MyInventory',
variable
);
gdjs.evtTools.inventory.unserializeFromVariable(
runtimeScene,
'MyInventory2',
variable
);
expect(
gdjs.evtTools.inventory.count(runtimeScene, 'MyInventory2', 'sword')
).to.be(2);
expect(
gdjs.evtTools.inventory.isEquipped(runtimeScene, 'MyInventory2', 'sword')
).to.be(true);
expect(
gdjs.evtTools.inventory.count(runtimeScene, 'MyInventory2', 'armor')
).to.be(1);
expect(
gdjs.evtTools.inventory.add(runtimeScene, 'MyInventory2', 'armor')
).to.be(false);
});

View File

@@ -0,0 +1,258 @@
// @flow
/**
* This is a declaration of an extension for GDevelop 5.
*
* Changes in this file are watched and automatically imported if the editor
* is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts).
*
* The file must be named "JsExtension.js", otherwise GDevelop won't load it.
* ⚠️ If you make a change and the extension is not loaded, open the developer console
* and search for any errors.
*
* More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
*/
/*::
// Import types to allow Flow to do static type checking on this file.
// Extensions declaration are typed using Flow (like the editor), but the files
// for the game engine are checked with TypeScript annotations.
import { type ObjectsRenderingService, type ObjectsEditorService } from '../JsExtensionTypes.flow.js'
*/
module.exports = {
createExtension: function (
_ /*: (string) => string */,
gd /*: libGDevelop */
) {
const extension = new gd.PlatformExtension();
extension
.setExtensionInformation(
'Leaderboards',
_('Leaderboards (experimental)'),
_('Allow your game to send scores to your leaderboards.'),
'Florian Rival',
'Open source (MIT License)'
)
.setExtensionHelpPath('/all-features/leaderboards')
.setCategory('Leaderboards')
.addInstructionOrExpressionGroupMetadata(_('Leaderboards (experimental)'))
.setIcon('JsPlatform/Extensions/leaderboard.svg');
extension
.addAction(
'SavePlayerScore',
_('Save player score'),
_("Save the player's score to the given leaderboard."),
_(
'Send to leaderboard _PARAM1_ the score _PARAM2_ with player name: _PARAM3_.'
),
_('Save score'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('leaderboardId', _('Leaderboard'), '', false)
.addParameter(
'expression',
_('Score to register for the player'),
'',
false
)
.addParameter('string', _('Name to register for the player'), '', false)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/sha256.js')
.addIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.savePlayerScore');
extension
.addCondition(
'HasLastSaveErrored',
_('Last score save has errored'),
_('Check if the last attempt to save a score has errored.'),
_('Last score save in leaderboard _PARAM0_ has errored'),
_('Save score'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.addParameter('leaderboardId', _('Leaderboard'), '', true)
.setParameterLongDescription(
_(
'If no leaderboard is specified, will return the value related to the last leaderboard save action.'
)
)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.hasSavingErrored');
extension
.addCondition(
'HasLastSaveSucceeded',
_('Last score save has succeeded'),
_('Check if the last attempt to save a score has succeeded.'),
_('Last score save in leaderboard _PARAM0_ has succeeded'),
_('Save score'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.addParameter('leaderboardId', _('Leaderboard'), '', true)
.setParameterLongDescription(
_(
'If no leaderboard is specified, will return the value related to the last leaderboard save action that successfully ended.'
)
)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.hasBeenSaved');
extension
.addCondition(
'IsSaving',
_('Score is saving'),
_('Check if a score is currently being saved in leaderboard.'),
_('Score is saving in leaderboard _PARAM0_'),
_('Save score'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.addParameter('leaderboardId', _('Leaderboard'), '', true)
.setParameterLongDescription(
_(
'If no leaderboard is specified, will return the value related to the last leaderboard save action.'
)
)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.isSaving');
extension
.addStrExpression(
'LastSaveError',
_('Error of last save attempt'),
_('Get the error of the last save attempt.'),
_('Error of last save attempt in leaderboard _PARAM0_'),
'JsPlatform/Extensions/leaderboard.svg'
)
.addParameter('leaderboardId', _('Leaderboard'), '', true)
.setParameterLongDescription(
_(
'If no leaderboard is specified, will return the value related to the last leaderboard save action.'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.getLastSaveError');
extension
.addCondition(
'IsLeaderboardViewErrored',
_('Leaderboard display has errored'),
_('Check if the display of the leaderboard errored.'),
_('Leaderboard display has errored'),
_('Display leaderboard'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.isLeaderboardViewErrored');
extension
.addCondition(
'IsLeaderboardViewLoaded',
_('Leaderboard display has loaded'),
_(
'Check if the display of the leaderboard has finished loading and been displayed on screen.'
),
_('Leaderboard display has loaded and is displayed on screen'),
_('Display leaderboard'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.isLeaderboardViewLoaded');
extension
.addCondition(
'IsLeaderboardViewLoading',
_('Leaderboard display is loading'),
_('Check if the display of the leaderboard is loading.'),
_('Leaderboard display is loading'),
_('Display leaderboard'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.isLeaderboardViewLoading');
extension
.addStrExpression(
'FormatPlayerName',
_('Format player name'),
_('Formats a name so that it can be submitted to a leaderboard.'),
_('Save score'),
'JsPlatform/Extensions/leaderboard.svg'
)
.addParameter('string', _('Raw player name'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.formatPlayerName');
extension
.addAction(
'DisplayLeaderboard',
_('Display leaderboard'),
_(
'Display the specified leaderboard on top of the game. If a leaderboard was already displayed on top of the game, the new leaderboard will replace it.'
),
_('Display leaderboard _PARAM1_ (display a loader: _PARAM2_)'),
_('Display leaderboard'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('leaderboardId', _('Leaderboard'), '', false)
.addParameter(
'yesorno',
_('Display loader while leaderboard is loading'),
'',
false
)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.displayLeaderboard');
extension
.addAction(
'CloseLeaderboardView',
_('Close current leaderboard'),
_('Close the leaderboard currently displayed on top of the game.'),
_('Close current leaderboard displayed on top of the game'),
_('Display leaderboard'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.addCodeOnlyParameter('currentScene', '')
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName('gdjs.evtTools.leaderboards.closeLeaderboardView');
return extension;
},
runExtensionSanityTests: function (
gd /*: libGDevelop */,
extension /*: gdPlatformExtension*/
) {
return [];
},
};

View File

@@ -0,0 +1,645 @@
/// <reference path="sha256.d.ts" />
namespace gdjs {
const logger = new gdjs.Logger('Leaderboards');
export namespace evtTools {
export namespace leaderboards {
const computeDigest = (payload: string): string => {
const shaObj = new jsSHA('SHA-256', 'TEXT', { encoding: 'UTF8' });
shaObj.update(payload);
return shaObj.getHash('B64');
};
// Score saving
class ScoreSavingState {
lastScoreSavingStartedAt: number | null;
lastScoreSavingSucceededAt: number | null;
currentlySavingScore: number | null;
currentlySavingPlayerName: string | null;
lastSavedScore: number | null;
lastSavedPlayerName: string | null;
lastSaveError: string | null;
isScoreSaving: boolean;
hasScoreBeenSaved: boolean;
hasScoreSavingErrored: boolean;
constructor() {
this.lastScoreSavingStartedAt = null;
this.lastScoreSavingSucceededAt = null;
this.currentlySavingScore = null;
this.currentlySavingPlayerName = null;
this.lastSavedScore = null;
this.lastSavedPlayerName = null;
this.lastSaveError = null;
this.isScoreSaving = false;
this.hasScoreBeenSaved = false;
this.hasScoreSavingErrored = false;
}
isSameAsLastScore(playerName: string, score: number): boolean {
return (
this.lastSavedPlayerName === playerName &&
this.lastSavedScore === score
);
}
isAlreadySavingThisScore(playerName: string, score: number): boolean {
return (
this.isScoreSaving &&
this.currentlySavingPlayerName === playerName &&
this.currentlySavingScore === score
);
}
isTooSoonToSaveAnotherScore(): boolean {
return (
!!this.lastScoreSavingSucceededAt &&
Date.now() - this.lastScoreSavingSucceededAt < 500
);
}
startSaving(playerName: string, score: number): void {
this.lastScoreSavingStartedAt = Date.now();
this.isScoreSaving = true;
this.hasScoreBeenSaved = false;
this.hasScoreSavingErrored = false;
this.currentlySavingScore = score;
this.currentlySavingPlayerName = playerName;
}
closeSaving(): void {
this.lastScoreSavingSucceededAt = Date.now();
this.lastSavedScore = this.currentlySavingScore;
this.lastSavedPlayerName = this.currentlySavingPlayerName;
this.isScoreSaving = false;
this.hasScoreBeenSaved = true;
}
setError(errorCode: string): void {
this.lastSaveError = errorCode;
this.isScoreSaving = false;
this.hasScoreBeenSaved = false;
this.hasScoreSavingErrored = true;
}
}
let _scoreSavingStateByLeaderboard: {
[leaderboardId: string]: ScoreSavingState;
} = {};
// Leaderboard display
let _requestedLeaderboardId: string | null;
let _leaderboardViewIframe: HTMLIFrameElement | null = null;
let _leaderboardViewIframeErrored: boolean = false;
let _leaderboardViewIframeLoading: boolean = false;
let _leaderboardViewIframeLoaded: boolean = false;
let _errorTimeoutId: NodeJS.Timeout | null = null;
let _leaderboardViewClosingCallback:
| ((event: MessageEvent) => void)
| null = null;
const _loaderContainer: HTMLDivElement = document.createElement('div');
_loaderContainer.style.backgroundColor = '#000000';
_loaderContainer.style.display = 'flex';
_loaderContainer.style.height = '100%';
_loaderContainer.style.width = '100%';
_loaderContainer.style.justifyContent = 'center';
_loaderContainer.style.alignItems = 'center';
_loaderContainer.style.position = 'relative';
_loaderContainer.style.zIndex = '2';
const _loader = document.createElement('img');
_loader.setAttribute('width', '50px');
_loader.setAttribute(
'src',
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CjxjaXJjbGUgb3BhY2l0eT0nMC4yNScgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iNCI+PC9jaXJjbGU+CjxwYXRoIG9wYWNpdHk9JzAuNzUnIGZpbGw9IiNGRkZGRkYiIGQ9Ik00IDEyYTggOCAwIDAxOC04VjBDNS4zNzMgMCAwIDUuMzczIDAgMTJoNHptMiA1LjI5MUE3Ljk2MiA3Ljk2MiAwIDAxNCAxMkgwYzAgMy4wNDIgMS4xMzUgNS44MjQgMyA3LjkzOGwzLTIuNjQ3eiI+PC9wYXRoPgo8L3N2Zz4='
);
try {
_loader.animate(
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(359deg)' }],
{
duration: 3000,
iterations: Infinity,
}
);
} catch {
logger.warn('Animation not supported, loader will be fixed.');
}
_loaderContainer.appendChild(_loader);
const getLastScoreSavingState = function ({
hasSucceeded,
}: {
hasSucceeded: boolean;
}): ScoreSavingState | null {
const getDateField = (scoreSavingState: ScoreSavingState) =>
hasSucceeded
? scoreSavingState.lastScoreSavingSucceededAt
: scoreSavingState.lastScoreSavingStartedAt;
const scoreSavingStates = Object.values(
_scoreSavingStateByLeaderboard
).filter((scoreSavingState) => !!getDateField(scoreSavingState));
if (scoreSavingStates.length === 0) return null;
let lastScoreSavingState = scoreSavingStates[0];
scoreSavingStates.forEach((scoreSavingState) => {
const currentItemDate = getDateField(scoreSavingState);
const lastItemDate = getDateField(lastScoreSavingState);
if (
currentItemDate &&
lastItemDate &&
currentItemDate > lastItemDate
) {
lastScoreSavingState = scoreSavingState;
}
});
return lastScoreSavingState;
};
export const savePlayerScore = function (
runtimeScene: gdjs.RuntimeScene,
leaderboardId: string,
score: float,
playerName: string
) {
let scoreSavingState: ScoreSavingState;
if (_scoreSavingStateByLeaderboard[leaderboardId]) {
scoreSavingState = _scoreSavingStateByLeaderboard[leaderboardId];
if (scoreSavingState.isAlreadySavingThisScore(playerName, score)) {
logger.warn(
'There is already a request to save with this player name and this score. Ignoring this one.'
);
return;
}
if (scoreSavingState.isSameAsLastScore(playerName, score)) {
logger.warn(
'The player and score to be sent are the same as previous one. Ignoring this one.'
);
const errorCode = 'SAME_AS_PREVIOUS';
scoreSavingState.setError(errorCode);
return;
}
if (scoreSavingState.isTooSoonToSaveAnotherScore()) {
logger.warn(
'Last entry was sent too little time ago. Ignoring this one.'
);
const errorCode = 'TOO_FAST';
scoreSavingState.setError(errorCode);
return;
}
} else {
scoreSavingState = new ScoreSavingState();
_scoreSavingStateByLeaderboard[leaderboardId] = scoreSavingState;
}
scoreSavingState.startSaving(playerName, score);
const baseUrl = 'https://api.gdevelop-app.com/play';
const game = runtimeScene.getGame();
const payload = JSON.stringify({
playerName: formatPlayerName(playerName),
score: score,
sessionId: game.getSessionId(),
clientPlayerId: game.getPlayerId(),
location:
typeof window !== 'undefined' && (window as any).location
? (window as any).location.href
: '',
});
fetch(
`${baseUrl}/game/${gdjs.projectData.properties.projectUuid}/leaderboard/${leaderboardId}/entry`,
{
body: payload,
method: 'POST',
headers: {
'Content-Type': 'application/json',
Digest: computeDigest(payload),
},
}
).then(
(response) => {
if (!response.ok) {
const errorCode = response.status.toString();
logger.error(
'Server responded with an error:',
errorCode,
response.statusText
);
scoreSavingState.setError(errorCode);
return;
}
scoreSavingState.closeSaving();
return response.text().then(
(text) => {},
(error) => {
logger.warn(
'An error occurred when reading response but score has been saved:',
error
);
}
);
},
(error) => {
logger.error('Error while submitting a leaderboard score:', error);
const errorCode = 'REQUEST_NOT_SENT';
scoreSavingState.setError(errorCode);
}
);
};
export const isSaving = function (leaderboardId?: string): boolean {
if (leaderboardId) {
return _scoreSavingStateByLeaderboard[leaderboardId]
? _scoreSavingStateByLeaderboard[leaderboardId].isScoreSaving
: false;
}
const lastScoreSavingState = getLastScoreSavingState({
hasSucceeded: false,
});
return lastScoreSavingState
? lastScoreSavingState.isScoreSaving
: false;
};
export const hasBeenSaved = function (leaderboardId?: string): boolean {
if (leaderboardId) {
return _scoreSavingStateByLeaderboard[leaderboardId]
? _scoreSavingStateByLeaderboard[leaderboardId].hasScoreBeenSaved
: false;
}
const lastScoreSavingState = getLastScoreSavingState({
hasSucceeded: true,
});
return lastScoreSavingState
? lastScoreSavingState.hasScoreBeenSaved
: false;
};
export const hasSavingErrored = function (
leaderboardId?: string
): boolean {
if (leaderboardId) {
return _scoreSavingStateByLeaderboard[leaderboardId]
? _scoreSavingStateByLeaderboard[leaderboardId]
.hasScoreSavingErrored
: false;
}
const lastScoreSavingState = getLastScoreSavingState({
hasSucceeded: false,
});
return lastScoreSavingState
? lastScoreSavingState.hasScoreSavingErrored
: false;
};
export const getLastSaveError = function (
leaderboardId?: string
): string | null {
if (leaderboardId) {
return _scoreSavingStateByLeaderboard[leaderboardId]
? _scoreSavingStateByLeaderboard[leaderboardId].lastSaveError
: 'NO_DATA_ERROR';
}
const lastScoreSavingState = getLastScoreSavingState({
hasSucceeded: false,
});
return lastScoreSavingState
? lastScoreSavingState.lastSaveError
: 'NO_DATA_ERROR';
};
export const formatPlayerName = function (rawName: string): string {
if (
!rawName ||
typeof rawName !== 'string' ||
(typeof rawName === 'string' && rawName.length === 0)
) {
return `Player${Math.round(
(Math.random() * 9 + 1) * 10000 // Number between 10,000 and 99,999
)}`;
}
return rawName
.trim()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s/g, '_')
.replace(/[^\w|-]/g, '')
.slice(0, 30);
};
const checkLeaderboardAvailability = function (
url: string
): Promise<boolean> {
return fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}).then(
(response) => {
if (!response.ok) {
logger.error(
`Error while fetching leaderboard view, server returned: ${response.status} ${response.statusText}`
);
return false;
}
return true;
},
(err) => {
logger.error('Error while fetching leaderboard view:', err);
return false;
}
);
};
const receiveMessageFromLeaderboardView = function (
runtimeScene: gdjs.RuntimeScene,
displayLoader: boolean,
event: MessageEvent
) {
switch (event.data) {
case 'closeLeaderboardView':
closeLeaderboardView(runtimeScene);
break;
case 'leaderboardViewLoaded':
if (displayLoader) {
if (_errorTimeoutId) clearTimeout(_errorTimeoutId);
displayLoaderInLeaderboardView(false, runtimeScene, {
callOnErrorIfDomElementContainerMissing: false,
});
}
if (!_leaderboardViewIframe) {
handleErrorDisplayingLeaderboard(
runtimeScene,
"The leaderboard view couldn't be found. Doing nothing."
);
return;
}
_leaderboardViewIframe.style.opacity = '1';
_leaderboardViewIframeLoaded = true;
_leaderboardViewIframeLoading = false;
break;
}
};
const handleErrorDisplayingLeaderboard = function (
runtimeScene: gdjs.RuntimeScene,
message: string
) {
logger.error(message);
_leaderboardViewIframeErrored = true;
_leaderboardViewIframeLoading = false;
closeLeaderboardView(runtimeScene);
};
const resetLeaderboardDisplayErrorTimeout = (
runtimeScene: gdjs.RuntimeScene
) => {
if (_errorTimeoutId) clearTimeout(_errorTimeoutId);
_errorTimeoutId = setTimeout(() => {
if (!_leaderboardViewIframeLoaded) {
handleErrorDisplayingLeaderboard(
runtimeScene,
'Leaderboard page did not send message in time. Closing leaderboard view.'
);
}
}, 5000);
};
const displayLoaderInLeaderboardView = function (
yesOrNo: boolean,
runtimeScene: gdjs.RuntimeScene,
options: { callOnErrorIfDomElementContainerMissing: boolean }
): boolean {
const domElementContainer = runtimeScene
.getGame()
.getRenderer()
.getDomElementContainer();
if (!domElementContainer) {
if (options.callOnErrorIfDomElementContainerMissing) {
handleErrorDisplayingLeaderboard(
runtimeScene,
"The div element covering the game couldn't be found, the leaderboard cannot be displayed."
);
}
return false;
}
if (yesOrNo) {
if (
domElementContainer.children &&
domElementContainer.children.length > 0
) {
domElementContainer.insertBefore(
_loaderContainer,
domElementContainer.children[0]
);
} else {
domElementContainer.appendChild(_loaderContainer);
}
if (_leaderboardViewIframe) {
_leaderboardViewIframe.style.opacity = '0';
}
} else {
try {
domElementContainer.removeChild(_loaderContainer);
if (_leaderboardViewIframe) {
_leaderboardViewIframe.style.opacity = '1';
}
} catch {}
}
return true;
};
const computeLeaderboardDisplayingIframe = function (
url: string
): HTMLIFrameElement {
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.id = 'leaderboard-view';
iframe.style.position = 'absolute';
// To trigger iframe loading and be able to listen to its events, use `opacity: 0` instead of `visibility: hidden` or `display: none`
iframe.style.opacity = '0';
iframe.style.pointerEvents = 'all';
iframe.style.backgroundColor = '#FFFFFF';
iframe.style.top = '0px';
iframe.style.height = '100%';
iframe.style.left = '0px';
iframe.style.width = '100%';
iframe.style.border = 'none';
return iframe;
};
export const displayLeaderboard = function (
runtimeScene: gdjs.RuntimeScene,
leaderboardId: string,
displayLoader: boolean
) {
// First ensure we're not trying to display multiple times the same leaderboard (in which case
// we "de-duplicate" the request to display it).
if (leaderboardId === _requestedLeaderboardId) {
if (_leaderboardViewIframeLoading) {
logger.warn(
`Already loading the view for the requested loader (${leaderboardId}), ignoring.`
);
return;
}
if (_leaderboardViewIframeLoaded) {
logger.warn(
`Already loaded the view for the requested loader (${leaderboardId}), ignoring.`
);
return;
}
}
// We are now assured we want to display a new (or different) leaderboard: start loading it.
_requestedLeaderboardId = leaderboardId;
_leaderboardViewIframeErrored = false;
_leaderboardViewIframeLoaded = false;
_leaderboardViewIframeLoading = true;
if (displayLoader) {
displayLoaderInLeaderboardView(true, runtimeScene, {
callOnErrorIfDomElementContainerMissing: true,
});
}
const gameId = gdjs.projectData.properties.projectUuid;
const targetUrl = `https://liluo.io/games/${gameId}/leaderboard/${leaderboardId}?inGameEmbedded=true`;
checkLeaderboardAvailability(targetUrl).then(
(isAvailable) => {
if (leaderboardId !== _requestedLeaderboardId) {
logger.warn(
`Received a response for leaderboard ${leaderboardId} though the last leaderboard requested is ${_requestedLeaderboardId}, ignoring this response.`
);
return;
}
if (!isAvailable) {
handleErrorDisplayingLeaderboard(
runtimeScene,
'Leaderboard data could not be fetched. Closing leaderboard view if there is one.'
);
return;
}
if (_leaderboardViewIframe) {
resetLeaderboardDisplayErrorTimeout(runtimeScene);
if (displayLoader) {
displayLoaderInLeaderboardView(true, runtimeScene, {
callOnErrorIfDomElementContainerMissing: false,
});
}
_leaderboardViewIframe.src = targetUrl;
} else {
const domElementContainer = runtimeScene
.getGame()
.getRenderer()
.getDomElementContainer();
if (!domElementContainer) {
handleErrorDisplayingLeaderboard(
runtimeScene,
"The div element covering the game couldn't be found, the leaderboard cannot be displayed."
);
return;
}
resetLeaderboardDisplayErrorTimeout(runtimeScene);
_leaderboardViewIframe = computeLeaderboardDisplayingIframe(
targetUrl
);
if (typeof window !== 'undefined') {
_leaderboardViewClosingCallback = (event: MessageEvent) => {
receiveMessageFromLeaderboardView(
runtimeScene,
displayLoader,
event
);
};
(window as any).addEventListener(
'message',
_leaderboardViewClosingCallback,
true
);
}
domElementContainer.appendChild(_leaderboardViewIframe);
}
},
(err) => {
logger.error(err);
handleErrorDisplayingLeaderboard(
runtimeScene,
'An error occurred when fetching leaderboard data. Closing leaderboard view if there is one.'
);
return;
}
);
};
export const isLeaderboardViewErrored = function (): boolean {
return _leaderboardViewIframeErrored;
};
export const isLeaderboardViewLoaded = function (): boolean {
return _leaderboardViewIframeLoaded;
};
export const isLeaderboardViewLoading = function (): boolean {
return _leaderboardViewIframeLoading;
};
export const closeLeaderboardView = function (
runtimeScene: gdjs.RuntimeScene
) {
try {
displayLoaderInLeaderboardView(false, runtimeScene, {
callOnErrorIfDomElementContainerMissing: false,
});
if (!_leaderboardViewIframe) {
logger.info(
"The iframe displaying the current leaderboard couldn't be found, the leaderboard view must be already closed."
);
return;
}
const domElementContainer = runtimeScene
.getGame()
.getRenderer()
.getDomElementContainer();
if (!domElementContainer) {
logger.info(
"The div element covering the game couldn't be found, the leaderboard view must be already closed."
);
return;
}
if (typeof window !== 'undefined') {
(window as any).removeEventListener(
'message',
_leaderboardViewClosingCallback,
true
);
_leaderboardViewClosingCallback = null;
}
domElementContainer.removeChild(_leaderboardViewIframe);
_leaderboardViewIframe = null;
} finally {
// Don't reset the loading flag (the view of another leaderboard might be loading)
// or the error flag (we want to persist the error flag even after the view is closed),
// but reset the flag indicating the view is loaded (if it was).
_leaderboardViewIframeLoaded = false;
const gameCanvas = runtimeScene.getGame().getRenderer().getCanvas();
if (gameCanvas) gameCanvas.focus();
}
};
}
}
}

234
Extensions/Leaderboards/sha256.d.ts vendored Normal file
View File

@@ -0,0 +1,234 @@
declare type EncodingType = 'UTF8' | 'UTF16BE' | 'UTF16LE';
declare type FormatNoTextType =
| 'HEX'
| 'B64'
| 'BYTES'
| 'ARRAYBUFFER'
| 'UINT8ARRAY';
declare type FormatType = 'TEXT' | FormatNoTextType;
declare type GenericInputType =
| {
value: string;
format: 'TEXT';
encoding?: EncodingType;
}
| {
value: string;
format: 'B64' | 'HEX' | 'BYTES';
}
| {
value: ArrayBuffer;
format: 'ARRAYBUFFER';
}
| {
value: Uint8Array;
format: 'UINT8ARRAY';
};
declare type FixedLengthOptionsNoEncodingType =
| {
hmacKey?: GenericInputType;
}
| {
numRounds?: number;
};
declare type FixedLengthOptionsEncodingType =
| {
hmacKey?: GenericInputType;
encoding?: EncodingType;
}
| {
numRounds?: number;
encoding?: EncodingType;
};
interface packedValue {
value: number[];
binLen: number;
}
declare abstract class jsSHABase<StateT, VariantT> {
/**
* @param variant The desired SHA variant.
* @param inputFormat The input format to be used in future `update` calls.
* @param options Hashmap of extra input options.
*/
protected readonly shaVariant: VariantT;
protected readonly inputFormat: FormatType;
protected readonly utfType: EncodingType;
protected readonly numRounds: number;
protected abstract intermediateState: StateT;
protected keyWithIPad: number[];
protected keyWithOPad: number[];
protected remainder: number[];
protected remainderLen: number;
protected updateCalled: boolean;
protected processedLen: number;
protected macKeySet: boolean;
protected abstract readonly variantBlockSize: number;
protected abstract readonly bigEndianMod: -1 | 1;
protected abstract readonly outputBinLen: number;
protected abstract readonly isVariableLen: boolean;
protected abstract readonly HMACSupported: boolean;
protected abstract readonly converterFunc: (
input: any,
existingBin: number[],
existingBinLen: number
) => packedValue;
protected abstract readonly roundFunc: (block: number[], H: StateT) => StateT;
protected abstract readonly finalizeFunc: (
remainder: number[],
remainderBinLen: number,
processedBinLen: number,
H: StateT,
outputLen: number
) => number[];
protected abstract readonly stateCloneFunc: (state: StateT) => StateT;
protected abstract readonly newStateFunc: (variant: VariantT) => StateT;
protected abstract readonly getMAC:
| ((options: { outputLen: number }) => number[])
| null;
protected constructor(
variant: VariantT,
inputFormat: 'TEXT',
options?: FixedLengthOptionsEncodingType
);
protected constructor(
variant: VariantT,
inputFormat: FormatNoTextType,
options?: FixedLengthOptionsNoEncodingType
);
/**
* Hashes as many blocks as possible. Stores the rest for either a future update or getHash call.
*
* @param srcString The input to be hashed.
*/
update(srcString: string | ArrayBuffer | Uint8Array): void;
/**
* Returns the desired SHA hash of the input fed in via `update` calls.
*
* @param format The desired output formatting
* @param options Hashmap of output formatting options. `outputLen` must be specified for variable length hashes.
* `outputLen` replaces the now deprecated `shakeLen` key.
* @returns The hash in the format specified.
*/
getHash(
format: 'HEX',
options?: {
outputUpper?: boolean;
outputLen?: number;
shakeLen?: number;
}
): string;
getHash(
format: 'B64',
options?: {
b64Pad?: string;
outputLen?: number;
shakeLen?: number;
}
): string;
getHash(
format: 'BYTES',
options?: {
outputLen?: number;
shakeLen?: number;
}
): string;
getHash(
format: 'UINT8ARRAY',
options?: {
outputLen?: number;
shakeLen?: number;
}
): Uint8Array;
getHash(
format: 'ARRAYBUFFER',
options?: {
outputLen?: number;
shakeLen?: number;
}
): ArrayBuffer;
/**
* Sets the HMAC key for an eventual `getHMAC` call. Must be called immediately after jsSHA object instantiation.
*
* @param key The key used to calculate the HMAC
* @param inputFormat The format of key.
* @param options Hashmap of extra input options.
*/
setHMACKey(
key: string,
inputFormat: 'TEXT',
options?: {
encoding?: EncodingType;
}
): void;
setHMACKey(key: string, inputFormat: 'B64' | 'HEX' | 'BYTES'): void;
setHMACKey(key: ArrayBuffer, inputFormat: 'ARRAYBUFFER'): void;
setHMACKey(key: Uint8Array, inputFormat: 'UINT8ARRAY'): void;
/**
* Internal function that sets the MAC key.
*
* @param key The packed MAC key to use
*/
protected _setHMACKey(key: packedValue): void;
/**
* Returns the the HMAC in the specified format using the key given by a previous `setHMACKey` call.
*
* @param format The desired output formatting.
* @param options Hashmap of extra outputs options.
* @returns The HMAC in the format specified.
*/
getHMAC(
format: 'HEX',
options?: {
outputUpper?: boolean;
}
): string;
getHMAC(
format: 'B64',
options?: {
b64Pad?: string;
}
): string;
getHMAC(format: 'BYTES'): string;
getHMAC(format: 'UINT8ARRAY'): Uint8Array;
getHMAC(format: 'ARRAYBUFFER'): ArrayBuffer;
/**
* Internal function that returns the "raw" HMAC
*/
protected _getHMAC(): number[];
}
declare type VariantType = 'SHA-224' | 'SHA-256';
declare class jsSHA extends jsSHABase<number[], VariantType> {
intermediateState: number[];
variantBlockSize: number;
bigEndianMod: -1 | 1;
outputBinLen: number;
isVariableLen: boolean;
HMACSupported: boolean;
converterFunc: (
input: any,
existingBin: number[],
existingBinLen: number
) => packedValue;
roundFunc: (block: number[], H: number[]) => number[];
finalizeFunc: (
remainder: number[],
remainderBinLen: number,
processedBinLen: number,
H: number[]
) => number[];
stateCloneFunc: (state: number[]) => number[];
newStateFunc: (variant: VariantType) => number[];
getMAC: () => number[];
constructor(
variant: VariantType,
inputFormat: 'TEXT',
options?: FixedLengthOptionsEncodingType
);
constructor(
variant: VariantType,
inputFormat: FormatNoTextType,
options?: FixedLengthOptionsNoEncodingType
);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,68 @@
// @ts-check
describe('Leaderboards', () => {
describe('formatPlayerName', () => {
it('it returns name if correct', () => {
expect(gdjs.evtTools.leaderboards.formatPlayerName('PlayerName')).to.be(
'PlayerName'
);
});
it('it returns name with underscores instead of whitespaces except for leading and trailing ones that are removed', () => {
expect(
gdjs.evtTools.leaderboards.formatPlayerName('\tMy Player Name ')
).to.be('My_Player_Name');
});
it("it doesn't change a name with vertical bars and hyphens", () => {
expect(gdjs.evtTools.leaderboards.formatPlayerName('Pla-yer|Name')).to.be(
'Pla-yer|Name'
);
});
it('it truncates name if longer than 30', () => {
expect(
gdjs.evtTools.leaderboards.formatPlayerName(
'aPlayerNameTh4tIsT00LongToBeSaved'
)
).to.be('aPlayerNameTh4tIsT00LongToBeSa');
});
it('it generates a predefined player name with a random number if input is void/wrong type/empty', () => {
// @ts-ignore
expect(gdjs.evtTools.leaderboards.formatPlayerName(null)).to.match(
/^Player\d{5}/
);
// @ts-ignore
expect(gdjs.evtTools.leaderboards.formatPlayerName(5)).to.match(
/^Player\d{5}/
);
// @ts-ignore
expect(gdjs.evtTools.leaderboards.formatPlayerName(undefined)).to.match(
/^Player\d{5}/
);
// @ts-ignore
expect(gdjs.evtTools.leaderboards.formatPlayerName(() => {})).to.match(
/^Player\d{5}/
);
// @ts-ignore
expect(gdjs.evtTools.leaderboards.formatPlayerName('')).to.match(
/^Player\d{5}/
);
});
it('it removes accents from latin letters', () => {
expect(gdjs.evtTools.leaderboards.formatPlayerName('plâyèrÏonisé')).to.be(
'playerIonise'
);
});
it('it removes non-accepted characters in a long name', () => {
expect(
gdjs.evtTools.leaderboards.formatPlayerName(
'aιΥÉᚱÀeThatsTooonToBeՏaѵÊĐThisPartAppears'
)
).to.be('aEAeThatsTooonToBeaEThisPartAp');
});
});
});

View File

@@ -16,30 +16,30 @@ This project is released under the MIT License.
*/
void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddAction("ParticleColor1",
_("Initial color"),
_("Modify initial color of particles."),
_("Put initial color of particles of _PARAM0_ to _PARAM1_"),
_("Start color"),
_("Modify start color of particles."),
_("Change particles start color of _PARAM0_ to _PARAM1_"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter")
.AddParameter("color", _("Initial color"));
.AddParameter("color", _("Start color"));
obj.AddAction("ParticleColor2",
_("Final color"),
_("Modify final color of particles."),
_("Put final color of particles of _PARAM0_ to _PARAM1_"),
_("End color"),
_("Modify end color of particles."),
_("Change particles end color of _PARAM0_ to _PARAM1_"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter")
.AddParameter("color", _("Final color"));
.AddParameter("color", _("End color"));
obj.AddAction(
"ParticleRed1",
_("Red color, parameter 1"),
_("Modify parameter 1 of the red color."),
_("the parameter 1 of red color"),
_("Start color red component"),
_("Modify the start color red component."),
_("the start color red component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -48,9 +48,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition(
"ParticleRed1",
_("Red color, parameter 1"),
_("Test parameter 1 of the red color"),
_("the parameter 1 of red color"),
_("Start color red component"),
_("Compare the start color red component."),
_("the start color red component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -59,9 +59,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddAction(
"ParticleRed2",
_("Red color, parameter 2"),
_("Modify parameter 2 of the red color"),
_("the parameter 2 of red color"),
_("End color red component"),
_("Modify the end color red component."),
_("the end color red component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -70,9 +70,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition(
"ParticleRed2",
_("Red color, parameter 2"),
_("Test parameter 2 of the red color"),
_("the parameter 2 of red color"),
_("End color red component"),
_("Compare the end color red component."),
_("the end color red component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -81,9 +81,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddAction(
"ParticleBlue1",
_("Blue color, parameter 1"),
_("Modify parameter 1 of blue color"),
_("the parameter 1 of blue color"),
_("Start color blue component"),
_("Modify the start color blue component."),
_("the start color blue component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -92,9 +92,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition(
"ParticleBlue1",
_("Blue color, parameter 1"),
_("Test parameter 1 of blue color"),
_("the parameter 1 of blue color"),
_("Start color blue component"),
_("Compare the start color blue component."),
_("the start color blue component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -103,9 +103,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddAction(
"ParticleBlue2",
_("Blue color, parameter 2"),
_("Modify parameter 2 of blue color"),
_("the parameter 2 of blue color"),
_("End color blue component"),
_("Modify the end color blue component."),
_("the end color blue component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -114,9 +114,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition(
"ParticleBlue2",
_("Blue color, parameter 2"),
_("Test parameter 2 of blue color"),
_("the parameter 2 of blue color"),
_("End color blue component"),
_("Compare the end color blue component."),
_("the end color blue component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -125,9 +125,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddAction(
"ParticleGreen1",
_("Green color, parameter 1"),
_("Modify parameter 1 of green color"),
_("the parameter 1 of green color"),
_("Start color green component"),
_("Modify the start color green component."),
_("the start color green component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -136,9 +136,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition(
"ParticleGreen1",
_("Green color, parameter 1"),
_("Test parameter 1 of green color"),
_("the parameter 1 of green color"),
_("Start color green component"),
_("Compare the start color green component."),
_("the start color green component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -147,9 +147,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddAction(
"ParticleGreen2",
_("Green color, parameter 2"),
_("Modify the parameter 2 of the green color"),
_("the parameter 2 of green color"),
_("End color green component"),
_("Modify the end color green component."),
_("the end color green component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -158,9 +158,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition(
"ParticleGreen2",
_("Green color, parameter 2"),
_("Test the parameter 2 of the green color"),
_("the parameter 2 of green color"),
_("End color green component"),
_("Compare the end color green component."),
_("the end color green component"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -168,9 +168,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
.UseStandardRelationalOperatorParameters("number");
obj.AddAction("ParticleSize1",
_("Size, parameter 1"),
_("Modify parameter 1 of the size of particles"),
_("the parameter 1 of size"),
_("Start size"),
_("Modify the particle start size."),
_("the start size"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -178,9 +178,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
.UseStandardOperatorParameters("number");
obj.AddCondition("ParticleSize1",
_("Size, parameter 1"),
_("Test parameter 1 of the size of particles"),
_("the parameter 1 of the size"),
_("Start size"),
_("Compare the particle start size."),
_("the start size"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -188,9 +188,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
.UseStandardRelationalOperatorParameters("number");
obj.AddAction("ParticleSize2",
_("Size, parameter 2"),
_("Modify parameter 2 of the size of particles"),
_("the parameter 2 of size"),
_("End size"),
_("Modify the particle end size."),
_("the end size"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -198,9 +198,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
.UseStandardOperatorParameters("number");
obj.AddCondition("ParticleSize2",
_("Size, parameter 2"),
_("Test parameter 2 of the size of particles"),
_("the parameter 2 of the size"),
_("End size"),
_("Compare the particle end size."),
_("the end size"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -210,7 +210,7 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddAction(
"ParticleAngle1",
_("Angle, parameter 1"),
_("Modify parameter 1 of the angle of particles"),
_("Modify parameter 1 of the angle of particles."),
_("the parameter 1 of angle"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
@@ -220,7 +220,7 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition("ParticleAngle1",
_("Angle, parameter 1"),
_("Test parameter 1 of the angle of particles"),
_("Compare parameter 1 of the angle of particles."),
_("the parameter 1 of angle"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
@@ -241,7 +241,7 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition("ParticleAngle2",
_("Angle, parameter 2"),
_("Test parameter 2 of the angle of particles"),
_("Compare parameter 2 of the angle of particles."),
_("the parameter 2 of angle"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
@@ -250,9 +250,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
.UseStandardRelationalOperatorParameters("number");
obj.AddAction("ParticleAlpha1",
_("Transparency, parameter 1"),
_("Modify parameter 1 of the transparency of particles"),
_("the parameter 1 of the transparency"),
_("Start opacity"),
_("Modify the start opacity of particles."),
_("the start opacity"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -261,9 +261,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition(
"ParticleAlpha1",
_("Transparency, parameter 1"),
_("Test parameter 1 of the transparency of particles"),
_("the parameter 1 of the transparency"),
_("Start opacity"),
_("Compare the start opacity of particles."),
_("the start opacity"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -271,9 +271,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
.UseStandardRelationalOperatorParameters("number");
obj.AddAction("ParticleAlpha2",
_("Transparency, parameter 2"),
_("Modify parameter 2 of the transparency of particles"),
_("the parameter 2 of the transparency"),
_("End opacity"),
_("Modify the end opacity of particles."),
_("the end opacity"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
@@ -282,9 +282,9 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
obj.AddCondition(
"ParticleAlpha2",
_("Transparency, parameter 2"),
_("Test parameter 2 of the transparency of particles"),
_("the parameter 2 of the transparency"),
_("Start opacity"),
_("Compare the end opacity of particles."),
_("the end opacity"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")

View File

@@ -28,7 +28,7 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
obj.AddAction(
"RendererParam1",
_("Rendering first parameter"),
_("Modify first parameter of rendering ( Size/Length ).\nParticles "
_("Modify first parameter of rendering (Size/Length).\nParticles "
"have to be recreated in order to take changes in account."),
_("the rendering 1st parameter"),
_("Setup"),
@@ -40,7 +40,7 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
obj.AddCondition(
"RendererParam1",
_("Rendering first parameter"),
_("Test the first parameter of rendering ( Size/Length )."),
_("Test the first parameter of rendering (Size/Length)."),
_("the 1st rendering parameter"),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon24.png",
@@ -51,7 +51,7 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
obj.AddAction("RendererParam2",
_("Rendering second parameter"),
_("Modify the second parameter of rendering ( Size/Length "
_("Modify the second parameter of rendering (Size/Length"
").\nParticles have to be recreated in order to take changes "
"in account."),
_("the rendering 2nd parameter"),
@@ -64,7 +64,7 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
obj.AddCondition(
"RendererParam2",
_("Rendering second parameter"),
_("Test the second parameter of rendering ( Size/Length )."),
_("Test the second parameter of rendering (Size/Length)."),
_("the 2nd rendering parameter"),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon24.png",
@@ -135,14 +135,25 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
.UseStandardRelationalOperatorParameters("number");
obj.AddAction("Texture",
_("Image"),
_("Change the image of particles ( if displayed )."),
_("Change image (using an expression)"),
_("Change the image of particles (if displayed)."),
_("Change the image of particles of _PARAM0_ to _PARAM1_"),
_("Advanced"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter")
.AddParameter("string", _("Image to use"))
.SetParameterLongDescription("Indicate the name of the resource");
obj.AddAction("SetTextureFromResource",
_("Change image"),
_("Change the image of particles (if displayed)."),
_("Change the image of particles of _PARAM0_ to _PARAM1_"),
_("Common"),
"CppPlatform/Extensions/particleSystemicon24.png",
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter")
.AddParameter("string", _("New image"));
.AddParameter("imageResource", _("Image file (or image resource name)"));
obj.AddCondition(
"Texture",
@@ -157,7 +168,7 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
obj.AddStrExpression("Texture",
_("Particles image"),
_("Name of the image displayed by particles"),
_("Name of the image displayed by particles."),
_("Particles"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
@@ -172,7 +183,7 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
obj.AddExpression("CurrentParticleCount",
_("Particles count"),
_("Number of particles currently displayed"),
_("Number of particles currently displayed."),
_("Particles"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
@@ -193,35 +204,35 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
obj.AddExpression("Tank",
_("Capacity"),
_("Capacity"),
_("Capacity of the particle tank."),
_("Common"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("Flow",
_("Flow"),
_("Flow"),
_("Flow of the particles (particles/second)."),
_("Common"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("EmitterForceMin",
_("Emission minimal force"),
_("Emission minimal force"),
_("The minimal emission force of the particles."),
_("Common"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("EmitterForceMax",
_("Emission maximal force"),
_("Emission maximal force"),
_("The maximal emission force of the particles."),
_("Common"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("EmitterAngle",
_("Emission angle"),
_("Emission angle"),
_("Emission angle of the particles."),
_("Common"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
@@ -238,104 +249,104 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ZoneRadius",
_("Radius of the emission zone"),
_("Radius of the emission zone"),
_("Radius of emission zone"),
_("The radius of the emission zone."),
_("Common"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleGravityX",
_("X Gravity of particles"),
_("X Gravity of particles"),
_("X gravity"),
_("Gravity of particles applied on X-axis."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleGravityY",
_("Y Gravity of particles"),
_("Y Gravity of particles"),
_("Y gravity"),
_("Gravity of particles applied on Y-axis."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleGravityAngle",
_("Gravity angle"),
_("Gravity angle"),
_("Angle of gravity."),
_("Common"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleGravityLength",
_("Gravity"),
_("Gravity value"),
_("Value of gravity."),
_("Common"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleLifeTimeMin",
_("Minimum lifetime of particles"),
_("Minimum lifetime of particles"),
_("Minimum lifetime of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleLifeTimeMax",
_("Maximum lifetime of particles"),
_("Maximum lifetime of particles"),
_("Maximum lifetime of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleRed1",
_("Parameter 1 of red color"),
_("Parameter 1 of red color"),
_("Start color red component"),
_("The start color red component of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleRed2",
_("Parameter 2 of red color"),
_("Parameter 2 of red color"),
_("End color red component"),
_("The end color red component of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleBlue1",
_("Parameter 1 of blue color"),
_("Parameter 1 of blue color"),
_("Start color blue component"),
_("The start color blue component of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleBlue2",
_("Parameter 2 of blue color"),
_("Parameter 2 of blue color"),
_("End color blue component"),
_("The end color blue component of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleGreen1",
_("Parameter 1 of green color"),
_("Parameter 1 of green color"),
_("Start color green component"),
_("The start color green component of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleGreen2",
_("Parameter 2 of green color"),
_("Parameter 2 of green color"),
_("End color green component"),
_("The end color green component of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleAlpha1",
_("Parameter 1 of transparency"),
_("Parameter 1 of transparency"),
_("Start opacity"),
_("Start opacity of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleAlpha2",
_("Parameter 2 of transparency"),
_("Parameter 2 of transparency"),
_("End opacity"),
_("End opacity of the particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleSize1",
_("Parameter 1 of size"),
_("Parameter 1 of size"),
_("Start size"),
_("Start size of particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);
obj.AddExpression("ParticleSize2",
_("Parameter 2 of size"),
_("Parameter 2 of size"),
_("End size"),
_("End size of particles."),
_("Setup"),
"CppPlatform/Extensions/particleSystemicon16.png")
.AddParameter("object", _("Object"), "ParticleEmitter", false);

View File

@@ -169,6 +169,10 @@ class ParticleSystemJsExtension : public gd::PlatformExtension {
actions["ParticleSystem::Flow"].SetFunctionName("setFlow").SetGetter(
"getFlow");
conditions["ParticleSystem::Flow"].SetFunctionName("getFlow");
actions["ParticleSystem::SetTextureFromResource"]
.AddCodeOnlyParameter("currentScene", "")
.SetFunctionName("setTexture")
.SetGetter("getTexture");
actions["ParticleSystem::Texture"]
.AddCodeOnlyParameter("currentScene", "")
.SetFunctionName("setTexture")

View File

@@ -537,7 +537,7 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
aut.AddScopedCondition("IsUsingControl",
_("Control pressed or simulated"),
_("A control was applied from a default control or a simulated by an action."),
_("A control was applied from a default control or simulated by an action."),
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
_(""),
"res/conditions/keyboard24.png",

View File

@@ -578,12 +578,6 @@ module.exports = {
const TEXT_MASK_PADDING = 2;
class RenderedTextInputObjectInstance extends RenderedInstance {
_pixiText;
_pixiTextMask;
_pixiGraphics;
_fontResourceName = '';
_finalTextColor = 0x0;
constructor(
project,
layout,
@@ -601,6 +595,8 @@ module.exports = {
pixiResourcesLoader
);
this._fontResourceName = '';
this._finalTextColor = 0x0;
this._pixiGraphics = new PIXI.Graphics();
this._pixiTextMask = new PIXI.Graphics();
this._pixiText = new PIXI.Text(' ', {

View File

@@ -145,6 +145,28 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
.SetIncludeFile(
"TopDownMovementBehavior/TopDownMovementRuntimeBehavior.h");
aut.AddScopedCondition("IsUsingControl",
_("Control pressed or simulated"),
_("A control was applied from a default control or simulated by an action."),
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
_("Controls"),
"res/conditions/keyboard24.png",
"res/conditions/keyboard.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
.AddParameter("stringWithSelector",
_("Key"),
"[\"Left\", \"Right\", \"Up\", \"Down\", \"Stick\"]")
.MarkAsAdvanced();
aut.AddExpression("StickAngle",
_("Stick angle"),
_("Return the angle of the simulated stick input (in degrees)"),
_("Controls"),
"CppPlatform/Extensions/topdownmovementicon16.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior");
aut.AddCondition("IsMoving",
_("Is moving"),
_("Check if the object is moving."),
@@ -532,6 +554,30 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
.SetIncludeFile(
"TopDownMovementBehavior/TopDownMovementRuntimeBehavior.h");
aut.AddScopedAction("SetVelocityX",
_("Speed on the X axis"),
_("Change the speed on the X axis of the movement"),
_("the speed on the X axis of the movement"),
_("Movement"),
"CppPlatform/Extensions/topdownmovementicon24.png",
"CppPlatform/Extensions/topdownmovementicon16.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
.UseStandardOperatorParameters("number")
.MarkAsAdvanced();
aut.AddScopedAction("SetVelocityY",
_("Speed on the Y axis"),
_("Change the speed on the Y axis of the movement"),
_("the speed on the Y axis of the movement"),
_("Movement"),
"CppPlatform/Extensions/topdownmovementicon24.png",
"CppPlatform/Extensions/topdownmovementicon16.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
.UseStandardOperatorParameters("number")
.MarkAsAdvanced();
aut.AddExpressionAndConditionAndAction("number",
"MovementAngleOffset",
_("Movement angle offset"),

View File

@@ -106,6 +106,9 @@ class TopDownMovementBehaviorJsExtension : public gd::PlatformExtension {
.SetFunctionName("ignoreDefaultControls");
autActions["TopDownMovementBehavior::SimulateStick"].SetFunctionName(
"simulateStick");
autConditions["TopDownMovementBehavior::TopDownMovementBehavior::IsUsingControl"].SetFunctionName(
"isUsingControl");
autExpressions["StickAngle"].SetFunctionName("getLastStickInputAngle");
autExpressions["Acceleration"].SetFunctionName("getAcceleration");
autExpressions["Deceleration"].SetFunctionName("getDeceleration");
@@ -116,6 +119,12 @@ class TopDownMovementBehaviorJsExtension : public gd::PlatformExtension {
autExpressions["Angle"].SetFunctionName("getAngle");
autExpressions["XVelocity"].SetFunctionName("getXVelocity");
autExpressions["YVelocity"].SetFunctionName("getYVelocity");
autActions["TopDownMovementBehavior::TopDownMovementBehavior::SetVelocityX"]
.SetFunctionName("setXVelocity")
.SetGetter("getXVelocity");
autActions["TopDownMovementBehavior::TopDownMovementBehavior::SetVelocityY"]
.SetFunctionName("setYVelocity")
.SetGetter("getYVelocity");
autExpressions["MovementAngleOffset"].SetFunctionName(
"getMovementAngleOffset");

View File

@@ -99,6 +99,10 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
player.getBehavior(topDownName).simulateDownKey();
}
runtimeScene.renderAndStep(1000 / 60);
expect(
player.getBehavior(topDownName).getXVelocity()
).to.be.above(0);
expect(player.getBehavior(topDownName).getYVelocity()).to.be(0);
}
expect(player.getX()).to.be.above(200 + 20);
@@ -120,6 +124,10 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
player.getBehavior(topDownName).simulateUpKey();
}
runtimeScene.renderAndStep(1000 / 60);
expect(
player.getBehavior(topDownName).getXVelocity()
).to.be.below(0);
expect(player.getBehavior(topDownName).getYVelocity()).to.be(0);
}
expect(player.getX()).to.be.below(200 - 20);
@@ -141,6 +149,10 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
player.getBehavior(topDownName).simulateLeftKey();
}
runtimeScene.renderAndStep(1000 / 60);
expect(player.getBehavior(topDownName).getXVelocity()).to.be(0);
expect(
player.getBehavior(topDownName).getYVelocity()
).to.be.above(0);
}
expect(player.getX()).to.be(200);
@@ -162,6 +174,10 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
player.getBehavior(topDownName).simulateLeftKey();
}
runtimeScene.renderAndStep(1000 / 60);
expect(player.getBehavior(topDownName).getXVelocity()).to.be(0);
expect(
player.getBehavior(topDownName).getYVelocity()
).to.be.below(0);
}
expect(player.getX()).to.be(200);

View File

@@ -35,10 +35,11 @@ namespace gdjs {
private _rightKey: boolean = false;
private _upKey: boolean = false;
private _downKey: boolean = false;
private _leftKeyPressedDuration: integer = -1;
private _rightKeyPressedDuration: integer = -1;
private _upKeyPressedDuration: integer = -1;
private _downKeyPressedDuration: integer = -1;
private _leftKeyPressedDuration: float = 0;
private _rightKeyPressedDuration: float = 0;
private _upKeyPressedDuration: float = 0;
private _downKeyPressedDuration: float = 0;
private _wasStickUsed: boolean = false;
private _stickAngle: float = 0;
private _stickForce: float = 0;
@@ -192,20 +193,28 @@ namespace gdjs {
return this._xVelocity !== 0 || this._yVelocity !== 0;
}
getSpeed() {
getSpeed(): float {
return Math.sqrt(
this._xVelocity * this._xVelocity + this._yVelocity * this._yVelocity
);
}
getXVelocity() {
getXVelocity(): float {
return this._xVelocity;
}
getYVelocity() {
setXVelocity(velocityX: float): void {
this._xVelocity = velocityX;
}
getYVelocity(): float {
return this._yVelocity;
}
setYVelocity(velocityY: float): void {
this._yVelocity = velocityY;
}
getAngle(): float {
return this._angle;
}
@@ -242,31 +251,31 @@ namespace gdjs {
!this._ignoreDefaultControls &&
runtimeScene.getGame().getInputManager().isKeyPressed(UPKEY);
const elapsedTime = this.owner.getElapsedTime(runtimeScene);
if (!this._leftKey) {
this._leftKeyPressedDuration = 0;
} else {
this._leftKeyPressedDuration += elapsedTime;
}
if (!this._rightKey) {
this._rightKeyPressedDuration = 0;
} else {
this._rightKeyPressedDuration += elapsedTime;
}
if (!this._downKey) {
this._downKeyPressedDuration = 0;
} else {
this._downKeyPressedDuration += elapsedTime;
}
if (!this._upKey) {
this._upKeyPressedDuration = 0;
} else {
this._upKeyPressedDuration += elapsedTime;
}
let direction = -1;
if (!this._allowDiagonals) {
const elapsedTime = this.owner.getElapsedTime(runtimeScene);
if (!this._leftKey) {
this._leftKeyPressedDuration = 0;
} else {
this._leftKeyPressedDuration += elapsedTime;
}
if (!this._rightKey) {
this._rightKeyPressedDuration = 0;
} else {
this._rightKeyPressedDuration += elapsedTime;
}
if (!this._downKey) {
this._downKeyPressedDuration = 0;
} else {
this._downKeyPressedDuration += elapsedTime;
}
if (!this._upKey) {
this._upKeyPressedDuration = 0;
} else {
this._upKeyPressedDuration += elapsedTime;
}
if (this._upKey && !this._downKey) {
direction = 6;
} else if (!this._upKey && this._downKey) {
@@ -322,20 +331,34 @@ namespace gdjs {
const object = this.owner;
const timeDelta = this.owner.getElapsedTime(runtimeScene) / 1000;
let directionInRad = 0;
let directionInDeg = 0;
const previousVelocityX = this._xVelocity;
const previousVelocityY = this._yVelocity;
this._wasStickUsed = false;
// These 4 values are not actually used.
// JavaScript doesn't allow to declare
// variables without assigning them a value.
let directionInRad = 0;
let directionInDeg = 0;
let cos = 1;
let sin = 0;
// Update the speed of the object:
if (direction !== -1) {
directionInRad =
((direction + this._movementAngleOffset / 45) * Math.PI) / 4.0;
directionInDeg = direction * 45 + this._movementAngleOffset;
this._xVelocity +=
this._acceleration * timeDelta * Math.cos(directionInRad);
this._yVelocity +=
this._acceleration * timeDelta * Math.sin(directionInRad);
// This makes the trigo resilient to rounding errors on directionInRad.
cos = Math.cos(directionInRad);
sin = Math.sin(directionInRad);
if (cos === -1 || cos === 1) {
sin = 0;
}
if (sin === -1 || sin === 1) {
cos = 0;
}
this._xVelocity += this._acceleration * timeDelta * cos;
this._yVelocity += this._acceleration * timeDelta * sin;
} else if (this._stickForce !== 0) {
if (!this._allowDiagonals) {
this._stickAngle = 90 * Math.floor((this._stickAngle + 45) / 90);
@@ -343,19 +366,36 @@ namespace gdjs {
directionInDeg = this._stickAngle + this._movementAngleOffset;
directionInRad = (directionInDeg * Math.PI) / 180;
const norm = this._acceleration * timeDelta * this._stickForce;
this._xVelocity += norm * Math.cos(directionInRad);
this._yVelocity += norm * Math.sin(directionInRad);
// This makes the trigo resilient to rounding errors on directionInRad.
cos = Math.cos(directionInRad);
sin = Math.sin(directionInRad);
if (cos === -1 || cos === 1) {
sin = 0;
}
if (sin === -1 || sin === 1) {
cos = 0;
}
this._xVelocity += norm * cos;
this._yVelocity += norm * sin;
this._wasStickUsed = true;
this._stickForce = 0;
} else if (this._yVelocity !== 0 || this._xVelocity !== 0) {
directionInRad = Math.atan2(this._yVelocity, this._xVelocity);
directionInDeg = (directionInRad * 180.0) / Math.PI;
const xVelocityWasPositive = this._xVelocity >= 0;
const yVelocityWasPositive = this._yVelocity >= 0;
this._xVelocity -=
this._deceleration * timeDelta * Math.cos(directionInRad);
this._yVelocity -=
this._deceleration * timeDelta * Math.sin(directionInRad);
// This makes the trigo resilient to rounding errors on directionInRad.
cos = Math.cos(directionInRad);
sin = Math.sin(directionInRad);
if (cos === -1 || cos === 1) {
sin = 0;
}
if (sin === -1 || sin === 1) {
cos = 0;
}
this._xVelocity -= this._deceleration * timeDelta * cos;
this._yVelocity -= this._deceleration * timeDelta * sin;
if (this._xVelocity > 0 !== xVelocityWasPositive) {
this._xVelocity = 0;
}
@@ -366,8 +406,8 @@ namespace gdjs {
const squaredSpeed =
this._xVelocity * this._xVelocity + this._yVelocity * this._yVelocity;
if (squaredSpeed > this._maxSpeed * this._maxSpeed) {
this._xVelocity = this._maxSpeed * Math.cos(directionInRad);
this._yVelocity = this._maxSpeed * Math.sin(directionInRad);
this._xVelocity = this._maxSpeed * cos;
this._yVelocity = this._maxSpeed * sin;
}
// No acceleration for angular speed for now.
@@ -451,6 +491,33 @@ namespace gdjs {
this._stickAngle = stickAngle % 360;
this._stickForce = Math.max(0, Math.min(1, stickForce));
}
/**.
* @param input The control to be tested [Left,Right,Up,Down,Stick].
* @returns true if the key was used since the last `doStepPreEvents` call.
*/
isUsingControl(input: string): boolean {
if (input === 'Left') {
return this._leftKeyPressedDuration > 0;
}
if (input === 'Right') {
return this._rightKeyPressedDuration > 0;
}
if (input === 'Up') {
return this._upKeyPressedDuration > 0;
}
if (input === 'Down') {
return this._downKeyPressedDuration > 0;
}
if (input === 'Stick') {
return this._wasStickUsed;
}
return false;
}
getLastStickInputAngle() {
return this._stickAngle;
}
}
export namespace TopDownMovementRuntimeBehavior {
export interface BasisTransformation {

View File

@@ -384,7 +384,7 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionContext(
// to create the new object as the object names used in the function
// are not the same as the objects available in the scene.
" createObject: function(objectName) {\n"
" var objectsList = "
" const objectsList = "
"eventsFunctionContext._objectsMap[objectName];\n" +
// TODO: we could speed this up by storing a map of object names, but
// the cost of creating/storing it for each events function might not
@@ -405,6 +405,21 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionContext(
// Unknown object, don't create anything:
" return null;\n" +
" },\n"
// Function to count instances on the scene. We need it here because
// it needs the objects map to get the object names of the parent context.
" getInstancesCountOnScene: function(objectName) {\n"
" const objectsList = "
"eventsFunctionContext._objectsMap[objectName];\n" +
" let count = 0;\n" +
" if (objectsList) {\n" +
" for(const objectName in objectsList.items)\n" +
" count += parentEventsFunctionContext ?\n" +
"parentEventsFunctionContext.getInstancesCountOnScene(objectName) "
":\n" +
" runtimeScene.getInstancesCountOnScene(objectName);\n" +
" }\n" +
" return count;\n" +
" },\n"
// Allow to get a layer directly from the context for convenience:
" getLayer: function(layerName) {\n"
" return runtimeScene.getLayer(layerName);\n"
@@ -815,7 +830,7 @@ gd::String EventsCodeGenerator::GenerateObjectsDeclarationCode(
gd::String declarationsCode;
for (auto object : context.GetObjectsListsToBeDeclared()) {
gd::String objectListDeclaration = "";
if (!context.ObjectAlreadyDeclared(object)) {
if (!context.ObjectAlreadyDeclaredByParents(object)) {
objectListDeclaration += "gdjs.copyArray(" +
GenerateAllInstancesGetterCode(object) + ", " +
GetObjectListName(object, context) + ");";
@@ -825,9 +840,9 @@ gd::String EventsCodeGenerator::GenerateObjectsDeclarationCode(
declarationsCode += objectListDeclaration + "\n";
}
for (auto object : context.GetObjectsListsToBeDeclaredWithoutPicking()) {
for (auto object : context.GetObjectsListsToBeEmptyIfJustDeclared()) {
gd::String objectListDeclaration = "";
if (!context.ObjectAlreadyDeclared(object)) {
if (!context.ObjectAlreadyDeclaredByParents(object)) {
objectListDeclaration =
GetObjectListName(object, context) + ".length = 0;\n";
context.SetObjectDeclared(object);
@@ -838,7 +853,7 @@ gd::String EventsCodeGenerator::GenerateObjectsDeclarationCode(
}
for (auto object : context.GetObjectsListsToBeDeclaredEmpty()) {
gd::String objectListDeclaration = "";
if (!context.ObjectAlreadyDeclared(object)) {
if (!context.ObjectAlreadyDeclaredByParents(object)) {
objectListDeclaration =
GetObjectListName(object, context) + ".length = 0;\n";
context.SetObjectDeclared(object);
@@ -979,12 +994,16 @@ gd::String EventsCodeGenerator::GenerateObject(
// avoid re-creating them at runtime. Arrays are passed as reference in JS and
// we always use the same static arrays, making this possible.
auto declareMapOfObjects =
[this](const std::vector<gd::String>& objects,
const gd::EventsCodeGenerationContext& context) {
[this](const std::vector<gd::String>& declaredObjectNames,
const gd::EventsCodeGenerationContext& context,
const std::vector<gd::String>& notDeclaredObjectNames = {}) {
// The map name must be unique for each set of objects lists.
// We generate it from the objects lists names.
gd::String objectsMapName = GetCodeNamespaceAccessor() + "mapOf";
gd::String mapDeclaration;
for (auto& objectName : objects) {
// The map name must be unique for each set of objects lists.
// Map each declared object to its list.
for (auto& objectName : declaredObjectNames) {
objectsMapName +=
ManObjListName(GetObjectListName(objectName, context));
@@ -993,8 +1012,20 @@ gd::String EventsCodeGenerator::GenerateObject(
"\": " + GetObjectListName(objectName, context);
}
// Map each object not declared to an empty list.
// Useful for parameters willing to get objects lists without
// picking the objects for future instructions.
for (auto& objectName : notDeclaredObjectNames) {
objectsMapName += "Empty" + ManObjListName(objectName);
if (!mapDeclaration.empty()) mapDeclaration += ", ";
mapDeclaration += "\"" + ConvertToString(objectName) +
"\": []";
}
// TODO: this should be de-duplicated.
AddCustomCodeOutsideMain(objectsMapName + " = Hashtable.newFrom({" +
mapDeclaration + "});");
mapDeclaration + "});\n");
return objectsMapName;
};
@@ -1006,14 +1037,32 @@ gd::String EventsCodeGenerator::GenerateObject(
gd::String objectsMapName = declareMapOfObjects(realObjects, context);
output = objectsMapName;
} else if (type == "objectListWithoutPicking") {
} else if (type == "objectListOrEmptyIfJustDeclared") {
std::vector<gd::String> realObjects =
ExpandObjectsName(objectName, context);
for (auto& objectName : realObjects)
context.ObjectsListWithoutPickingNeeded(objectName);
context.ObjectsListNeededOrEmptyIfJustDeclared(objectName);
gd::String objectsMapName = declareMapOfObjects(realObjects, context);
output = objectsMapName;
} else if (type == "objectListOrEmptyWithoutPicking") {
std::vector<gd::String> realObjects = ExpandObjectsName(objectName, context);
// Find the objects not yet declared, and handle them separately so they are
// passed as empty object lists.
std::vector<gd::String> objectToBeDeclaredNames;
std::vector<gd::String> objectNotYetDeclaredNames;
for (auto& objectName : realObjects) {
if (context.ObjectAlreadyDeclaredByParents(objectName) ||
context.IsToBeDeclared(objectName)) {
objectToBeDeclaredNames.push_back(objectName);
} else {
objectNotYetDeclaredNames.push_back(objectName);
}
}
gd::String objectsMapName = declareMapOfObjects(objectToBeDeclaredNames, context, objectNotYetDeclaredNames);
output = objectsMapName;
} else if (type == "objectPtr") {
std::vector<gd::String> realObjects =
ExpandObjectsName(objectName, context);

View File

@@ -33,6 +33,8 @@ AudioExtension::AudioExtension() {
"gdjs.evtTools.sound.pauseMusicOnChannel");
GetAllActions()["RePlayMusicCanal"].SetFunctionName(
"gdjs.evtTools.sound.continueMusicOnChannel");
GetAllActions()["FadeMusicVolume"].SetFunctionName(
"gdjs.evtTools.sound.fadeMusicVolume");
GetAllActions()["PreloadMusic"].SetFunctionName(
"gdjs.evtTools.sound.preloadMusic");
@@ -44,6 +46,9 @@ AudioExtension::AudioExtension() {
"gdjs.evtTools.sound.unloadSound");
GetAllActions()["UnloadAllAudio"].SetFunctionName(
"gdjs.evtTools.sound.unloadAllAudio");
GetAllActions()["FadeSoundVolume"].SetFunctionName(
"gdjs.evtTools.sound.fadeSoundVolume");
GetAllConditions()["MusicPlaying"].SetFunctionName(
"gdjs.evtTools.sound.isMusicOnChannelPlaying");

View File

@@ -252,10 +252,21 @@ BaseObjectExtension::BaseObjectExtension() {
"gdjs.evtTools.object.createObjectOnScene");
GetAllActions()["CreateByName"].SetFunctionName(
"gdjs.evtTools.object.createObjectFromGroupOnScene");
GetAllExpressions()["Count"].SetFunctionName(
"gdjs.evtTools.object.pickedObjectsCount");
"gdjs.evtTools.object.pickedObjectsCount"); // Deprecated
GetAllConditions()["NbObjet"].SetFunctionName(
"gdjs.evtTools.object.pickedObjectsCount");
"gdjs.evtTools.object.pickedObjectsCount"); // Deprecated
GetAllExpressions()["SceneInstancesCount"].SetFunctionName(
"gdjs.evtTools.object.getSceneInstancesCount");
GetAllConditions()["SceneInstancesCount"].SetFunctionName(
"gdjs.evtTools.object.getSceneInstancesCount");
GetAllExpressions()["PickedInstancesCount"].SetFunctionName(
"gdjs.evtTools.object.getPickedInstancesCount");
GetAllConditions()["PickedInstancesCount"].SetFunctionName(
"gdjs.evtTools.object.getPickedInstancesCount");
GetAllConditions()["CollisionNP"].SetFunctionName(
"gdjs.evtTools.object.hitBoxesCollisionTest");
GetAllConditions()["Raycast"].SetFunctionName(

View File

@@ -280,7 +280,7 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
//"OR" condition must declare objects list, but without getting
// the objects from the scene. Lists are either empty or come from
// a parent event.
parentContext.ObjectsListWithoutPickingNeeded(*it);
parentContext.ObjectsListNeededOrEmptyIfJustDeclared(*it);
// We need to duplicate the object lists : The "final" ones will
// be filled with objects by conditions, but they will have no
// incidence on further conditions, as conditions use "normal"

View File

@@ -37,7 +37,9 @@ MathematicalToolsExtension::MathematicalToolsExtension() {
GetAllExpressions()["atanh"].SetFunctionName("gdjs.evtTools.common.atanh");
GetAllExpressions()["cbrt"].SetFunctionName("gdjs.evtTools.common.cbrt");
GetAllExpressions()["ceil"].SetFunctionName("Math.ceil");
GetAllExpressions()["ceilTo"].SetFunctionName("gdjs.evtTools.common.ceilTo");
GetAllExpressions()["floor"].SetFunctionName("Math.floor");
GetAllExpressions()["floorTo"].SetFunctionName("gdjs.evtTools.common.floorTo");
GetAllExpressions()["cosh"].SetFunctionName("gdjs.evtTools.common.cosh");
GetAllExpressions()["sinh"].SetFunctionName("gdjs.evtTools.common.sinh");
GetAllExpressions()["tanh"].SetFunctionName("gdjs.evtTools.common.tanh");
@@ -63,6 +65,7 @@ MathematicalToolsExtension::MathematicalToolsExtension() {
GetAllExpressions()["int"].SetFunctionName("Math.round");
GetAllExpressions()["rint"].SetFunctionName("Math.round");
GetAllExpressions()["round"].SetFunctionName("Math.round");
GetAllExpressions()["roundTo"].SetFunctionName("gdjs.evtTools.common.roundTo");
GetAllExpressions()["trunc"].SetFunctionName("gdjs.evtTools.common.trunc");
GetAllExpressions()["lerp"].SetFunctionName("gdjs.evtTools.common.lerp");
GetAllExpressions()["XFromAngleAndDistance"].SetFunctionName("gdjs.evtTools.common.getXFromAngleAndDistance");

View File

@@ -17,6 +17,8 @@ MouseExtension::MouseExtension() {
"gdjs.evtTools.input.getMouseX");
GetAllConditions()["MouseY"].SetFunctionName(
"gdjs.evtTools.input.getMouseY");
GetAllConditions()["IsMouseInsideCanvas"].SetFunctionName(
"gdjs.evtTools.input.isMouseInsideCanvas");
GetAllConditions()["SourisX"].SetFunctionName(
"gdjs.evtTools.input.getMouseX"); // Deprecated
GetAllConditions()["SourisY"].SetFunctionName(
@@ -53,9 +55,9 @@ MouseExtension::MouseExtension() {
"gdjs.evtTools.input.getMouseY"); // Deprecated
GetAllConditions()["PopStartedTouch"].SetFunctionName(
"gdjs.evtTools.input.popStartedTouch");
"gdjs.evtTools.input.popStartedTouch"); // Deprecated
GetAllConditions()["PopEndedTouch"].SetFunctionName(
"gdjs.evtTools.input.popEndedTouch");
"gdjs.evtTools.input.popEndedTouch"); // Deprecated
GetAllConditions()["TouchX"].SetFunctionName("gdjs.evtTools.input.getTouchX");
GetAllConditions()["TouchY"].SetFunctionName("gdjs.evtTools.input.getTouchY");
@@ -65,9 +67,18 @@ MouseExtension::MouseExtension() {
"gdjs.evtTools.input.getTouchY");
GetAllExpressions()["LastTouchId"].SetFunctionName(
"gdjs.evtTools.input.getLastTouchId");
"gdjs.evtTools.input.getLastTouchId"); // Deprecated
GetAllExpressions()["LastEndedTouchId"].SetFunctionName(
"gdjs.evtTools.input.getLastEndedTouchId");
"gdjs.evtTools.input.getLastEndedTouchId"); // Deprecated
GetAllConditions()["HasAnyTouchStarted"].SetFunctionName(
"gdjs.evtTools.input.hasAnyTouchStarted");
GetAllConditions()["HasTouchEnded"].SetFunctionName(
"gdjs.evtTools.input.hasTouchEnded");
GetAllExpressions()["StartedTouchCount"].SetFunctionName(
"gdjs.evtTools.input.getStartedTouchCount");
GetAllExpressions()["StartedTouchId"].SetFunctionName(
"gdjs.evtTools.input.getStartedTouchIdentifier");
GetAllExpressions()["MouseWheelDelta"].SetFunctionName(
"gdjs.evtTools.input.getMouseWheelDelta");

View File

@@ -59,6 +59,8 @@ namespace gdjs {
if (
// Don't reload Box2d as it would confuse and crash the asm.js library.
endsWith(srcFilename, 'box2d.js') ||
// Don't reload sha256.js library.
endsWith(srcFilename, 'sha256.js') ||
// Don't reload shifty.js library.
endsWith(srcFilename, 'shifty.js') ||
// Don't reload shopify-buy library.

View File

@@ -306,6 +306,60 @@ namespace gdjs {
): number {
return distance * Math.sin(gdjs.toRad(angle));
};
/**
* Rounds a number to the Nth decimal place
* @param {float} value
* @param {number} decimalPlace
* @returns the rounded value
*/
export const roundTo = function (
value: float,
decimalPlace: number
): number {
if (!decimalPlace || !Number.isInteger(decimalPlace))
return Math.round(value);
return (
Math.round(value * Math.pow(10, decimalPlace)) /
Math.pow(10, decimalPlace)
);
};
/**
* Rounds down a number to the Nth decimal place
* @param {float} value
* @param {number} decimalPlace
* @returns the rounded value
*/
export const floorTo = function (
value: float,
decimalPlace: number
): number {
if (!decimalPlace || !Number.isInteger(decimalPlace))
return Math.floor(value);
return (
Math.floor(value * Math.pow(10, decimalPlace)) /
Math.pow(10, decimalPlace)
);
};
/**
* Rounds up a number to the Nth decimal place
* @param {float} value
* @param {number} decimalPlace
* @returns the rounded value
*/
export const ceilTo = function (
value: float,
decimalPlace: number
): number {
if (!decimalPlace || !Number.isInteger(decimalPlace))
return Math.ceil(value);
return (
Math.ceil(value * Math.pow(10, decimalPlace)) /
Math.pow(10, decimalPlace)
);
};
}
}
}

View File

@@ -6,7 +6,13 @@
namespace gdjs {
export namespace evtTools {
export namespace input {
/**
* @deprecated
*/
export let lastTouchId = 0;
/**
* @deprecated
*/
export let lastEndedTouchId = 0;
/**
@@ -266,6 +272,12 @@ namespace gdjs {
)[1];
};
export const isMouseInsideCanvas = function (
runtimeScene: gdjs.RuntimeScene
) {
return runtimeScene.getGame().getInputManager().isMouseInsideCanvas();
};
export const _cursorIsOnObject = function (obj, runtimeScene) {
return obj.cursorOnObject(runtimeScene);
};
@@ -285,10 +297,10 @@ namespace gdjs {
};
export const getTouchX = function (
runtimeScene,
identifier,
layer,
camera
runtimeScene: gdjs.RuntimeScene,
identifier: integer,
layer: string,
camera: integer
) {
return runtimeScene
.getLayer(layer)
@@ -298,12 +310,12 @@ namespace gdjs {
)[0];
};
export const getTouchY = function (
runtimeScene,
identifier,
layer,
camera
) {
export const getTouchY = (
runtimeScene: gdjs.RuntimeScene,
identifier: integer,
layer: string,
camera: integer
) => {
return runtimeScene
.getLayer(layer)
.convertCoords(
@@ -312,15 +324,64 @@ namespace gdjs {
)[1];
};
export const hasAnyTouchStarted = (
runtimeScene: gdjs.RuntimeScene
): boolean => {
return (
runtimeScene.getGame().getInputManager().getStartedTouchIdentifiers()
.length > 0
);
};
export const getStartedTouchCount = (
runtimeScene: gdjs.RuntimeScene
): integer => {
return runtimeScene
.getGame()
.getInputManager()
.getStartedTouchIdentifiers().length;
};
export const getStartedTouchIdentifier = (
runtimeScene: gdjs.RuntimeScene,
index: integer
): integer => {
return runtimeScene
.getGame()
.getInputManager()
.getStartedTouchIdentifiers()[index];
};
export const hasTouchEnded = (
runtimeScene: gdjs.RuntimeScene,
identifier: integer
): boolean => {
return runtimeScene
.getGame()
.getInputManager()
.hasTouchEnded(identifier);
};
/**
* @deprecated
*/
export const getLastTouchId = function () {
return gdjs.evtTools.input.lastTouchId || 0;
};
/**
* @deprecated
*/
export const getLastEndedTouchId = function () {
return gdjs.evtTools.input.lastEndedTouchId || 0;
};
export const popStartedTouch = function (runtimeScene) {
/**
* @deprecated
*/
export const popStartedTouch = function (
runtimeScene: gdjs.RuntimeScene
) {
const startedTouchId = runtimeScene
.getGame()
.getInputManager()
@@ -332,7 +393,10 @@ namespace gdjs {
return false;
};
export const popEndedTouch = function (runtimeScene) {
/**
* @deprecated
*/
export const popEndedTouch = function (runtimeScene: gdjs.RuntimeScene) {
const endedTouchId = runtimeScene
.getGame()
.getInputManager()

View File

@@ -547,17 +547,43 @@ namespace gdjs {
};
/**
* Allows events to get the number of objects picked.
* Return the number of instances in the specified lists of objects.
*/
export const pickedObjectsCount = function (objectsLists) {
let size = 0;
const lists = gdjs.staticArray(gdjs.evtTools.object.pickedObjectsCount);
export const getPickedInstancesCount = (objectsLists: ObjectsLists) => {
let count = 0;
const lists = gdjs.staticArray(
gdjs.evtTools.object.getPickedInstancesCount
);
objectsLists.values(lists);
for (let i = 0, len = lists.length; i < len; ++i) {
size += lists[i].length;
count += lists[i].length;
}
return size;
return count;
};
/**
* Return the number of instances of the specified objects living on the scene.
*/
export const getSceneInstancesCount = (
objectsContext: EventsFunctionContext | gdjs.RuntimeScene,
objectsLists: ObjectsLists
) => {
let count = 0;
const objectNames = gdjs.staticArray(
gdjs.evtTools.object.getSceneInstancesCount
);
objectsLists.keys(objectNames);
const uniqueObjectNames = new Set(objectNames);
for (const objectName of uniqueObjectNames) {
count += objectsContext.getInstancesCountOnScene(objectName);
}
return count;
};
/** @deprecated */
export const pickedObjectsCount = getPickedInstancesCount;
}
}
}

View File

@@ -428,6 +428,35 @@ namespace gdjs {
runtimeScene
.getSoundManager()
.unloadAudio(soundFile, /* isMusic= */ true);
export const fadeSoundVolume = (
runtimeScene: gdjs.RuntimeScene,
channel: integer,
toVolume: float,
timeOfFade: float /* in seconds */
) => {
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
if (sound) sound.fade(sound.getVolume(), toVolume, timeOfFade * 1000);
else {
logger.error(
`Cannot fade the volume of a non-existing sound on channel ${channel}.`
);
}
};
export const fadeMusicVolume = (
runtimeScene: gdjs.RuntimeScene,
channel: integer,
toVolume: float,
timeOfFade: float /* in seconds */
) => {
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
if (music) music.fade(music.getVolume(), toVolume, timeOfFade * 1000);
else {
logger.error(
`Cannot fade the volume of a non-existing music on channel ${channel}.`
);
}
};
}
}
}

View File

@@ -50,10 +50,14 @@ namespace gdjs {
private _howl: Howl;
/**
* The volume at which the sound is being played.
* The **initial** volume at which the sound is being played.
* Once the sound is started, this volume can be not in sync
* (in the case the sound is faded by Howler), so volume must
* be gotten from `this._howl` directly.
*
* This value is clamped between 0 and 1.
*/
private _volume: float;
private _initialVolume: float;
/**
* Whether the sound is being played in a loop or not.
@@ -80,7 +84,7 @@ namespace gdjs {
constructor(howl: Howl, volume: float, loop: boolean, rate: float) {
this._howl = howl;
this._volume = clampVolume(volume);
this._initialVolume = clampVolume(volume);
this._loop = loop;
this._rate = rate;
}
@@ -104,7 +108,7 @@ 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.volume(this._initialVolume, newID); // this._initialVolume 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.
@@ -223,7 +227,8 @@ namespace gdjs {
* @returns A float from 0 to 1.
*/
getVolume(): float {
return this._volume;
if (this._id === null) return this._initialVolume;
return this._howl.volume(this._id);
}
/**
@@ -232,10 +237,10 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setVolume(volume: float): this {
this._volume = clampVolume(volume);
this._initialVolume = 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);
if (this._id !== null) this._howl.volume(this._initialVolume, this._id);
return this;
}
@@ -295,7 +300,8 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
fade(from: float, to: float, duration: float): this {
if (this._id !== null) this._howl.fade(from, to, duration, this._id);
if (this._id !== null)
this._howl.fade(clampVolume(from), clampVolume(to), duration, this._id);
return this;
}

View File

@@ -28,6 +28,7 @@ namespace gdjs {
_releasedMouseButtons: Array<boolean>;
_mouseX: float = 0;
_mouseY: float = 0;
_isMouseInsideCanvas: boolean = true;
_mouseWheelDelta: float = 0;
_touches: Hashtable<Touch>;
//Identifiers of the touches that started during/before the frame.
@@ -37,6 +38,15 @@ namespace gdjs {
_endedTouches: Array<integer> = [];
_touchSimulateMouse: boolean = true;
/**
* @deprecated
*/
_lastStartedTouchIndex = 0;
/**
* @deprecated
*/
_lastEndedTouchIndex = 0;
constructor() {
this._pressedKeys = new Hashtable();
this._releasedKeys = new Hashtable();
@@ -193,6 +203,27 @@ namespace gdjs {
return this._mouseY;
}
/**
* Should be called when the mouse leave the canvas.
*/
onMouseLeave(): void {
this._isMouseInsideCanvas = false;
}
/**
* Should be called when the mouse enter the canvas.
*/
onMouseEnter(): void {
this._isMouseInsideCanvas = true;
}
/**
* @return true when the mouse is inside the canvas.
*/
isMouseInsideCanvas(): boolean {
return this._isMouseInsideCanvas;
}
/**
* Should be called whenever a mouse button is pressed.
* @param buttonCode The mouse button code associated to the event.
@@ -254,11 +285,11 @@ namespace gdjs {
*
* @return the touch X position, relative to the game view.
*/
getTouchX(identifier: integer): float {
if (!this._touches.containsKey(identifier)) {
getTouchX(publicIdentifier: integer): float {
if (!this._touches.containsKey(publicIdentifier)) {
return 0;
}
return this._touches.get(identifier).x;
return this._touches.get(publicIdentifier).x;
}
/**
@@ -266,11 +297,19 @@ namespace gdjs {
*
* @return the touch Y position, relative to the game view.
*/
getTouchY(identifier: integer): float {
if (!this._touches.containsKey(identifier)) {
getTouchY(publicIdentifier: integer): float {
if (!this._touches.containsKey(publicIdentifier)) {
return 0;
}
return this._touches.get(identifier).y;
return this._touches.get(publicIdentifier).y;
}
/**
* @param publicIdentifier the touch identifier
* @returns true if the touch has just ended.
*/
hasTouchEnded(publicIdentifier: integer): boolean {
return this._endedTouches.includes(publicIdentifier);
}
/**
@@ -286,17 +325,19 @@ namespace gdjs {
return InputManager._allTouchIds;
}
onTouchStart(identifier: integer, x: float, y: float): void {
this._startedTouches.push(identifier);
this._touches.put(identifier, { x: x, y: y, justEnded: false });
onTouchStart(rawIdentifier: integer, x: float, y: float): void {
const publicIdentifier = this.getPublicTouchIdentifier(rawIdentifier);
this._startedTouches.push(publicIdentifier);
this._touches.put(publicIdentifier, { x: x, y: y, justEnded: false });
if (this._touchSimulateMouse) {
this.onMouseMove(x, y);
this.onMouseButtonPressed(InputManager.MOUSE_LEFT_BUTTON);
}
}
onTouchMove(identifier: integer, x: float, y: float): void {
const touch = this._touches.get(identifier);
onTouchMove(rawIdentifier: integer, x: float, y: float): void {
const publicIdentifier = this.getPublicTouchIdentifier(rawIdentifier);
const touch = this._touches.get(publicIdentifier);
if (!touch) {
return;
}
@@ -307,27 +348,50 @@ namespace gdjs {
}
}
onTouchEnd(identifier: number): void {
this._endedTouches.push(identifier);
if (this._touches.containsKey(identifier)) {
onTouchEnd(rawIdentifier: number): void {
const publicIdentifier = this.getPublicTouchIdentifier(rawIdentifier);
this._endedTouches.push(publicIdentifier);
if (this._touches.containsKey(publicIdentifier)) {
//Postpone deletion at the end of the frame
this._touches.get(identifier).justEnded = true;
this._touches.get(publicIdentifier).justEnded = true;
}
if (this._touchSimulateMouse) {
this.onMouseButtonReleased(InputManager.MOUSE_LEFT_BUTTON);
}
}
/**
* Add 1 to the identifier to avoid identifiers taking
* the GDevelop default variable value which is 0.
* @param rawIdentifier The identifier given by the browser.
* @returns The identifier used in events.
*/
private getPublicTouchIdentifier(rawIdentifier: integer): integer {
return rawIdentifier + 1;
}
getStartedTouchIdentifiers(): integer[] {
return this._startedTouches;
}
/**
* @deprecated
*/
popStartedTouch(): integer | undefined {
return this._startedTouches.shift();
const publicIdentifier = this._startedTouches[
this._lastStartedTouchIndex
];
this._lastStartedTouchIndex++;
return publicIdentifier;
}
/**
* @deprecated
*/
popEndedTouch(): integer | undefined {
return this._endedTouches.shift();
const publicIdentifier = this._endedTouches[this._lastEndedTouchIndex];
this._lastEndedTouchIndex++;
return publicIdentifier;
}
/**
@@ -344,6 +408,13 @@ namespace gdjs {
this._touchSimulateMouse = enable;
}
/**
* @returns true if the touch events are used to simulate mouse events.
*/
isSimulatingMouseWithTouch(): boolean {
return this._touchSimulateMouse;
}
/**
* Notify the input manager that the frame ended, so anything that last
* only for one frame (started/ended touches) should be reset.
@@ -366,6 +437,8 @@ namespace gdjs {
this._releasedKeys.clear();
this._releasedMouseButtons.length = 0;
this._mouseWheelDelta = 0;
this._lastStartedTouchIndex = 0;
this._lastEndedTouchIndex = 0;
}
/**

View File

@@ -81,14 +81,36 @@ namespace gdjs {
Number.isInteger(zoomFactor)
) {
// Camera rounding is important for pixel perfect games.
// Otherwise the camera position fractional part is added to
// Otherwise, the camera position fractional part is added to
// the sprite one and it changes in which direction sprites are rounded.
// It makes sprites rounding inconsistent with each other
// and they seems to move on pixel left and right.
this._pixiContainer.position.x = Math.round(
// and they seem to move on pixel left and right.
//
// PIXI uses a floor function on sprites position on the screen,
// so a floor must be applied on the camera position too.
// According to the above calculus,
// _pixiContainer.position is the opposite of the camera,
// this is why the ceil function is used floor(x) = -ceil(-x).
//
// When the camera directly follows an object,
// given this object dimension is even,
// the decimal part of onScenePosition and cameraPosition are the same.
//
// Doing the calculus without rounding:
// onScreenPosition = onScenePosition - cameraPosition
// onScreenPosition = 980.75 - 200.75
// onScreenPosition = 780
//
// Doing the calculus with rounding:
// onScreenPosition = floor(onScenePosition + ceil(-cameraPosition))
// onScreenPosition = floor(980.75 + ceil(-200.75))
// onScreenPosition = floor(980.75 - 200)
// onScreenPosition = floor(780.75)
// onScreenPosition = 780
this._pixiContainer.position.x = Math.ceil(
this._pixiContainer.position.x
);
this._pixiContainer.position.y = Math.round(
this._pixiContainer.position.y = Math.ceil(
this._pixiContainer.position.y
);
}

View File

@@ -14,6 +14,7 @@ namespace gdjs {
getHeight: () => number;
getWidth: () => number;
isLightingLayer?: () => boolean;
getName: () => string;
}
/**
@@ -37,12 +38,12 @@ namespace gdjs {
effectData.effectType
);
if (!filterCreator) {
console.log(
'Effect "' +
effectData.name +
'" has an unknown effect type: "' +
effectData.effectType +
'". Was it registered properly? Is the effect type correct?'
console.warn(
`Effect: "${
effectData.name
}", on layer: "${target.getName()}", has an unknown effect type: "${
effectData.effectType
}". Was it registered properly? Is the effect type correct?`
);
return false;
}
@@ -115,9 +116,12 @@ namespace gdjs {
effectAdded =
this.updateAllEffectParameters(rendererEffects, effectData) &&
effectAdded;
effectAdded =
this.applyEffect(rendererObject, rendererEffects[effectData.name]) &&
effectAdded;
if (rendererEffects[effectData.name]) {
effectAdded =
this.applyEffect(rendererObject, rendererEffects[effectData.name]) &&
effectAdded;
}
return effectAdded;
}

View File

@@ -442,7 +442,11 @@ namespace gdjs {
/**
* Add the standard events handler.
*/
bindStandardEvents(manager, window, document) {
bindStandardEvents(
manager: gdjs.InputManager,
window: Window,
document: Document
) {
const renderer = this._pixiRenderer;
if (!renderer) return;
const canvas = renderer.view;
@@ -451,7 +455,7 @@ namespace gdjs {
//to game coordinates.
const that = this;
function getEventPosition(e) {
function getEventPosition(e: MouseEvent | Touch) {
const pos = [e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop];
// Handle the fact that the game is stretched to fill the canvas.
@@ -462,6 +466,18 @@ namespace gdjs {
return pos;
}
function isInsideCanvas(e: MouseEvent | Touch) {
const x = e.pageX - canvas.offsetLeft;
const y = e.pageY - canvas.offsetTop;
return (
0 <= x &&
x < (that._canvasWidth || 1) &&
0 <= y &&
y < (that._canvasHeight || 1)
);
}
//Some browsers lacks definition of some variables used to do calculations
//in getEventPosition. They are defined to 0 as they are useless.
@@ -480,6 +496,7 @@ namespace gdjs {
document.documentElement === undefined ||
document.documentElement === null
) {
// @ts-ignore
document.documentElement = {};
}
if (isNaN(document.documentElement.scrollLeft)) {
@@ -571,6 +588,12 @@ namespace gdjs {
);
return false;
};
canvas.onmouseleave = function (e) {
manager.onMouseLeave();
};
canvas.onmouseenter = function (e) {
manager.onMouseEnter();
};
window.addEventListener(
'click',
function (e) {
@@ -606,6 +629,15 @@ namespace gdjs {
for (let i = 0; i < e.changedTouches.length; ++i) {
const pos = getEventPosition(e.changedTouches[i]);
manager.onTouchMove(e.changedTouches[i].identifier, pos[0], pos[1]);
// This works because touch events are sent
// when they continue outside of the canvas.
if (manager.isSimulatingMouseWithTouch()) {
if (isInsideCanvas(e.changedTouches[i])) {
manager.onMouseEnter();
} else {
manager.onMouseLeave();
}
}
}
}
});

View File

@@ -55,6 +55,8 @@ namespace gdjs {
_scaleMode: 'linear' | 'nearest';
_pixelsRounding: boolean;
_renderer: RuntimeGameRenderer;
_sessionId: string | null;
_playerId: string | null;
//Game loop management (see startGameLoop method)
_sceneStack: SceneStack;
@@ -120,6 +122,8 @@ namespace gdjs {
? new gdjs.DebuggerClient(this)
: null;
this._isPreview = this._options.isPreview || false;
this._sessionId = null;
this._playerId = null;
}
/**
@@ -648,8 +652,7 @@ namespace gdjs {
return;
}
const baseUrl = 'https://api.gdevelop-app.com/analytics';
const playerId = this._makePlayerUuid();
let sessionId: string | null = null;
this._playerId = this._makePlayerUuid();
let lastSessionHitTime = Date.now();
fetch(baseUrl + '/session', {
method: 'POST',
@@ -659,7 +662,7 @@ namespace gdjs {
// precisely identify someone.
body: JSON.stringify({
gameId: this._data.properties.projectUuid,
playerId: playerId,
playerId: this._playerId,
game: {
name: this._data.properties.name || '',
packageName: this._data.properties.packageName || '',
@@ -691,13 +694,13 @@ namespace gdjs {
})
.then((response) => response.text())
.then((returnedSessionId) => {
sessionId = returnedSessionId;
this._sessionId = returnedSessionId;
})
.catch(() => {});
/* Ignore any error */
const sendSessionHit = () => {
if (!sessionId) {
if (!this._sessionId) {
return;
}
@@ -711,8 +714,8 @@ namespace gdjs {
baseUrl + '/session-hit',
JSON.stringify({
gameId: this._data.properties.projectUuid,
playerId: playerId,
sessionId: sessionId,
playerId: this._playerId,
sessionId: this._sessionId,
})
);
};
@@ -741,6 +744,7 @@ namespace gdjs {
}
}
this._sessionMetricsInitialized = true;
this._sessionId = this._sessionId;
}
/**
@@ -762,6 +766,14 @@ namespace gdjs {
}
}
getSessionId(): string | null {
return this._sessionId;
}
getPlayerId(): string | null {
return this._playerId;
}
/**
* Called by the game renderer when the window containing the game
* has changed size (this can result from a resize of the window,

View File

@@ -1150,6 +1150,19 @@ namespace gdjs {
return this._allInstancesList;
}
/**
* Return the number of instances of the specified object living on the scene.
* @param objectName The object name for which instances must be counted.
*/
getInstancesCountOnScene(objectName: string): integer {
const instances = this._instances.get(objectName);
if (instances) {
return instances.length;
}
return 0;
}
/**
* Check if the scene was just resumed.
* This is true during the first frame after the scene has been unpaused.

View File

@@ -37,6 +37,9 @@ declare type EventsFunctionContext = {
/** Create a new object from its name. The object is added to the instances living on the scene. */
createObject: (objectName: string) => gdjs.RuntimeObject;
/** Return the number of instances of the specified object on the scene. */
getInstancesCountOnScene: (objectName: string) => integer;
/** Get the value (string or number) of an argument that was passed to the events function. To get objects, use `getObjects`. */
getArgument: (argumentName: string) => string | number;

View File

@@ -31,6 +31,7 @@ const untransformedPaths = [
'GDJS/Runtime/libs/rbush.js',
// Extensions pre-built files:
'Extensions/Leaderboards/sha256.js',
'Extensions/Firebase/A_firebasejs',
'Extensions/BBText/pixi-multistyle-text/dist',
'Extensions/DialogueTree/bondage.js/dist',

View File

@@ -0,0 +1,871 @@
{
"firstLayout": "",
"gdVersion": {
"build": 99,
"major": 4,
"minor": 0,
"revision": 0
},
"properties": {
"adaptGameResolutionAtRuntime": true,
"folderProject": false,
"orientation": "landscape",
"packageName": "com.example.countinginstances",
"pixelsRounding": false,
"projectUuid": "ac8533e3-75f6-4a07-be87-3c1b63086ae5",
"scaleMode": "linear",
"sizeOnStartupMode": "",
"useExternalSourceFiles": false,
"version": "1.0.0",
"name": "Counting instances of objects",
"description": "Show how to count instances of objects in events",
"author": "",
"windowWidth": 800,
"windowHeight": 600,
"latestCompilationDirectory": "",
"maxFPS": 60,
"minFPS": 20,
"verticalSync": false,
"platformSpecificAssets": {},
"loadingScreen": {
"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": [
"wWP8BSlAW0UP4NeaHa2LcmmDzmH2"
],
"categories": [],
"playableDevices": [],
"extensionProperties": [],
"platforms": [
{
"name": "GDevelop JS platform"
}
],
"currentPlatform": "GDevelop JS platform"
},
"resources": {
"resources": [
{
"alwaysLoaded": false,
"file": "assets/Green laser (01).png",
"kind": "image",
"metadata": "",
"name": "Green laser (01).png",
"smoothed": true,
"userAdded": false,
"origin": {
"identifier": "https://resources.gdevelop-app.com/assets/Space Shooter/PNG/Lasers/Green laser (01).png",
"name": "gdevelop-asset-store"
}
},
{
"alwaysLoaded": false,
"file": "assets/Blue laser (08).png",
"kind": "image",
"metadata": "",
"name": "Blue laser (08).png",
"smoothed": true,
"userAdded": false,
"origin": {
"identifier": "https://resources.gdevelop-app.com/assets/Space Shooter/PNG/Lasers/Blue laser (08).png",
"name": "gdevelop-asset-store"
}
}
],
"resourceFolders": []
},
"objects": [],
"objectsGroups": [],
"variables": [],
"layouts": [
{
"b": 209,
"disableInputWhenNotFocused": true,
"mangledName": "Untitled_32scene",
"name": "Untitled scene",
"oglFOV": 90,
"oglZFar": 500,
"oglZNear": 1,
"r": 209,
"standardSortMethod": true,
"stopSoundsOnStartup": true,
"title": "",
"v": 209,
"uiSettings": {
"grid": false,
"gridType": "rectangular",
"gridWidth": 32,
"gridHeight": 32,
"gridOffsetX": 0,
"gridOffsetY": 0,
"gridColor": 10401023,
"gridAlpha": 0.8,
"snap": false,
"zoomFactor": 0.6554000000000001,
"windowMask": false
},
"objectsGroups": [
{
"name": "Lasers",
"objects": [
{
"name": "BlueLaser08"
},
{
"name": "GreenLaser01"
}
]
}
],
"variables": [],
"instances": [
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "GreenLaser01",
"persistentUuid": "e060a322-83c6-4e2c-928e-8d330e56b04c",
"width": 0,
"x": 205,
"y": 465,
"zOrder": 1,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "GreenLaser01",
"persistentUuid": "f567e2b4-09a3-48d3-9aa1-34c0bce85502",
"width": 0,
"x": 591,
"y": 220,
"zOrder": 1,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "GreenLaser01",
"persistentUuid": "dc887e97-930c-43dd-ab4a-261cdf17c074",
"width": 0,
"x": 578,
"y": 292,
"zOrder": 1,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "GreenLaser01",
"persistentUuid": "eb9de920-4fe2-45e1-a599-454805b9ee93",
"width": 0,
"x": 306,
"y": 363,
"zOrder": 1,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "GreenLaser01",
"persistentUuid": "8776b539-c81b-4f02-b802-bf72c6f5738d",
"width": 0,
"x": 572,
"y": 396,
"zOrder": 1,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "SceneCount",
"persistentUuid": "3d4287c8-7ffb-4582-b68f-a0d772b203a4",
"width": 0,
"x": 456,
"y": 22,
"zOrder": 2,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "PickedCount",
"persistentUuid": "9d481f2d-e0b7-42b2-bc86-26ea170c77ba",
"width": 0,
"x": 456,
"y": 55,
"zOrder": 3,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "MajorityText",
"persistentUuid": "ce2ef383-5364-45d2-a8ff-b72098a96e57",
"width": 0,
"x": 21,
"y": 87,
"zOrder": 4,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "GreenLaser01",
"persistentUuid": "782332b4-f841-4467-a805-5d6cd0d21ac5",
"width": 0,
"x": 535,
"y": 293,
"zOrder": 5,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "SceneCountText",
"persistentUuid": "6f2892d5-653a-4644-b8a1-c85e3ffe0179",
"width": 0,
"x": 21,
"y": 24,
"zOrder": 6,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "PickedCountText",
"persistentUuid": "ec495d87-0205-4c7d-9a3f-c06654702302",
"width": 0,
"x": 21,
"y": 53,
"zOrder": 7,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "BlueLaser08",
"persistentUuid": "ee5bf82c-f2cf-415d-a750-c92b65bac682",
"width": 0,
"x": 516,
"y": 239,
"zOrder": 8,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "BlueLaser08",
"persistentUuid": "8fa6d0aa-7a72-4f7e-97d0-c051425bdb8e",
"width": 0,
"x": 231,
"y": 262,
"zOrder": 9,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"layer": "",
"locked": false,
"name": "BlueLaser08",
"persistentUuid": "088c49ca-65b9-4a42-8058-25c49b9d0621",
"width": 0,
"x": 532,
"y": 474,
"zOrder": 10,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
}
],
"objects": [
{
"name": "GreenLaser01",
"tags": "",
"type": "Sprite",
"updateIfNotVisible": false,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Draggable",
"type": "DraggableBehavior::Draggable",
"checkCollisionMask": true
}
],
"animations": [
{
"name": "",
"useMultipleDirections": false,
"directions": [
{
"looping": true,
"timeBetweenFrames": 0.02500000037252903,
"sprites": [
{
"hasCustomCollisionMask": false,
"image": "Green laser (01).png",
"points": [],
"originPoint": {
"name": "origine",
"x": 0,
"y": 0
},
"centerPoint": {
"automatic": true,
"name": "centre",
"x": 0,
"y": 0
},
"customCollisionMask": []
}
]
}
]
}
]
},
{
"name": "BlueLaser08",
"tags": "",
"type": "Sprite",
"updateIfNotVisible": false,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "Draggable",
"type": "DraggableBehavior::Draggable",
"checkCollisionMask": true
}
],
"animations": [
{
"name": "",
"useMultipleDirections": false,
"directions": [
{
"looping": true,
"timeBetweenFrames": 0.02500000037252903,
"sprites": [
{
"hasCustomCollisionMask": false,
"image": "Blue laser (08).png",
"points": [],
"originPoint": {
"name": "origine",
"x": 0,
"y": 0
},
"centerPoint": {
"automatic": true,
"name": "centre",
"x": 0,
"y": 0
},
"customCollisionMask": []
}
]
}
]
}
]
},
{
"bold": false,
"italic": false,
"name": "SceneCount",
"smoothed": true,
"tags": "",
"type": "TextObject::Text",
"underlined": false,
"variables": [],
"effects": [],
"behaviors": [],
"string": "Text",
"font": "",
"characterSize": 20,
"color": {
"b": 0,
"g": 0,
"r": 0
}
},
{
"bold": false,
"italic": false,
"name": "PickedCount",
"smoothed": true,
"tags": "",
"type": "TextObject::Text",
"underlined": false,
"variables": [],
"effects": [],
"behaviors": [],
"string": "Text",
"font": "",
"characterSize": 20,
"color": {
"b": 0,
"g": 0,
"r": 0
}
},
{
"bold": false,
"italic": false,
"name": "MajorityText",
"smoothed": true,
"tags": "",
"type": "TextObject::Text",
"underlined": false,
"variables": [],
"effects": [],
"behaviors": [],
"string": "Text",
"font": "",
"characterSize": 20,
"color": {
"b": 0,
"g": 0,
"r": 0
}
},
{
"bold": false,
"italic": false,
"name": "PickedCountText",
"smoothed": true,
"tags": "",
"type": "TextObject::Text",
"underlined": false,
"variables": [],
"effects": [],
"behaviors": [],
"string": "Number of lasers instances on the left side:",
"font": "",
"characterSize": 20,
"color": {
"b": 0,
"g": 0,
"r": 0
}
},
{
"bold": false,
"italic": false,
"name": "SceneCountText",
"smoothed": true,
"tags": "",
"type": "TextObject::Text",
"underlined": false,
"variables": [],
"effects": [],
"behaviors": [],
"string": "Number of lasers instances living on the scene:",
"font": "",
"characterSize": 20,
"color": {
"b": 0,
"g": 0,
"r": 0
}
}
],
"events": [
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Standard",
"conditions": [],
"actions": [
{
"type": {
"inverted": false,
"value": "TextObject::String"
},
"parameters": [
"SceneCount",
"=",
"ToString(SceneInstancesCount(Lasers)) \n"
],
"subInstructions": []
}
],
"events": [
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Comment",
"color": {
"b": 109,
"g": 230,
"r": 255,
"textB": 0,
"textG": 0,
"textR": 0
},
"comment": "You can also use \"SceneInstancesCount\" in functions, it works:",
"comment2": ""
},
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Standard",
"conditions": [],
"actions": [
{
"type": {
"inverted": false,
"value": "TextObject::String"
},
"parameters": [
"SceneCount",
"=",
"ToString(SampleInstancesCounter::CountInstancesOnScene(Lasers))"
],
"subInstructions": []
}
],
"events": []
}
]
},
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"inverted": false,
"value": "PosX"
},
"parameters": [
"Lasers",
"<",
"SceneWindowWidth()/2"
],
"subInstructions": []
}
],
"actions": [
{
"type": {
"inverted": false,
"value": "TextObject::String"
},
"parameters": [
"PickedCount",
"=",
"ToString(PickedInstancesCount(Lasers)) \n"
],
"subInstructions": []
}
],
"events": []
},
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Standard",
"conditions": [],
"actions": [
{
"type": {
"inverted": false,
"value": "TextObject::String"
},
"parameters": [
"MajorityText",
"=",
"\"Please drag objects on the left side to have a majority of them here.\""
],
"subInstructions": []
}
],
"events": []
},
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"inverted": false,
"value": "PosX"
},
"parameters": [
"Lasers",
"<",
"SceneWindowWidth()/2"
],
"subInstructions": []
},
{
"type": {
"inverted": false,
"value": "PickedInstancesCount"
},
"parameters": [
"Lasers",
">",
"SceneInstancesCount(Lasers) / 2"
],
"subInstructions": []
}
],
"actions": [
{
"type": {
"inverted": false,
"value": "TextObject::String"
},
"parameters": [
"MajorityText",
"=",
"\"There is a majority of lasers instances on the left side of the scene. Good job!\""
],
"subInstructions": []
}
],
"events": []
},
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Comment",
"color": {
"b": 109,
"g": 230,
"r": 255,
"textB": 0,
"textG": 0,
"textR": 0
},
"comment": "Also check that the condition to count the number of instances on the scene works:",
"comment2": ""
},
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"inverted": false,
"value": "SceneInstancesCount"
},
"parameters": [
"",
"Lasers",
">",
"0"
],
"subInstructions": []
},
{
"type": {
"inverted": false,
"value": "SceneInstancesCount"
},
"parameters": [
"",
"Lasers",
"<",
"10"
],
"subInstructions": []
}
],
"actions": [
{
"type": {
"inverted": false,
"value": "SceneBackground"
},
"parameters": [
"",
"\"243;243;243\""
],
"subInstructions": []
}
],
"events": []
}
],
"layers": [
{
"ambientLightColorB": 32,
"ambientLightColorG": 0,
"ambientLightColorR": 0,
"followBaseLayerCamera": false,
"isLightingLayer": false,
"name": "",
"visibility": true,
"cameras": [
{
"defaultSize": true,
"defaultViewport": true,
"height": 0,
"viewportBottom": 1,
"viewportLeft": 0,
"viewportRight": 1,
"viewportTop": 0,
"width": 0
}
],
"effects": []
}
],
"behaviorsSharedData": []
}
],
"externalEvents": [],
"eventsFunctionsExtensions": [
{
"author": "",
"category": "",
"description": "",
"extensionNamespace": "",
"fullName": "",
"helpPath": "",
"iconUrl": "",
"name": "SampleInstancesCounter",
"previewIconUrl": "",
"shortDescription": "",
"version": "",
"tags": [],
"authorIds": [],
"dependencies": [],
"eventsFunctions": [
{
"description": "Count instances on the scene.",
"fullName": "Count instances on the scene.",
"functionType": "Expression",
"group": "",
"name": "CountInstancesOnScene",
"private": false,
"sentence": "",
"events": [
{
"disabled": false,
"folded": false,
"type": "BuiltinCommonInstructions::Standard",
"conditions": [],
"actions": [
{
"type": {
"inverted": false,
"value": "SetReturnNumber"
},
"parameters": [
"SceneInstancesCount(ObjectToCount)"
],
"subInstructions": []
}
],
"events": []
}
],
"parameters": [
{
"codeOnly": false,
"defaultValue": "",
"description": "Objects to count",
"longDescription": "",
"name": "ObjectToCount",
"optional": false,
"supplementaryInformation": "",
"type": "objectList"
}
],
"objectGroups": []
}
],
"eventsBasedBehaviors": []
}
],
"externalLayouts": [],
"externalSourceFiles": []
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

View File

@@ -75,6 +75,7 @@ module.exports = function (config) {
'../../newIDE/app/resources/GDJS/Runtime/Extensions/LinkedObjects/linkedobjects.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventory.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventorytools.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Leaderboards/leaderboardstools.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightruntimeobject.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightruntimeobject-pixi-renderer.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightobstacleruntimebehavior.js',

View File

@@ -3,7 +3,26 @@
*/
describe('gdjs.InputManager', function() {
var inputManager = new gdjs.InputManager();
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: [],
});
const inputManager = runtimeScene
.getGame()
.getInputManager();
const inputTools = gdjs.evtTools.input;
it('should handle keyboards events', function() {
expect(inputManager.anyKeyPressed()).to.be(false);
@@ -90,17 +109,98 @@ describe('gdjs.InputManager', function() {
expect(
inputManager.isMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON)
).to.be(false);
expect(inputManager.isMouseInsideCanvas()).to.be(true);
inputManager.onMouseLeave();
expect(inputManager.isMouseInsideCanvas()).to.be(false);
inputManager.onMouseEnter();
expect(inputManager.isMouseInsideCanvas()).to.be(true);
});
it('should handle touch events', function() {
inputManager.onTouchStart(46, 510, 610);
inputManager.onTouchStart(10, 510, 610);
expect(inputManager.getStartedTouchIdentifiers()).to.have.length(2);
expect(inputManager.getTouchX(46)).to.be(510);
expect(inputManager.getTouchY(46)).to.be(610);
inputManager.onTouchStart(10, 470, 320);
expect(inputManager.popStartedTouch()).to.be(46);
expect(inputManager.popStartedTouch()).to.be(10);
expect(inputTools.hasAnyTouchStarted(runtimeScene)).to.be(true);
expect(inputTools.getStartedTouchCount(runtimeScene)).to.be(2);
expect(inputTools.getStartedTouchIdentifier(runtimeScene, 0)).to.be(47);
expect(inputTools.getTouchX(runtimeScene, 47)).to.be(510);
expect(inputTools.getTouchY(runtimeScene, 47)).to.be(610);
expect(inputTools.getStartedTouchIdentifier(runtimeScene, 1)).to.be(11);
expect(inputTools.getTouchX(runtimeScene, 11)).to.be(470);
expect(inputTools.getTouchY(runtimeScene, 11)).to.be(320);
// Events can ask touches again
expect(inputTools.hasAnyTouchStarted(runtimeScene)).to.be(true);
expect(inputTools.getStartedTouchCount(runtimeScene)).to.be(2);
inputManager.onFrameEnded();
inputManager.onTouchEnd(10);
expect(inputTools.hasTouchEnded(runtimeScene, 11)).to.be(true);
expect(inputTools.hasTouchEnded(runtimeScene, 47)).to.be(false);
expect(inputTools.hasAnyTouchStarted(runtimeScene)).to.be(false);
expect(inputTools.getTouchX(runtimeScene, 11)).to.be(470);
expect(inputTools.getTouchY(runtimeScene, 11)).to.be(320);
inputManager.onFrameEnded();
expect(inputManager.getAllTouchIdentifiers()).to.have.length(1);
});
it('should handle legacy and new touch events without any conflict', function() {
inputManager.onTouchStart(46, 510, 610);
inputManager.onTouchStart(10, 470, 320);
// legacy ones
expect(inputTools.popStartedTouch(runtimeScene)).to.be(true);
expect(inputTools.getLastTouchId(runtimeScene)).to.be(47);
expect(inputTools.getTouchX(runtimeScene, 47)).to.be(510);
expect(inputTools.getTouchY(runtimeScene, 47)).to.be(610);
expect(inputTools.popStartedTouch(runtimeScene)).to.be(true);
expect(inputTools.getLastTouchId(runtimeScene)).to.be(11);
expect(inputTools.getTouchX(runtimeScene, 11)).to.be(470);
expect(inputTools.getTouchY(runtimeScene, 11)).to.be(320);
expect(inputTools.hasTouchEnded(runtimeScene)).to.be(false);
// new ones
expect(inputTools.hasAnyTouchStarted(runtimeScene)).to.be(true);
expect(inputTools.getStartedTouchCount(runtimeScene)).to.be(2);
expect(inputTools.getStartedTouchIdentifier(runtimeScene, 0)).to.be(47);
expect(inputTools.getTouchX(runtimeScene, 47)).to.be(510);
expect(inputTools.getTouchY(runtimeScene, 47)).to.be(610);
expect(inputTools.getStartedTouchIdentifier(runtimeScene, 1)).to.be(11);
expect(inputTools.getTouchX(runtimeScene, 11)).to.be(470);
expect(inputTools.getTouchY(runtimeScene, 11)).to.be(320);
inputManager.onFrameEnded();
inputManager.onTouchEnd(10);
// legacy ones
expect(inputTools.popEndedTouch(runtimeScene)).to.be(true);
expect(inputTools.getLastEndedTouchId()).to.be(11);
expect(inputTools.popEndedTouch(runtimeScene)).to.be(false);
expect(inputTools.hasAnyTouchStarted(runtimeScene)).to.be(false);
expect(inputTools.getTouchX(runtimeScene, 11)).to.be(470);
expect(inputTools.getTouchY(runtimeScene, 11)).to.be(320);
// new ones
expect(inputTools.hasTouchEnded(runtimeScene, 11)).to.be(true);
expect(inputTools.hasTouchEnded(runtimeScene, 47)).to.be(false);
expect(inputTools.hasAnyTouchStarted(runtimeScene)).to.be(false);
expect(inputTools.getTouchX(runtimeScene, 11)).to.be(470);
expect(inputTools.getTouchY(runtimeScene, 11)).to.be(320);
inputManager.onFrameEnded();
expect(inputManager.getAllTouchIdentifiers()).to.have.length(1);
});
it('should handle deprecated touch events', function() {
inputManager.onTouchStart(46, 510, 610);
inputManager.onTouchStart(10, 470, 320);
expect(inputManager.getStartedTouchIdentifiers()).to.have.length(2);
expect(inputManager.getTouchX(47)).to.be(510);
expect(inputManager.getTouchY(47)).to.be(610);
expect(inputManager.popStartedTouch()).to.be(47);
expect(inputManager.popStartedTouch()).to.be(11);
expect(inputManager.popEndedTouch()).to.be(undefined);
inputManager.onFrameEnded();
@@ -108,13 +208,14 @@ describe('gdjs.InputManager', function() {
expect(inputManager.getAllTouchIdentifiers()).to.have.length(2);
expect(inputManager.getStartedTouchIdentifiers()).to.have.length(0);
expect(inputManager.popStartedTouch()).to.be(undefined);
expect(inputManager.popEndedTouch()).to.be(10);
expect(inputManager.getTouchX(10)).to.be(510);
expect(inputManager.getTouchY(10)).to.be(610);
expect(inputManager.popEndedTouch()).to.be(11);
expect(inputManager.getTouchX(11)).to.be(470);
expect(inputManager.getTouchY(11)).to.be(320);
inputManager.onFrameEnded();
expect(inputManager.getAllTouchIdentifiers()).to.have.length(1);
});
it('should simulate (or not) mouse events', function() {
inputManager.touchSimulateMouse();
expect(inputManager.isMouseButtonPressed(0)).to.be(false);

View File

@@ -0,0 +1,140 @@
// @ts-check
describe('gdjs.evtTools.object', function () {
it('can count picked instances of objects', function () {
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 objectA1 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'MyObjectA',
type: '',
variables: [],
behaviors: [],
effects: [],
});
const objectA2 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'MyObjectA',
type: '',
variables: [],
behaviors: [],
effects: [],
});
const objectB1 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'MyObjectB',
type: '',
variables: [],
behaviors: [],
effects: [],
});
expect(
gdjs.evtTools.object.getPickedInstancesCount(
Hashtable.newFrom({
MyObjectA: [objectA1, objectA2],
MyObjectB: [objectB1],
})
)
).to.be(3);
expect(
gdjs.evtTools.object.getPickedInstancesCount(
Hashtable.newFrom({
MyObjectA: [],
MyObjectB: [],
})
)
).to.be(0);
// Also test the deprecated name for this function:
expect(
gdjs.evtTools.object.pickedObjectsCount(
Hashtable.newFrom({
MyObjectA: [objectA1, objectA2],
MyObjectB: [objectB1],
})
)
).to.be(3);
});
it('can count instances of objects living on the scene', function () {
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 objectA1 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'MyObjectA',
type: '',
variables: [],
behaviors: [],
effects: [],
});
const objectA2 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'MyObjectA',
type: '',
variables: [],
behaviors: [],
effects: [],
});
const objectB1 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'MyObjectB',
type: '',
variables: [],
behaviors: [],
effects: [],
});
runtimeScene.addObject(objectA1);
runtimeScene.addObject(objectA2);
runtimeScene.addObject(objectB1);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
Hashtable.newFrom({
MyObjectA: [objectA1],
MyObjectB: [objectB1],
})
)
).to.be(2 + 1);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
Hashtable.newFrom({
MyObjectA: [objectA1],
MyObjectB: [],
})
)
).to.be(2 + 1);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
Hashtable.newFrom({
MyObjectA: [objectA1],
})
)
).to.be(2);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
Hashtable.newFrom({
MyObjectA: [],
})
)
).to.be(2);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
Hashtable.newFrom({
MyObjectC: [],
})
)
).to.be(0);
});
});

View File

@@ -0,0 +1,76 @@
/**
* Generate the code from events (using GDJS platform)
* and create a JavaScript function that runs it.
*
* The JavaScript function must be called with the `runtimeScene` to be used.
* In this context, GDJS game engine does not exist, so you must pass a mock
* to it to validate that the events are working properly.
*/
function generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction,
logCode = false
) {
const namespace = 'functionNamespace';
const eventsFunctionsExtensionCodeGenerator =
new gd.EventsFunctionsExtensionCodeGenerator(project);
const includeFiles = new gd.SetString();
const code =
eventsFunctionsExtensionCodeGenerator.generateFreeEventsFunctionCompleteCode(
eventsFunction,
namespace,
includeFiles,
true
);
eventsFunctionsExtensionCodeGenerator.delete();
includeFiles.delete();
if (logCode) console.log(code);
// Create a "real" JavaScript function with the generated code.
const runCompiledEventsFunction = new Function(
'gdjs',
'runtimeScene',
'functionArguments',
// Expose some global variables that are expected by the generated code:
`Hashtable = gdjs.Hashtable;` +
'\n' +
code +
// Return the function for it to be called (if arguments are passed).
`;
return functionArguments ?
functionNamespace.func.apply(functionNamespace.func, [runtimeScene, ...functionArguments, runtimeScene]) :
null;`
);
return runCompiledEventsFunction;
}
/** Helper to create compiled events from serialized events, creating a project and the events function. */
function generateCompiledEventsFromSerializedEvents(
gd,
eventsSerializerElement
) {
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction.getEvents().unserializeFrom(project, eventsSerializerElement);
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
eventsFunction.delete();
project.delete();
return runCompiledEvents;
}
module.exports = {
generateCompiledEventsForEventsFunction,
generateCompiledEventsFromSerializedEvents,
};

View File

@@ -62,6 +62,10 @@ class RuntimeObject {
returnVariable(variable) {
return variable;
}
getVariableNumber(variable) {
return variable.getAsNumber();
}
}
/**
@@ -205,6 +209,36 @@ const createObjectOnScene = (objectsContext, objectsLists, x, y, layer) => {
}
};
/**
* @param {any} objectsContext
* @param {Hashtable<RuntimeObject[]>} objectsLists
*/
const getSceneInstancesCount = (objectsContext, objectsLists) => {
let count = 0;
const objectNames = [];
objectsLists.keys(objectNames);
const uniqueObjectNames = new Set(objectNames);
for (const objectName of uniqueObjectNames) {
count += objectsContext.getInstancesCountOnScene(objectName);
}
return count;
}
/**
* @param {Hashtable<RuntimeObject[]>} objectsLists
*/
const getPickedInstancesCount = (objectsLists) => {
let count = 0;
const lists = [];
objectsLists.values(lists);
for (let i = 0, len = lists.length; i < len; ++i) {
count += lists[i].length;
}
return count;
}
/** A minimal implementation of gdjs.RuntimeScene for testing. */
class RuntimeScene {
constructor() {
@@ -232,6 +266,16 @@ class RuntimeScene {
getOnceTriggers() {
return this._onceTriggers;
}
/** @param {string} objectName */
getInstancesCountOnScene(objectName) {
const instances = this._instances[objectName];
if (instances) {
return instances.length;
}
return 0;
}
}
/**
@@ -249,7 +293,7 @@ function makeMinimalGDJSMock() {
gdjs: {
evtTools: {
variable: { getVariableNumber: (variable) => variable.getAsNumber() },
object: { createObjectOnScene },
object: { createObjectOnScene, getSceneInstancesCount, getPickedInstancesCount },
},
registerBehavior: (behaviorTypeName, Ctor) => {
behaviorCtors[behaviorTypeName] = Ctor;

View File

@@ -1,20 +1,21 @@
const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
const { makeMinimalGDJSMock } = require('../TestUtils/GDJSMocks');
const {
generateCompiledEventsForEventsFunction,
generateCompiledEventsFromSerializedEvents,
} = require('../TestUtils/CodeGenerationHelpers.js');
/**
* Helper generating an event, ready to be unserialized, adding 1 to
* "TestVariable" of the specified object (or object group).
*/
const makeAddOneToObjectTestVariableEvent = (objectName) => ({
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { inverted: false, value: 'ModVarObjet' },
type: { value: 'ModVarObjet' },
parameters: [objectName, 'TestVariable', '+', '1'],
subInstructions: [],
},
],
events: [],
@@ -33,37 +34,30 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
// Create nested events using And and StrEqual conditions
const serializerElement = gd.Serializer.fromJSObject([
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['Counter', '=', '0'],
subInstructions: [],
},
],
events: [],
},
{
disabled: false,
folded: false,
infiniteLoopWarning: true,
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { inverted: false, value: 'VarScene' },
type: { value: 'VarScene' },
parameters: ['Counter', '<', '4'],
subInstructions: [],
},
],
conditions: [],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
subInstructions: [],
},
],
events: [],
@@ -86,50 +80,42 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
// Create nested events using Or and StrEqual conditions
const serializerElement = gd.Serializer.fromJSObject([
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::Or',
},
parameters: [],
subInstructions: [
{
type: { inverted: false, value: 'Egal' },
type: { value: 'Egal' },
parameters: ['1', '=', '2'],
subInstructions: [],
},
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::Or',
},
parameters: [],
subInstructions: [
// This should be true and make the entire conditions true.
{
type: { inverted: false, value: 'StrEqual' },
type: { value: 'StrEqual' },
parameters: ['"1"', '=', '"1"'],
subInstructions: [],
},
],
},
{
type: { inverted: false, value: 'StrEqual' },
type: { value: 'StrEqual' },
parameters: ['"1"', '=', '"2"'],
subInstructions: [],
},
],
},
],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '=', '1'],
subInstructions: [],
},
],
events: [],
@@ -154,73 +140,61 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
// Create nested events using And and StrEqual conditions
const serializerElement = gd.Serializer.fromJSObject([
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::And',
},
parameters: [],
subInstructions: [
{
type: { inverted: false, value: 'Egal' },
type: { value: 'Egal' },
parameters: ['1', '=', '1'],
subInstructions: [],
},
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::And',
},
parameters: [],
subInstructions: [
{
type: { inverted: false, value: 'Egal' },
type: { value: 'Egal' },
parameters: ['1', '=', '1'],
subInstructions: [],
},
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::And',
},
parameters: [],
subInstructions: [
{
type: { inverted: false, value: 'Egal' },
type: { value: 'Egal' },
parameters: ['1', '=', '1'],
subInstructions: [],
},
{
type: { inverted: false, value: 'StrEqual' },
type: { value: 'StrEqual' },
parameters: ['"1"', '=', '"1"'],
subInstructions: [],
},
],
},
{
type: { inverted: false, value: 'StrEqual' },
type: { value: 'StrEqual' },
parameters: ['"1"', '=', '"1"'],
subInstructions: [],
},
],
},
{
type: { inverted: false, value: 'StrEqual' },
type: { value: 'StrEqual' },
parameters: ['"1"', '=', '"1"'],
subInstructions: [],
},
],
},
],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '=', '1'],
subInstructions: [],
},
],
events: [],
@@ -244,15 +218,12 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
it('generates a working function creating objects', function () {
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { inverted: false, value: 'Create' },
type: { value: 'Create' },
parameters: ['', 'MyObjectA', '0', '0', ''],
subInstructions: [],
},
],
events: [makeAddOneToObjectTestVariableEvent('MyObjectA')],
@@ -426,47 +397,37 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
// Event to create an object, then add
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
subInstructions: [],
},
],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
subInstructions: [],
},
],
events: [],
},
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
subInstructions: [],
},
],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
subInstructions: [],
},
],
events: [],
@@ -507,47 +468,37 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
const eventsSerializerElement = gd.Serializer.fromJSON(
JSON.stringify([
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
subInstructions: [],
},
],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
subInstructions: [],
},
],
events: [],
},
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
subInstructions: [],
},
],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
subInstructions: [],
},
],
events: [],
@@ -607,15 +558,12 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
// Event to create an object, then add
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
subInstructions: [],
},
],
events: [],
@@ -632,10 +580,7 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction,
{
dontCallGeneratedFunction: true,
}
eventsFunction
);
const { gdjs, runtimeScene, mocks } = makeMinimalGDJSMock();
@@ -654,9 +599,7 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
// Simulate a hot reloading by recompiling the function and running it again.
const runHotReloadedCompiledEvents =
generateCompiledEventsForEventsFunction(gd, project, eventsFunction, {
dontCallGeneratedFunction: true,
});
generateCompiledEventsForEventsFunction(gd, project, eventsFunction);
runHotReloadedCompiledEvents(
gdjs,
runtimeScene /*, Don't pass arguments to not run the function. */
@@ -676,71 +619,3 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
project.delete();
});
});
/**
* Generate the code from events (using GDJS platform)
* and create a JavaScript function that runs it.
*
* The JavaScript function must be called with the `runtimeScene` to be used.
* In this context, GDJS game engine does not exist, so you must pass a mock
* to it to validate that the events are working properly.
*/
function generateCompiledEventsForEventsFunction(gd, project, eventsFunction) {
const namespace = 'functionNamespace';
const eventsFunctionsExtensionCodeGenerator =
new gd.EventsFunctionsExtensionCodeGenerator(project);
const includeFiles = new gd.SetString();
const code =
eventsFunctionsExtensionCodeGenerator.generateFreeEventsFunctionCompleteCode(
eventsFunction,
namespace,
includeFiles,
true
);
eventsFunctionsExtensionCodeGenerator.delete();
includeFiles.delete();
// Uncomment to see the generated code:
// console.log(code);
// Create a "real" JavaScript function with the generated code.
const runCompiledEventsFunction = new Function(
'gdjs',
'runtimeScene',
'functionArguments',
// Expose some global variables that are expected by the generated code:
`Hashtable = gdjs.Hashtable;` +
'\n' +
code +
// Return the function for it to be called (if arguments are passed).
`;
return functionArguments ?
functionNamespace.func.apply(functionNamespace.func, [runtimeScene, ...functionArguments, runtimeScene]) :
null;`
);
return runCompiledEventsFunction;
}
/** Helper to create compiled events from serialized events, creating a project and the events function. */
function generateCompiledEventsFromSerializedEvents(
gd,
eventsSerializerElement
) {
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction.getEvents().unserializeFrom(project, eventsSerializerElement);
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
eventsFunction.delete();
project.delete();
return runCompiledEvents;
}

View File

@@ -51,27 +51,21 @@ describe('libGD.js - GDJS Behavior Code Generation integration tests', function
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
disabled: false,
folded: false,
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
inverted: false,
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
subInstructions: [],
},
],
actions: [
{
type: { inverted: false, value: 'ModVarScene' },
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
subInstructions: [],
},
],
events: [],
},
]);
eventsBasedBehavior

View File

@@ -0,0 +1,559 @@
const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
const { makeMinimalGDJSMock } = require('../TestUtils/GDJSMocks');
const {
generateCompiledEventsForEventsFunction,
generateCompiledEventsFromSerializedEvents,
} = require('../TestUtils/CodeGenerationHelpers.js');
describe('libGD.js - GDJS Object Code Generation integration tests', function () {
let gd = null;
beforeAll((done) =>
initializeGDevelopJs().then((module) => {
gd = module;
done();
})
);
describe('SceneInstancesCount', () => {
const prepareCompiledEvents = () => {
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
// This condition should pass, but do not change the picking of the objects.
conditions: [
{
type: { value: 'SceneInstancesCount' },
parameters: ['', 'MyParamObject', '>', '0'],
},
],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'ResultBeforePicking',
'=',
'SceneInstancesCount(MyParamObject)',
],
},
{
type: { value: 'ModVarObjet' },
parameters: ['MyParamObject', 'Picked', '=', '1'],
},
{
type: { value: 'ModVarScene' },
parameters: [
'ResultAfterPicking',
'=',
'SceneInstancesCount(MyParamObject)',
],
},
],
},
]);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
const objectParameter = new gd.ParameterMetadata();
objectParameter.setType('object');
objectParameter.setName('MyParamObject');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.delete();
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
eventsFunction.delete();
project.delete();
return { runCompiledEvents };
};
it('counts instances from the scene in a function, when no instances are passed as parameters', () => {
const { runCompiledEvents } = prepareCompiledEvents();
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
const myObjectA1 = runtimeScene.createObject('MyObjectA');
const myObjectA2 = runtimeScene.createObject('MyObjectA');
const myObjectB1 = runtimeScene.createObject('MyObjectB');
const myObjectB2 = runtimeScene.createObject('MyObjectB');
const myObjectB3 = runtimeScene.createObject('MyObjectB');
// Run the function passing no objects as parameters.
const emptyObjectsLists = gdjs.Hashtable.newFrom({
MyObjectA: [],
MyObjectB: [],
MyObjectC: [],
});
runCompiledEvents(gdjs, runtimeScene, [emptyObjectsLists]);
// Check that the instances from the scene were counted.
expect(
runtimeScene.getVariables().get('ResultBeforePicking').getAsNumber()
).toBe(5);
expect(
runtimeScene.getVariables().get('ResultAfterPicking').getAsNumber()
).toBe(5);
// Check that the action did not modify any object.
expect(myObjectA1.getVariables().get('Picked').getAsNumber()).toBe(0);
expect(myObjectA2.getVariables().get('Picked').getAsNumber()).toBe(0);
expect(myObjectB1.getVariables().get('Picked').getAsNumber()).toBe(0);
expect(myObjectB2.getVariables().get('Picked').getAsNumber()).toBe(0);
expect(myObjectB3.getVariables().get('Picked').getAsNumber()).toBe(0);
});
it('counts instances from the scene in a function, when some instances are passed as parameters', () => {
const { runCompiledEvents } = prepareCompiledEvents();
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
const myObjectA1 = runtimeScene.createObject('MyObjectA');
const myObjectA2 = runtimeScene.createObject('MyObjectA');
const myObjectB1 = runtimeScene.createObject('MyObjectB');
const myObjectB2 = runtimeScene.createObject('MyObjectB');
const myObjectB3 = runtimeScene.createObject('MyObjectB');
// Run the function passing some objects as parameters.
const objectsLists = gdjs.Hashtable.newFrom({
MyObjectA: [myObjectA1],
MyObjectB: [myObjectB2, myObjectB3],
MyObjectC: [],
});
runCompiledEvents(gdjs, runtimeScene, [objectsLists]);
// Check that the instances from the scene were counted.
expect(
runtimeScene.getVariables().get('ResultBeforePicking').getAsNumber()
).toBe(5);
expect(
runtimeScene.getVariables().get('ResultAfterPicking').getAsNumber()
).toBe(5);
// Check that the initial condition did not modify the objects picked by the action.
expect(myObjectA1.getVariables().get('Picked').getAsNumber()).toBe(1);
expect(myObjectB2.getVariables().get('Picked').getAsNumber()).toBe(1);
expect(myObjectB3.getVariables().get('Picked').getAsNumber()).toBe(1);
});
});
describe('PickedInstancesCount', () => {
it('counts picked instances in a function', function () {
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
// Verify the picked instances count is 0 at first.
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Result1',
'=',
'PickedInstancesCount(MyParamObject)',
],
},
],
// Then verify it changes when the instances are picked by an action.
events: [
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: { value: 'VarObjet' },
parameters: ['MyParamObject', 'PleaseCountMe', '=', '1'],
},
],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Result2',
'=',
'PickedInstancesCount(MyParamObject)',
],
},
],
events: [{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [],
events: [{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
// Verify the picked instances count works when deeply nested in sub events.
actions: [{
type: { value: 'ModVarScene' },
parameters: [
'Result3',
'=',
'PickedInstancesCount(MyParamObject)',
],
}]
}]
}]
}, {
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
// Verify the picked instances count is back to 0.
actions: [{
type: { value: 'ModVarScene' },
parameters: [
'Result4',
'=',
'PickedInstancesCount(MyParamObject)',
],
}]
}
],
},
]);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
const objectParameter = new gd.ParameterMetadata();
objectParameter.setType('object');
objectParameter.setName('MyParamObject');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.delete();
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
const myObjectA1 = runtimeScene.createObject('MyObjectA');
const myObjectA2 = runtimeScene.createObject('MyObjectA');
const myObjectB1 = runtimeScene.createObject('MyObjectB');
const myObjectB2 = runtimeScene.createObject('MyObjectB');
const myObjectB3 = runtimeScene.createObject('MyObjectB');
const objectsLists = gdjs.Hashtable.newFrom({
MyObjectA: [myObjectA1],
MyObjectB: [myObjectB1, myObjectB3],
MyObjectC: [],
});
myObjectA1.getVariables().get('PleaseCountMe').setNumber(1);
myObjectB1.getVariables().get('PleaseCountMe').setNumber(1);
myObjectB3.getVariables().get('PleaseCountMe').setNumber(1);
runCompiledEvents(gdjs, runtimeScene, [objectsLists]);
// Check that the picked instances were properly counted.
expect(runtimeScene.getVariables().get('Result1').getAsNumber()).toBe(0);
expect(runtimeScene.getVariables().get('Result2').getAsNumber()).toBe(3);
expect(runtimeScene.getVariables().get('Result3').getAsNumber()).toBe(3);
expect(runtimeScene.getVariables().get('Result4').getAsNumber()).toBe(0);
eventsFunction.delete();
project.delete();
});
it('counts picked instances in a function after creating an object', function () {
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Result1',
'=',
'PickedInstancesCount(MyParamObject)',
],
},
],
events: [
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Result2',
'=',
'PickedInstancesCount(MyParamObject)',
],
},
{
type: { value: 'Create' },
parameters: ['', 'MyParamObject', '0', '0', ''],
},
{
type: { value: 'ModVarScene' },
parameters: [
'Result3',
'=',
'PickedInstancesCount(MyParamObject)',
],
},
{
type: { value: 'ModVarObjet' },
parameters: ['MyParamObject', 'Picked', '=', '1'],
},
{
type: { value: 'ModVarScene' },
parameters: [
'Result4',
'=',
'PickedInstancesCount(MyParamObject)',
],
},
],
},
],
},
]);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
const objectParameter = new gd.ParameterMetadata();
objectParameter.setType('object');
objectParameter.setName('MyParamObject');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.delete();
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
const myObjectA1 = runtimeScene.createObject('MyObjectA');
const myObjectB1 = runtimeScene.createObject('MyObjectB');
const objectsLists = gdjs.Hashtable.newFrom({
MyObjectA: [myObjectA1],
MyObjectB: [myObjectB1],
MyObjectC: [],
});
runCompiledEvents(gdjs, runtimeScene, [objectsLists]);
// Check that the picked instances were properly counted.
expect(runtimeScene.getVariables().get('Result1').getAsNumber()).toBe(0);
expect(runtimeScene.getVariables().get('Result2').getAsNumber()).toBe(0);
expect(runtimeScene.getVariables().get('Result3').getAsNumber()).toBe(1);
// Check that the object was created.
expect(runtimeScene.getObjects('MyObjectA').length).toBe(2);
expect(runtimeScene.getObjects('MyObjectB').length).toBe(1);
// Check only the created object was modified.
expect(
runtimeScene
.getObjects('MyObjectA')[0]
.getVariables()
.get('Picked')
.getAsNumber()
).toBe(0);
expect(
runtimeScene
.getObjects('MyObjectA')[1]
.getVariables()
.get('Picked')
.getAsNumber()
).toBe(1);
eventsFunction.delete();
project.delete();
});
it('counts picked instances in a function after creating an object, including a partially picked object group', function () {
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Result1_MyObjectGroup',
'=',
'PickedInstancesCount(MyObjectGroup)',
],
},
{
type: { value: 'ModVarScene' },
parameters: [
'Result1_ObjectParam1',
'=',
'PickedInstancesCount(ObjectParam1)',
],
},
],
events: [
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: { value: 'VarObjet' },
parameters: ['ObjectParam1', 'PleaseCountMe', '=', '1'],
},
],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Result2_MyObjectGroup',
'=',
'PickedInstancesCount(MyObjectGroup)',
],
},
{
type: { value: 'ModVarScene' },
parameters: [
'Result2_ObjectParam1',
'=',
'PickedInstancesCount(ObjectParam1)',
],
},
{
type: { value: 'Create' },
parameters: ['', 'ObjectParam1', '0', '0', ''],
},
{
type: { value: 'ModVarScene' },
parameters: [
'Result3_MyObjectGroup',
'=',
'PickedInstancesCount(MyObjectGroup)',
],
},
{
type: { value: 'ModVarObjet' },
parameters: ['MyObjectGroup', 'Picked', '=', '1'],
},
{
type: { value: 'ModVarScene' },
parameters: [
'Result4_MyObjectGroup',
'=',
'PickedInstancesCount(MyObjectGroup)',
],
},
],
},
],
},
]);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
const group = eventsFunction.getObjectGroups().insert('MyObjectGroup', 0);
group.setName('MyObjectGroup');
group.addObject('ObjectParam1');
group.addObject('ObjectParam2');
const objectParameter = new gd.ParameterMetadata();
objectParameter.setType('object');
objectParameter.setName('ObjectParam1');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.setType('object');
objectParameter.setName('ObjectParam2');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.delete();
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
const myObjectA1 = runtimeScene.createObject('MyObjectA');
const myObjectB1 = runtimeScene.createObject('MyObjectB');
const myObjectB2 = runtimeScene.createObject('MyObjectB');
myObjectA1.getVariables().get('PleaseCountMe').setNumber(1);
myObjectB2.getVariables().get('PleaseCountMe').setNumber(1);
const objectsLists1 = gdjs.Hashtable.newFrom({
MyObjectA: [myObjectA1],
MyObjectB: [myObjectB1],
});
const objectsLists2 = gdjs.Hashtable.newFrom({
MyObjectB: [myObjectB2],
});
runCompiledEvents(gdjs, runtimeScene, [objectsLists1, objectsLists2]);
// Check that the picked instances were properly counted.
expect(runtimeScene.getVariables().has('Result1_MyObjectGroup')).toBe(
true
);
expect(runtimeScene.getVariables().has('Result1_ObjectParam1')).toBe(
true
);
expect(
runtimeScene.getVariables().get('Result1_MyObjectGroup').getAsNumber()
).toBe(0);
expect(
runtimeScene.getVariables().get('Result1_ObjectParam1').getAsNumber()
).toBe(0);
expect(
runtimeScene.getVariables().get('Result2_MyObjectGroup').getAsNumber()
).toBe(1);
expect(
runtimeScene.getVariables().get('Result2_ObjectParam1').getAsNumber()
).toBe(1);
expect(
runtimeScene.getVariables().get('Result3_MyObjectGroup').getAsNumber()
).toBe(2);
expect(
runtimeScene.getVariables().get('Result4_MyObjectGroup').getAsNumber()
).toBe(3);
// Check that the MyObjectA was created.
expect(runtimeScene.getObjects('MyObjectA').length).toBe(2);
expect(runtimeScene.getObjects('MyObjectB').length).toBe(2);
// Check only the created object and previously picked objects were modified.
expect(
runtimeScene
.getObjects('MyObjectA')[0]
.getVariables()
.get('Picked')
.getAsNumber()
).toBe(1);
expect(
runtimeScene
.getObjects('MyObjectA')[1]
.getVariables()
.get('Picked')
.getAsNumber()
).toBe(1);
expect(myObjectB1.getVariables().get('Picked').getAsNumber()).toBe(0);
expect(myObjectB2.getVariables().get('Picked').getAsNumber()).toBe(1);
eventsFunction.delete();
project.delete();
});
});
});

View File

@@ -1214,9 +1214,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"parse5": {
@@ -8019,8 +8019,8 @@
"dev": true
},
"webidl-tools": {
"version": "git://github.com/4ian/webidl-tools.git#87247d37944d9cfdecb4f73da93289929b4077df",
"from": "git://github.com/4ian/webidl-tools.git#87247d37944d9cfdecb4f73da93289929b4077df",
"version": "github:4ian/webidl-tools#348f9c03afc9d8f278efccdd74543e265a41fd11",
"from": "github:4ian/webidl-tools#348f9c03afc9d8f278efccdd74543e265a41fd11",
"dev": true,
"requires": {
"cheerio": "^0.20.0",
@@ -8032,13 +8032,13 @@
"stream-concat": "^0.1.0",
"vinyl": "^1.1.1",
"vinyl-fs": "^2.4.2",
"webidl2": "git://github.com/markandrus/webidl2.js.git#e470735423d73fbbc20d472d9e0174592b80a463",
"webidl2": "github:markandrus/webidl2.js#e470735423d73fbbc20d472d9e0174592b80a463",
"winston": "^2.2.0"
}
},
"webidl2": {
"version": "git://github.com/markandrus/webidl2.js.git#e470735423d73fbbc20d472d9e0174592b80a463",
"from": "git://github.com/markandrus/webidl2.js.git#e470735423d73fbbc20d472d9e0174592b80a463",
"version": "github:markandrus/webidl2.js#e470735423d73fbbc20d472d9e0174592b80a463",
"from": "github:markandrus/webidl2.js#e470735423d73fbbc20d472d9e0174592b80a463",
"dev": true
},
"whatwg-encoding": {
@@ -8129,9 +8129,9 @@
"dev": true
},
"winston": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz",
"integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==",
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz",
"integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==",
"dev": true,
"requires": {
"async": "~1.0.0",

View File

@@ -33,7 +33,7 @@
"grunt-string-replace": "^1.3.1",
"jest": "^23.5.0",
"shelljs": "^0.8.4",
"webidl-tools": "git://github.com/4ian/webidl-tools.git#87247d37944d9cfdecb4f73da93289929b4077df"
"webidl-tools": "github:4ian/webidl-tools#348f9c03afc9d8f278efccdd74543e265a41fd11"
},
"jest": {
"testEnvironment": "node",

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@blueprintjs/core": "file:src/Utils/BlueprintJsPlaceholder",
"@blueprintjs/icons": "file:src/Utils/BlueprintJsPlaceholder",
"@lingui/react": "git://github.com/4ian/lingui-react.git#master",
"@lingui/react": "github:4ian/lingui-react#master",
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.56",
@@ -32,7 +32,7 @@
"keen-tracking": "1.1.3",
"lodash": "4.17.4",
"node-require-function": "^1.2.0",
"pixi-simple-gesture": "git://github.com/4ian/pixi-simple-gesture#v0.3.3",
"pixi-simple-gesture": "github:4ian/pixi-simple-gesture#v0.3.3",
"pixi.js-legacy": "^6.1.2",
"prop-types": "^15.5.10",
"randomcolor": "^0.5.3",
@@ -49,7 +49,7 @@
"react-markdown": "^4.0.6",
"react-measure": "2.3.0",
"react-monaco-editor": "^0.18.0",
"react-mosaic-component": "git://github.com/4ian/react-mosaic#v3.1.0",
"react-mosaic-component": "github:4ian/react-mosaic#v3.1.0",
"react-share": "^4.4.0",
"react-sortable-hoc": "1.5.0",
"react-sortable-tree": "2.6.2",
@@ -41756,14 +41756,14 @@
}
},
"node_modules/react-mosaic-component/node_modules/classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/react-mosaic-component/node_modules/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/react-popper": {
"version": "2.2.5",
@@ -54912,7 +54912,7 @@
"@lingui/react": {
"version": "git+ssh://git@github.com/4ian/lingui-react.git#dc6b1e013470d952cf85f96cc4affdd28e29634a",
"integrity": "sha512-eoYJ8TI+8IolPh4fue9aIwX2OVp0YrPnV86QBZLfGhxknodVeNmx+4Ic4ym7rI5/davbk9AUZHcssiH+YZWVxw==",
"from": "@lingui/react@git://github.com/4ian/lingui-react.git#master",
"from": "@lingui/react@github:4ian/lingui-react#master",
"requires": {
"@lingui/core": "2.7.3",
"babel-runtime": "^6.26.0",
@@ -80358,7 +80358,7 @@
"pixi-simple-gesture": {
"version": "git+ssh://git@github.com/4ian/pixi-simple-gesture.git#c84e0cc3c62edeca019e708d9897ef6b97a0d18a",
"integrity": "sha512-DG1BxP8SK2iPMYWMOPGz5gKDXFmA8JPUpcyyNyIH55fpQraenuLYlosYFFMTRXEy0RZViTUu11H3VrYlfG2CgA==",
"from": "pixi-simple-gesture@git://github.com/4ian/pixi-simple-gesture#v0.3.3"
"from": "pixi-simple-gesture@github:4ian/pixi-simple-gesture#v0.3.3"
},
"pixi.js": {
"version": "6.1.2",
@@ -83077,7 +83077,7 @@
"react-mosaic-component": {
"version": "git+ssh://git@github.com/4ian/react-mosaic.git#d5ef155119d786c08c7c72e34997dcef2f01f98b",
"integrity": "sha512-Izfw/EkG1g39nrZbOqzY52rqFkVFA1SUSv1TLwk7soS1Wy7iHm6zrUgzJdfwKRC2GaDn9WAfSe5ZQ2vIJ/mu5A==",
"from": "react-mosaic-component@git://github.com/4ian/react-mosaic#v3.1.0",
"from": "react-mosaic-component@github:4ian/react-mosaic#v3.1.0",
"requires": {
"classnames": "^2.2.6",
"immutability-helper": "^3.0.1",
@@ -83091,14 +83091,14 @@
},
"dependencies": {
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}
}
},

View File

@@ -33,7 +33,7 @@
"dependencies": {
"@blueprintjs/core": "file:src/Utils/BlueprintJsPlaceholder",
"@blueprintjs/icons": "file:src/Utils/BlueprintJsPlaceholder",
"@lingui/react": "git://github.com/4ian/lingui-react.git#master",
"@lingui/react": "github:4ian/lingui-react#master",
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.56",
@@ -53,7 +53,7 @@
"keen-tracking": "1.1.3",
"lodash": "4.17.4",
"node-require-function": "^1.2.0",
"pixi-simple-gesture": "git://github.com/4ian/pixi-simple-gesture#v0.3.3",
"pixi-simple-gesture": "github:4ian/pixi-simple-gesture#v0.3.3",
"pixi.js-legacy": "^6.1.2",
"prop-types": "^15.5.10",
"randomcolor": "^0.5.3",
@@ -70,7 +70,7 @@
"react-markdown": "^4.0.6",
"react-measure": "2.3.0",
"react-monaco-editor": "^0.18.0",
"react-mosaic-component": "git://github.com/4ian/react-mosaic#v3.1.0",
"react-mosaic-component": "github:4ian/react-mosaic#v3.1.0",
"react-share": "^4.4.0",
"react-sortable-hoc": "1.5.0",
"react-sortable-tree": "2.6.2",

View File

@@ -0,0 +1,7 @@
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.1917 9.52621C61.6331 3.903 62.0217 29.4536 45.9739 23.6472L47.1917 9.52621Z" stroke="#2B3990" stroke-width="5"/>
<path d="M14.8011 9.96676C0.361326 4.34356 -0.0272331 29.8942 16.0187 24.0878L14.8011 9.96676Z" stroke="#2B3990" stroke-width="5"/>
<path d="M18.2426 30.1975C12.1426 23.6797 13.7746 11.1717 14.4872 4.10922H32.3022H47.9793C47.9793 4.10922 49.7339 23.6185 44.2953 30.1975C38.8567 36.7765 23.1267 35.4161 18.2426 30.1975Z" fill="#27AAE1" stroke="#2B3990" stroke-width="5"/>
<path d="M31.175 35.033C27.3891 35.033 31.175 35.033 25.1917 57.6229H37.2175C31.175 35.033 34.9609 35.033 31.175 35.033Z" fill="#27AAE1" stroke="#2B3990" stroke-width="5"/>
<rect x="18.6728" y="55.1399" width="25.0635" height="6.3261" rx="3" fill="#2B3990"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

View File

@@ -12,6 +12,7 @@ import { ExampleListItem } from './ExampleListItem';
import { ResponsiveWindowMeasurer } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
import { ExampleDialog } from './ExampleDialog';
import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem';
import { sendExampleDetailsOpened } from '../../Utils/Analytics/EventSender';
const styles = {
searchBar: {
@@ -116,6 +117,7 @@ export const ExampleStore = ({ isOpening, onOpen, focusOnMount }: Props) => {
exampleShortHeader={exampleShortHeader}
matches={getExampleMatches(exampleShortHeader)}
onChoose={() => {
sendExampleDetailsOpened(exampleShortHeader.slug);
setSelectedExampleShortHeader(exampleShortHeader);
}}
onOpen={() => {

View File

@@ -23,6 +23,7 @@ import { IconContainer } from '../../UI/IconContainer';
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
import RaisedButton from '../../UI/RaisedButton';
import Window from '../../Utils/Window';
import { useExtensionUpdate } from './UseExtensionUpdates';
const getTransformedDescription = (extensionHeader: ExtensionHeader) => {
if (
@@ -45,7 +46,7 @@ type Props = {|
onClose: () => void,
onInstall: () => Promise<void>,
onEdit?: () => void,
alreadyInstalled: boolean,
project: gdProject,
|};
const ExtensionInstallDialog = ({
@@ -54,8 +55,13 @@ const ExtensionInstallDialog = ({
onClose,
onInstall,
onEdit,
alreadyInstalled,
project,
}: Props) => {
const alreadyInstalled = project.hasEventsFunctionsExtensionNamed(
extensionShortHeader.name
);
const extensionUpdate = useExtensionUpdate(project, extensionShortHeader);
const [error, setError] = React.useState<?Error>(null);
const [
extensionHeader,
@@ -118,7 +124,11 @@ const ExtensionInstallDialog = ({
!isCompatible ? (
<Trans>Not compatible</Trans>
) : alreadyInstalled ? (
<Trans>Re-install/update</Trans>
extensionUpdate ? (
<Trans>Update</Trans>
) : (
<Trans>Re-install</Trans>
)
) : (
<Trans>Install in project</Trans>
)

View File

@@ -0,0 +1,58 @@
//@flow
import { diff } from 'semver/functions/diff';
import { useMemo } from 'react';
import type { ExtensionShortHeader } from '../../Utils/GDevelopServices/Extension';
type UpdateType = 'patch' | 'minor' | 'major';
type UpdateMetadata = {|
type: UpdateType,
currentVersion: string,
newestVersion: string,
|};
const getUpdateMetadataFromVersions = (
currentVersion: string,
newestVersion: string
): UpdateMetadata | null => {
try {
const versionDiff: UpdateType = diff(currentVersion, newestVersion);
if (['patch', 'minor', 'major'].includes(versionDiff)) {
return {
type: versionDiff,
currentVersion,
newestVersion,
};
}
} catch {
// An error will be thrown here only if the version is not in semver.
// Simply compare the strings for such extensions.
// Note that this is an edge case, the extension repository enforces semver, so this
// is only for local extensions that do not respect the best practices.
if (currentVersion !== newestVersion) {
return {
// Use minor as it is the most neutral option
type: 'minor',
currentVersion,
newestVersion,
};
}
}
return null;
};
export const useExtensionUpdate = (
project: gdProject,
extension: ExtensionShortHeader
): UpdateMetadata | null => {
return useMemo<UpdateMetadata | null>(
() =>
project.hasEventsFunctionsExtensionNamed(extension.name)
? getUpdateMetadataFromVersions(
project.getEventsFunctionsExtension(extension.name).getVersion(),
extension.version
)
: null,
[project, extension]
);
};

View File

@@ -9,6 +9,10 @@ import { ExtensionListItem } from './ExtensionListItem';
import { ResponsiveWindowMeasurer } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
import ExtensionInstallDialog from './ExtensionInstallDialog';
import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem';
import {
sendExtensionDetailsOpened,
sendExtensionAddedToProject,
} from '../../Utils/Analytics/EventSender';
const styles = {
searchBar: {
@@ -111,6 +115,7 @@ export const ExtensionStore = ({
extensionShortHeader={extensionShortHeader}
matches={getExtensionsMatches(extensionShortHeader)}
onChoose={() => {
sendExtensionDetailsOpened(extensionShortHeader.name);
setSelectedExtensionShortHeader(extensionShortHeader);
}}
/>
@@ -121,12 +126,11 @@ export const ExtensionStore = ({
</ResponsiveWindowMeasurer>
{!!selectedExtensionShortHeader && (
<ExtensionInstallDialog
project={project}
isInstalling={isInstalling}
extensionShortHeader={selectedExtensionShortHeader}
alreadyInstalled={project.hasEventsFunctionsExtensionNamed(
selectedExtensionShortHeader.name
)}
onInstall={async () => {
sendExtensionAddedToProject(selectedExtensionShortHeader.name);
const wasInstalled = await onInstall(selectedExtensionShortHeader);
if (wasInstalled) setSelectedExtensionShortHeader(null);
}}

View File

@@ -71,6 +71,7 @@ export const create = (authentication: Authentication) => {
renderExportDialog={props => (
<ExportDialog
project={props.project}
onSaveProject={props.onSaveProject}
onChangeSubscription={props.onChangeSubscription}
onClose={props.onClose}
automatedExporters={browserAutomatedExporters}
@@ -81,7 +82,10 @@ export const create = (authentication: Authentication) => {
)}
renderCreateDialog={props => (
<CreateProjectDialog
{...props}
open={props.open}
onClose={props.onClose}
onOpen={props.onOpen}
initialTab={props.initialTab}
onCreateBlank={onCreateBlank}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
/>

View File

@@ -124,10 +124,14 @@ export default class EventsBasedBehaviorPropertiesEditor extends React.Component
_setChoiceExtraInfo = (property: gdNamedPropertyDescriptor) => {
return (newExtraInfo: Array<string>) => {
const defaultValueIndex = getExtraInfoArray(property).indexOf(
property.getValue()
);
const vectorString = new gd.VectorString();
newExtraInfo.forEach(item => vectorString.push_back(item));
property.setExtraInfo(vectorString);
vectorString.delete();
property.setValue(newExtraInfo[defaultValueIndex] || '');
this.forceUpdate();
};
};
@@ -334,6 +338,28 @@ export default class EventsBasedBehaviorPropertiesEditor extends React.Component
disabled={false}
/>
)}
{property.getType() === 'Choice' && (
<SelectField
floatingLabelText={<Trans>Default value</Trans>}
value={property.getValue()}
onChange={(e, i, value) => {
property.setValue(value);
this.forceUpdate();
this.props.onPropertiesUpdated();
}}
fullWidth
>
{getExtraInfoArray(property).map(
(choice, index) => (
<SelectOption
key={index}
value={choice}
primaryText={choice}
/>
)
)}
</SelectField>
)}
</ResponsiveLineStackLayout>
{property.getType() === 'Choice' && (
<StringArrayEditor

View File

@@ -232,7 +232,7 @@ export default class EventsBasedBehaviorsList extends React.Component<
click: () => this._editName(eventsBasedBehavior),
},
{
label: i18n._(t`Remove`),
label: i18n._(t`Delete`),
click: () =>
this._deleteEventsBasedBehavior(eventsBasedBehavior, {
askForConfirmation: true,

View File

@@ -162,7 +162,7 @@ export const ExtensionDependenciesEditor = ({
</TableRowColumn>
<TableRowColumn>
<IconButton
tooltip={t`Remove`}
tooltip={t`Delete`}
onClick={() => {
eventsFunctionsExtension.removeDependencyAt(index);
forceUpdate();

View File

@@ -41,6 +41,7 @@ import Tune from '@material-ui/icons/Tune';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import { getParametersIndexOffset } from '../EventsFunctionsExtensionsLoader';
import { sendEventsExtractedAsFunction } from '../Utils/Analytics/EventSender';
const gd: libGDevelop = global.gd;
@@ -57,7 +58,11 @@ type Props = {|
) => void,
onCreateEventsFunction: (
extensionName: string,
eventsFunction: gdEventsFunction
eventsFunction: gdEventsFunction,
editorIdentifier:
| 'scene-events-editor'
| 'extension-events-editor'
| 'external-events-editor'
) => void,
onBehaviorEdited?: () => Promise<void>,
initiallyFocusedFunctionName: ?string,
@@ -668,6 +673,24 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
// Do nothing otherwise to avoid costly and useless extra renders.
};
onBeginCreateEventsFunction = () => {
sendEventsExtractedAsFunction({
step: 'begin',
parentEditor: 'extension-events-editor',
});
};
onCreateEventsFunction = (
extensionName: string,
eventsFunction: gdEventsFunction
) => {
this.props.onCreateEventsFunction(
extensionName,
eventsFunction,
'extension-events-editor'
);
};
render() {
const { project, eventsFunctionsExtension } = this.props;
const {
@@ -776,7 +799,8 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
this.props.openInstructionOrExpression
}
setToolbar={this.props.setToolbar}
onCreateEventsFunction={this.props.onCreateEventsFunction}
onBeginCreateEventsFunction={this.onBeginCreateEventsFunction}
onCreateEventsFunction={this.onCreateEventsFunction}
onOpenSettings={this._editOptions}
unsavedChanges={this.props.unsavedChanges}
/>

View File

@@ -293,7 +293,7 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
click: () => this._togglePrivate(eventsFunction),
},
{
label: i18n._(t`Remove`),
label: i18n._(t`Delete`),
click: () =>
this._deleteEventsFunction(eventsFunction, {
askForConfirmation: true,

View File

@@ -343,7 +343,7 @@ export default function NewInstructionEditorDialog({
cannotBeDismissed={true}
maxWidth={false}
noMargin
flexRowBody
flexBody
fullHeight={
true /* Always use full height to avoid a very small dialog when there are not a lot of objects. */
}

View File

@@ -6,7 +6,7 @@ import ResourceSelector from '../../ResourcesList/ResourceSelector';
import ResourcesLoader from '../../ResourcesLoader';
import { type ParameterFieldProps } from './ParameterFieldCommons';
export default class BitmapFontResourceField extends Component<
export default class FontResourceField extends Component<
ParameterFieldProps,
void
> {

View File

@@ -13,9 +13,12 @@ import {
describe('FormatExpressionCall', () => {
it('properly formats a free function, with one or more arguments', () => {
const freeExpressions = enumerateFreeExpressions('number');
const countExpression = filterExpressions(freeExpressions, 'Count')[0];
const countExpression = filterExpressions(
freeExpressions,
'PickedInstancesCount'
)[0];
expect(formatExpressionCall(countExpression, ['MyObject'])).toBe(
'Count(MyObject)'
'PickedInstancesCount(MyObject)'
);
const atan2Expression = filterExpressions(freeExpressions, 'atan2')[0];

View File

@@ -0,0 +1,58 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import ResourceSelector from '../../ResourcesList/ResourceSelector';
import ResourcesLoader from '../../ResourcesLoader';
import {
type ParameterFieldProps,
type ParameterFieldInterface,
} from './ParameterFieldCommons';
const ImageResourceField = React.forwardRef<
ParameterFieldProps,
ParameterFieldInterface
>((props, ref) => {
const fieldRef = React.useRef<?ResourceSelector>(null);
const focus = (selectAll: boolean = false) => {
if (fieldRef.current) fieldRef.current.focus(selectAll);
};
React.useImperativeHandle(ref, () => ({
focus,
}));
if (
!props.resourceSources ||
!props.onChooseResource ||
!props.resourceExternalEditors ||
!props.project
) {
console.error(
'Missing project, resourceSources, onChooseResource or resourceExternalEditors for ImageResourceField'
);
return null;
}
return (
<ResourceSelector
margin={props.isInline ? 'none' : 'dense'}
project={props.project}
resourceSources={props.resourceSources}
onChooseResource={props.onChooseResource}
resourceExternalEditors={props.resourceExternalEditors}
resourcesLoader={ResourcesLoader}
resourceKind="image"
fullWidth
initialResourceName={props.value}
onChange={props.onChange}
floatingLabelText={<Trans>Choose the image file to use</Trans>}
onRequestClose={props.onRequestClose}
onApply={props.onApply}
ref={fieldRef}
/>
);
});
export default ImageResourceField;

View File

@@ -0,0 +1,231 @@
// @flow
import React from 'react';
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import OpenInNew from '@material-ui/icons/OpenInNew';
import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flow';
import {
type ParameterFieldProps,
type ParameterFieldInterface,
} from './ParameterFieldCommons';
import SelectField from '../../UI/SelectField';
import SelectOption from '../../UI/SelectOption';
import { TextFieldWithButtonLayout } from '../../UI/Layout';
import RaisedButtonWithSplitMenu from '../../UI/RaisedButtonWithSplitMenu';
import { type Leaderboard } from '../../Utils/GDevelopServices/Play';
import LeaderboardContext from '../../Leaderboard/LeaderboardContext';
import LeaderboardDialog from '../../Leaderboard/LeaderboardDialog';
import GenericExpressionField from './GenericExpressionField';
import { breakUuid } from '../../Utils/GDevelopServices/Play';
import { useOnlineStatus } from '../../Utils/OnlineStatus';
const getInlineParameterDisplayValue = (
leaderboards: ?Array<Leaderboard>,
value: string
): string => {
if (!leaderboards) return value;
const leaderboard = leaderboards.find(
leaderboard => `"${leaderboard.id}"` === value
);
return leaderboard ? leaderboard.name : value;
};
const useFetchLeaderboards = () => {
const { leaderboards, listLeaderboards } = React.useContext(
LeaderboardContext
);
const fetchLeaderboards = React.useCallback(
async () => {
await listLeaderboards();
},
[listLeaderboards]
);
React.useEffect(
() => {
if (!leaderboards) {
fetchLeaderboards();
}
},
[fetchLeaderboards, leaderboards]
);
return leaderboards;
};
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
function LeaderboardIdField(props, ref) {
const isOnline = useOnlineStatus();
const leaderboards = useFetchLeaderboards();
const [isAdminOpen, setIsAdminOpen] = React.useState(false);
const inputFieldRef = React.useRef<?(GenericExpressionField | SelectField)>(
null
);
React.useImperativeHandle(ref, () => ({
focus: () => {
if (inputFieldRef.current) {
inputFieldRef.current.focus();
}
},
}));
const isCurrentValueInLeaderboardList =
leaderboards &&
!!leaderboards.find(leaderboard => `"${leaderboard.id}"` === props.value);
const [isExpressionField, setIsExpressionField] = React.useState(
!leaderboards || (!!props.value && !isCurrentValueInLeaderboardList)
);
const onChangeSelectValue = (event, value) => {
props.onChange(event.target.value);
};
const onChangeTextValue = (value: string) => {
props.onChange(value);
};
const fieldLabel = props.parameterMetadata
? props.parameterMetadata.getDescription()
: undefined;
const gameHasLeaderboards = leaderboards && leaderboards.length > 0;
const selectOptions = React.useMemo(
() =>
leaderboards && gameHasLeaderboards
? leaderboards.map(leaderboard => (
<SelectOption
key={leaderboard.id}
value={`"${leaderboard.id}"`}
primaryText={`${leaderboard.name} ${
leaderboard.id ? `(${breakUuid(leaderboard.id)})` : ''
}`}
/>
))
: [
<SelectOption
disabled
key="empty"
value="empty"
primaryText={''}
/>,
],
[leaderboards, gameHasLeaderboards]
);
return (
<I18n>
{({ i18n }) => (
<>
<TextFieldWithButtonLayout
renderTextField={() =>
!isExpressionField ? (
<SelectField
ref={inputFieldRef}
value={props.value}
onChange={onChangeSelectValue}
margin={props.isInline ? 'none' : 'dense'}
fullWidth
floatingLabelText={fieldLabel}
hintText={
gameHasLeaderboards
? props.parameterMetadata &&
props.parameterMetadata.isOptional()
? t`Choose a leaderboard (optional)`
: t`Choose a leaderboard`
: t`No leaderboards`
}
helperMarkdownText={
!gameHasLeaderboards
? i18n._(
t`There are currently no leaderboards created for this game. Open the leaderboards manager to create one.`
)
: (props.parameterMetadata &&
props.parameterMetadata.getLongDescription()) ||
null
}
>
{selectOptions}
</SelectField>
) : (
<GenericExpressionField
ref={inputFieldRef}
expressionType="string"
{...props}
onChange={onChangeTextValue}
onExtractAdditionalErrors={(
currentExpression: string,
currentExpressionNode: gdExpressionNode
) => {
if (!leaderboards) {
if (!isOnline)
return 'Unable to fetch leaderboards as you are offline.';
return 'Your game may not be registered, create one in the leaderboard manager.';
}
}}
/>
)
}
renderButton={style => (
<RaisedButtonWithSplitMenu
icon={<OpenInNew />}
style={style}
primary
onClick={() => setIsAdminOpen(true)}
buildMenuTemplate={i18n => [
{
label: isExpressionField
? i18n._(t`Select the leaderboard from a list`)
: i18n._(
t`Enter the leaderboard id as a text or an expression`
),
disabled: !leaderboards,
click: () => setIsExpressionField(!isExpressionField),
},
]}
/>
)}
/>
{isAdminOpen && !!props.project && (
<LeaderboardDialog
onClose={() => setIsAdminOpen(false)}
open={isAdminOpen}
project={props.project}
/>
)}
</>
)}
</I18n>
);
}
);
const InlineLeaderboardIdField = ({
value,
parameterMetadata,
InvalidParameterValue,
}: ParameterInlineRendererProps) => {
const leaderboards = useFetchLeaderboards();
if (!value) {
if (parameterMetadata.isOptional()) {
return (
<span>
<Trans>No leaderboard chosen</Trans>
</span>
);
} else {
return (
<InvalidParameterValue isEmpty>
<Trans>Choose a leaderboard</Trans>
</InvalidParameterValue>
);
}
}
return <span>{getInlineParameterDisplayValue(leaderboards, value)}</span>;
};
export const renderInlineLeaderboardIdField = (
props: ParameterInlineRendererProps
) => <InlineLeaderboardIdField {...props} />;

View File

@@ -37,6 +37,7 @@ import ObjectVariableField, {
renderInlineObjectVariable,
} from './ParameterFields/ObjectVariableField';
import LayerField from './ParameterFields/LayerField';
import ImageResourceField from './ParameterFields/ImageResourceField';
import AudioResourceField from './ParameterFields/AudioResourceField';
import VideoResourceField from './ParameterFields/VideoResourceField';
import JsonResourceField from './ParameterFields/JsonResourceField';
@@ -56,6 +57,9 @@ import ObjectAnimationNameField from './ParameterFields/ObjectAnimationNameField
import FunctionParameterNameField from './ParameterFields/FunctionParameterNameField';
import ExternalLayoutNameField from './ParameterFields/ExternalLayoutNameField';
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
import LeaderboardIdField, {
renderInlineLeaderboardIdField,
} from './ParameterFields/LeaderboardIdField';
const gd: libGDevelop = global.gd;
const components = {
@@ -78,6 +82,7 @@ const components = {
file: DefaultField, //TODO
musicfile: AudioResourceField,
soundfile: AudioResourceField,
imageResource: ImageResourceField,
videoResource: VideoResourceField,
jsonResource: JsonResourceField,
bitmapFontResource: BitmapFontResourceField,
@@ -95,6 +100,7 @@ const components = {
objectAnimationName: ObjectAnimationNameField,
functionParameterName: FunctionParameterNameField,
externalLayoutName: ExternalLayoutNameField,
leaderboardId: LeaderboardIdField,
};
const inlineRenderers: { [string]: ParameterInlineRenderer } = {
default: renderInlineDefaultField,
@@ -109,6 +115,7 @@ const inlineRenderers: { [string]: ParameterInlineRenderer } = {
trueorfalse: renderInlineTrueFalse,
operator: renderInlineOperator,
relationalOperator: renderInlineRelationalOperator,
leaderboardId: renderInlineLeaderboardIdField,
};
const userFriendlyTypeName: { [string]: MessageDescriptor } = {
mouse: t`Mouse button`,
@@ -128,6 +135,7 @@ const userFriendlyTypeName: { [string]: MessageDescriptor } = {
key: t`Keyboard key`,
musicfile: t`Audio resource`,
soundfile: t`Audio resource`,
imageResource: t`Image resource`,
videoResource: t`Video resource`,
bitmapFontResource: t`Bitmap font resource`,
fontResource: t`Font resource`,

View File

@@ -92,6 +92,9 @@ import {
addCreateBadgePreHookIfNotClaimed,
TRIVIAL_FIRST_EVENT,
} from '../Utils/GDevelopServices/Badge';
import LeaderboardContext, {
type LeaderboardState,
} from '../Leaderboard/LeaderboardContext';
const gd: libGDevelop = global.gd;
const zoomLevel = { min: 1, max: 50 };
@@ -117,6 +120,7 @@ type Props = {|
extensionName: string,
eventsFunction: gdEventsFunction
) => void,
onBeginCreateEventsFunction: () => void,
unsavedChanges?: ?UnsavedChanges,
|};
@@ -124,6 +128,7 @@ type ComponentProps = {|
...Props,
authenticatedUser: AuthenticatedUser,
preferences: Preferences,
leaderboardsManager: ?LeaderboardState,
|};
type State = {|
@@ -1090,6 +1095,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
eventsList.insertEvent(event, eventsList.getEventsCount())
);
this.props.onBeginCreateEventsFunction();
this.setState({
serializedEventsToExtract: serializeToJSObject(eventsList),
});
@@ -1563,11 +1570,13 @@ const EventsSheet = (props, ref) => {
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const preferences = React.useContext(PreferencesContext);
const leaderboardsManager = React.useContext(LeaderboardContext);
return (
<EventsSheetComponentWithoutHandle
ref={component}
authenticatedUser={authenticatedUser}
preferences={preferences}
leaderboardsManager={leaderboardsManager}
{...props}
/>
);

View File

@@ -24,9 +24,8 @@ import {
} from '../ExportPipeline.flow';
import {
ExplanationHeader,
WebProjectLink,
OnlineGameLink,
} from '../GenericExporters/OnlineWebExport';
import { type BuildStep } from '../Builds/BuildStepsProgress';
const gd: libGDevelop = global.gd;
type ExportState = null;
@@ -73,17 +72,19 @@ export const browserOnlineWebExportPipeline: ExportPipeline<
renderLaunchButtonLabel: () => <Trans>Generate link</Trans>,
renderCustomStepsProgress: (
build: ?Build,
errored: boolean,
exportStep: BuildStep,
getGameThumbnailUrl: (buildId: string) => ?string
) => (
<WebProjectLink
renderCustomStepsProgress: ({
build,
project,
onSaveProject,
errored,
exportStep,
}) => (
<OnlineGameLink
build={build}
project={project}
onSaveProject={onSaveProject}
errored={errored}
exportStep={exportStep}
getGameThumbnailUrl={getGameThumbnailUrl}
/>
),

View File

@@ -216,14 +216,14 @@ export default ({
<Line expand justifyContent="flex-end">
{game && !!build.s3Key && !isBuildPublished && (
<RaisedButton
label={<Trans>Publish this build on Liluo</Trans>}
label={<Trans>Publish this build on Liluo.io</Trans>}
onClick={() => onUpdatePublicBuild(build.id)}
disabled={gameUpdating}
/>
)}
{game && !!build.s3Key && isBuildPublished && (
<FlatButton
label={<Trans>Unpublish this build from Liluo</Trans>}
label={<Trans>Unpublish this build from Liluo.io</Trans>}
onClick={() => onUpdatePublicBuild(null)}
disabled={gameUpdating}
/>

View File

@@ -41,6 +41,7 @@ type ExportHomeProps = {|
setChosenExporterSection: (section: ExporterSection) => void,
cantExportBecauseOffline: boolean,
project: gdProject,
onSaveProject: () => Promise<void>,
onChangeSubscription: () => void,
authenticatedUser: AuthenticatedUser,
isNavigationDisabled: boolean,
@@ -54,6 +55,7 @@ const ExportHome = ({
setChosenExporterSection,
cantExportBecauseOffline,
project,
onSaveProject,
onChangeSubscription,
authenticatedUser,
isNavigationDisabled,
@@ -77,6 +79,7 @@ const ExportHome = ({
<ExportLauncher
exportPipeline={onlineWebExporter.exportPipeline}
project={project}
onSaveProject={onSaveProject}
onChangeSubscription={onChangeSubscription}
authenticatedUser={authenticatedUser}
setIsNavigationDisabled={setIsNavigationDisabled}

View File

@@ -23,7 +23,6 @@ import BuildStepsProgress, {
import {
registerGame,
getGame,
updateGame,
type Game,
setGameUserAcls,
getAclsFromUserIds,
@@ -35,7 +34,6 @@ import {
addCreateBadgePreHookIfNotClaimed,
TRIVIAL_FIRST_WEB_EXPORT,
} from '../../Utils/GDevelopServices/Badge';
import { getWebBuildThumbnailUrl } from '../../Utils/GDevelopServices/Build';
type State = {|
exportStep: BuildStep,
@@ -50,6 +48,7 @@ type State = {|
type Props = {|
project: gdProject,
onSaveProject: () => Promise<void>,
onChangeSubscription: () => void,
authenticatedUser: AuthenticatedUser,
exportPipeline: ExportPipeline<any, any, any, any, any>,
@@ -157,30 +156,22 @@ export default class ExportLauncher extends Component<Props, State> {
}
};
registerAndUpdateGame = async () => {
registerGameIfNot = async () => {
const profile = this.props.authenticatedUser.profile;
const getAuthorizationHeader = this.props.authenticatedUser
.getAuthorizationHeader;
const gameId = this.props.project.getProjectUuid();
const authorName =
this.props.project.getAuthor() || 'Unspecified publisher';
const gameName = this.props.project.getName() || 'Untitled game';
if (profile) {
const userId = profile.id;
try {
// Try to fetch the game to see if it's registered.
// Try to fetch the game to see if it's registered but do not do anything with it.
await getGame(getAuthorizationHeader, userId, gameId);
// Update the game details to ensure that it is up to date in GDevelop services.
const game = await updateGame(getAuthorizationHeader, userId, gameId, {
authorName,
gameName,
});
// We don't await for the authors update, as it is not required for publishing.
this.tryUpdateAuthors();
this.props.onGameUpdated(game);
} catch (err) {
if (err.response.status === 404) {
// If the game is not registered, register it before launching the export.
const authorName =
this.props.project.getAuthor() || 'Unspecified publisher';
const gameName = this.props.project.getName() || 'Untitled game';
const game = await registerGame(getAuthorizationHeader, userId, {
gameId,
authorName,
@@ -253,7 +244,7 @@ export default class ExportLauncher extends Component<Props, State> {
try {
setStep('register');
// We await for this call, allowing to link the build to the game just registered.
await this.registerAndUpdateGame();
await this.registerGameIfNot();
} catch {
// But if it fails, we don't prevent building the game.
console.warn('Error while registering the game - ignoring it.');
@@ -342,7 +333,12 @@ export default class ExportLauncher extends Component<Props, State> {
doneFooterOpen,
exportState,
} = this.state;
const { project, authenticatedUser, exportPipeline } = this.props;
const {
project,
authenticatedUser,
exportPipeline,
onSaveProject,
} = this.props;
if (!project) return null;
const getBuildLimit = (authenticatedUser: AuthenticatedUser): ?Limit =>
authenticatedUser.limits && exportPipeline.onlineBuildType
@@ -411,14 +407,13 @@ export default class ExportLauncher extends Component<Props, State> {
)}
{authenticatedUser.authenticated &&
(exportPipeline.renderCustomStepsProgress ? (
exportPipeline.renderCustomStepsProgress(
exportPipeline.renderCustomStepsProgress({
build,
project,
onSaveProject,
errored,
exportStep,
buildId =>
this.props.project &&
getWebBuildThumbnailUrl(this.props.project, buildId)
)
})
) : (
<Line expand>
<BuildStepsProgress

View File

@@ -43,6 +43,7 @@ export type Exporter = {|
export type ExportDialogWithoutExportsProps = {|
project: ?gdProject,
onSaveProject: () => Promise<void>,
onClose: () => void,
onChangeSubscription: () => void,
|};
@@ -57,6 +58,7 @@ type Props = {|
const ExportDialog = ({
project,
onSaveProject,
onClose,
allExportersRequireOnline,
onChangeSubscription,
@@ -197,6 +199,7 @@ const ExportDialog = ({
setChosenExporterKey={setChosenExporterKey}
setChosenExporterSection={setChosenExporterSection}
project={project}
onSaveProject={onSaveProject}
onChangeSubscription={onChangeSubscription}
authenticatedUser={authenticatedUser}
isNavigationDisabled={isNavigationDisabled}
@@ -233,6 +236,7 @@ const ExportDialog = ({
<ExportLauncher
exportPipeline={exporter.exportPipeline}
project={project}
onSaveProject={onSaveProject}
onChangeSubscription={onChangeSubscription}
authenticatedUser={authenticatedUser}
key={chosenExporterKey}

View File

@@ -46,12 +46,13 @@ export type ExportPipeline<
isNavigationDisabled: (exportStep: BuildStep, errored: boolean) => boolean,
renderCustomStepsProgress?: (
renderCustomStepsProgress?: ({
build: ?Build,
project: gdProject,
onSaveProject: () => Promise<void>,
errored: boolean,
exportStep: BuildStep,
getGameThumbnailUrl: (buildId: string) => ?string
) => React.Node,
}) => React.Node,
prepareExporter: (
context: ExportPipelineContext<ExportState>

View File

@@ -1,345 +0,0 @@
// @flow
import { Trans } from '@lingui/macro';
import { t } from '@lingui/macro';
import * as React from 'react';
import Text from '../../UI/Text';
import { Column, Line } from '../../UI/Grid';
import TextField from '../../UI/TextField';
import {
getBuildArtifactUrl,
type Build,
} from '../../Utils/GDevelopServices/Build';
import { type BuildStep } from '../Builds/BuildStepsProgress';
import RaisedButton from '../../UI/RaisedButton';
import Window from '../../Utils/Window';
import Copy from '../../UI/CustomSvgIcons/Copy';
import Share from '@material-ui/icons/Share';
import InfoBar from '../../UI/Messages/InfoBar';
import IconButton from '../../UI/IconButton';
import { CircularProgress, LinearProgress } from '@material-ui/core';
import FlatButton from '../../UI/FlatButton';
import Dialog from '../../UI/Dialog';
import {
EmailShareButton,
FacebookShareButton,
RedditShareButton,
TwitterShareButton,
WhatsappShareButton,
EmailIcon,
FacebookIcon,
RedditIcon,
TwitterIcon,
WhatsappIcon,
} from 'react-share';
import { TextFieldWithButtonLayout } from '../../UI/Layout';
import {
getGame,
getGameUrl,
updateGame,
type Game,
} from '../../Utils/GDevelopServices/Game';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import AlertMessage from '../../UI/AlertMessage';
const styles = {
icon: {
padding: 5,
},
};
export const ExplanationHeader = () => (
<Column noMargin alignItems="center" justifyContent="center">
<Line>
<Text align="center">
<Trans>
Generate a unique link, playable from any computer or mobile phone's
browser.
</Trans>
</Text>
</Line>
</Column>
);
type WebProjectLinkProps = {|
build: ?Build,
errored: boolean,
exportStep: BuildStep,
getGameThumbnailUrl: (buildId: string) => ?string,
|};
export const WebProjectLink = ({
build,
errored,
exportStep,
getGameThumbnailUrl,
}: WebProjectLinkProps) => {
const [showCopiedInfoBar, setShowCopiedInfoBar] = React.useState<boolean>(
false
);
const [isShareDialogOpen, setIsShareDialogOpen] = React.useState<boolean>(
false
);
const [game, setGame] = React.useState<?Game>(null);
const [isGameLoading, setIsGameLoading] = React.useState<boolean>(false);
const { getAuthorizationHeader, profile } = React.useContext(
AuthenticatedUserContext
);
const exportPending = !errored && exportStep !== '' && exportStep !== 'done';
const isBuildComplete = build && build.status === 'complete';
const isBuildPublished = build && game && build.id === game.publicWebBuildId;
const gameUrl = getGameUrl(game);
const buildUrl =
exportPending || !isBuildComplete
? null
: isBuildPublished
? gameUrl
: getBuildArtifactUrl(build, 's3Key');
const loadGame = React.useCallback(
async () => {
const gameId = build && build.gameId;
if (!profile || !gameId) return;
const { id } = profile;
try {
setIsGameLoading(true);
const game = await getGame(getAuthorizationHeader, id, gameId);
setGame(game);
setIsGameLoading(false);
} catch (err) {
setIsGameLoading(false);
console.error('Unable to load the game', err);
}
},
[build, getAuthorizationHeader, profile]
);
React.useEffect(
() => {
// Load game only once
if (!game && isBuildComplete) {
loadGame();
}
},
[game, loadGame, isBuildComplete]
);
const onOpen = () => {
if (!buildUrl) return;
Window.openExternalURL(buildUrl);
};
const onCopy = () => {
if (!buildUrl) return;
// TODO: use Clipboard.js, after it's been reworked to use this API and handle text.
navigator.clipboard.writeText(buildUrl);
setShowCopiedInfoBar(true);
};
const onShare = async () => {
if (!buildUrl || !navigator.share) return;
// We are on mobile (or on browsers supporting sharing using the system dialog).
const shareData = {
title: 'My GDevelop game',
text: 'Try the game I just created with #gdevelop',
url: buildUrl,
};
try {
await navigator.share(shareData);
} catch (err) {
console.error("Couldn't share the game", err);
}
};
React.useEffect(
() => {
if (exportStep === 'done') {
setIsShareDialogOpen(true);
}
},
[exportStep]
);
const onUpdatePublicBuild = React.useCallback(
async () => {
if (!profile || !game || !build) return;
const { id } = profile;
try {
setIsGameLoading(true);
const updatedGame = await updateGame(
getAuthorizationHeader,
id,
game.id,
{
publicWebBuildId: build.id,
thumbnailUrl: build.id ? getGameThumbnailUrl(build.id) : undefined,
}
);
setGame(updatedGame);
setIsGameLoading(false);
} catch (err) {
console.error('Unable to update the game', err);
setIsGameLoading(false);
}
},
[game, getAuthorizationHeader, profile, build, getGameThumbnailUrl]
);
if (!build && !exportStep) return null;
return (
<>
{exportPending && (
<>
<Text>
<Trans>Just a few seconds while we generate the link...</Trans>
</Text>
<LinearProgress />
</>
)}
<Dialog
title={<Trans>Share your game</Trans>}
actions={[
<FlatButton
key="close"
label={<Trans>Back</Trans>}
primary={false}
onClick={() => setIsShareDialogOpen(false)}
/>,
]}
open={isShareDialogOpen}
onRequestClose={() => setIsShareDialogOpen(false)}
>
{buildUrl && !isGameLoading ? (
<Column noMargin>
<TextFieldWithButtonLayout
noFloatingLabelText
renderTextField={() => (
<TextField
value={buildUrl}
readOnly
fullWidth
endAdornment={
<IconButton onClick={onCopy} tooltip={t`Copy`} edge="end">
<Copy />
</IconButton>
}
/>
)}
renderButton={style => (
<RaisedButton
primary
label={<Trans>Open</Trans>}
onClick={onOpen}
style={style}
/>
)}
/>
{isBuildPublished && navigator.share && (
<Line justifyContent="flex-end">
<FlatButton
label={<Trans>Share</Trans>}
onClick={onShare}
icon={<Share />}
/>
</Line>
)}
{isBuildPublished && !navigator.share && (
<Line justifyContent="space-between">
<Column justifyContent="center">
<AlertMessage kind="info">
<Trans>
This link is unique to your game. Show what you made to
the community!
</Trans>
</AlertMessage>
</Column>
<Column justifyContent="flex-end">
<Line>
<FacebookShareButton
url={buildUrl}
style={styles.icon}
quote={`Try the game I just created with GDevelop.io`}
hashtag="#gdevelop"
>
<FacebookIcon size={32} round />
</FacebookShareButton>
<RedditShareButton
url={buildUrl}
title={`Try the game I just created with r/gdevelop`}
style={styles.icon}
>
<RedditIcon size={32} round />
</RedditShareButton>
<TwitterShareButton
title={`Try the game I just created with GDevelop.io`}
hashtags={['gdevelop']}
url={buildUrl}
style={styles.icon}
>
<TwitterIcon size={32} round />
</TwitterShareButton>
<WhatsappShareButton
title={`Try the game I just created with GDevelop.io`}
url={buildUrl}
style={styles.icon}
>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
<EmailShareButton
subject="My GDevelop game"
body="Try the game I just created with GDevelop.io"
url={buildUrl}
style={styles.icon}
>
<EmailIcon size={32} round />
</EmailShareButton>
</Line>
</Column>
</Line>
)}
{!isBuildPublished && game && (
<Line>
<AlertMessage
kind="info"
renderRightButton={() => (
<RaisedButton
label={<Trans>Update your game</Trans>}
onClick={onUpdatePublicBuild}
/>
)}
>
<Trans>
This link is private so you can share it with friends and
testers. When you're ready you can update your Liluo.io game
page.
</Trans>
</AlertMessage>
</Line>
)}
</Column>
) : (
<Column alignItems="center">
<CircularProgress />
</Column>
)}
<InfoBar
message={<Trans>Copied to clipboard!</Trans>}
visible={showCopiedInfoBar}
hide={() => setShowCopiedInfoBar(false)}
/>
</Dialog>
</>
);
};
export const onlineWebExporter = {
key: 'onlinewebexport',
tabName: 'Web',
name: <Trans>Web</Trans>,
helpPage: '/publishing/web',
};

View File

@@ -0,0 +1,431 @@
// @flow
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import Text from '../../../UI/Text';
import { Column, Line } from '../../../UI/Grid';
import TextField from '../../../UI/TextField';
import {
getBuildArtifactUrl,
getWebBuildThumbnailUrl,
type Build,
} from '../../../Utils/GDevelopServices/Build';
import { type BuildStep } from '../../Builds/BuildStepsProgress';
import RaisedButton from '../../../UI/RaisedButton';
import Window from '../../../Utils/Window';
import Copy from '../../../UI/CustomSvgIcons/Copy';
import Share from '@material-ui/icons/Share';
import InfoBar from '../../../UI/Messages/InfoBar';
import IconButton from '../../../UI/IconButton';
import { CircularProgress, LinearProgress } from '@material-ui/core';
import FlatButton from '../../../UI/FlatButton';
import Dialog from '../../../UI/Dialog';
import {
EmailShareButton,
FacebookShareButton,
RedditShareButton,
TwitterShareButton,
WhatsappShareButton,
EmailIcon,
FacebookIcon,
RedditIcon,
TwitterIcon,
WhatsappIcon,
} from 'react-share';
import { TextFieldWithButtonLayout } from '../../../UI/Layout';
import {
getGame,
getGameUrl,
updateGame,
setGameSlug,
getGameSlugs,
type Game,
type GameSlug,
} from '../../../Utils/GDevelopServices/Game';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
import AlertMessage from '../../../UI/AlertMessage';
import OnlineGamePropertiesDialog from './OnlineGamePropertiesDialog';
import { showErrorBox } from '../../../UI/Messages/MessageBox';
import { type PartialGameChange } from '../../../GameDashboard/PublicGamePropertiesDialog';
const styles = {
icon: {
padding: 5,
},
};
type OnlineGameLinkProps = {|
build: ?Build,
project: gdProject,
onSaveProject: () => Promise<void>,
errored: boolean,
exportStep: BuildStep,
|};
const OnlineGameLink = ({
build,
project,
onSaveProject,
errored,
exportStep,
}: OnlineGameLinkProps) => {
const [showCopiedInfoBar, setShowCopiedInfoBar] = React.useState<boolean>(
false
);
const [isShareDialogOpen, setIsShareDialogOpen] = React.useState<boolean>(
false
);
const [
isOnlineGamePropertiesDialogOpen,
setIsOnlineGamePropertiesDialogOpen,
] = React.useState<boolean>(false);
const [game, setGame] = React.useState<?Game>(null);
const [slug, setSlug] = React.useState<?GameSlug>(null);
const [isGameLoading, setIsGameLoading] = React.useState<boolean>(false);
const { getAuthorizationHeader, profile } = React.useContext(
AuthenticatedUserContext
);
const exportPending = !errored && exportStep !== '' && exportStep !== 'done';
const isBuildComplete = build && build.status === 'complete';
const isBuildPublished = build && game && build.id === game.publicWebBuildId;
const gameUrl = getGameUrl(game, slug);
const buildUrl =
exportPending || !isBuildComplete
? null
: isBuildPublished
? gameUrl
: getBuildArtifactUrl(build, 's3Key');
const loadGame = React.useCallback(
async () => {
const gameId = build && build.gameId;
if (!profile || !gameId) return;
const { id } = profile;
try {
setIsGameLoading(true);
const [game, slugs] = await Promise.all([
getGame(getAuthorizationHeader, id, gameId),
getGameSlugs(getAuthorizationHeader, id, gameId).catch(err => {
console.error('Unable to get the game slug', err);
}),
]);
setGame(game);
if (slugs && slugs.length > 0) {
setSlug(slugs[0]);
}
} catch (err) {
console.error('Unable to load the game', err);
} finally {
setIsGameLoading(false);
}
},
[build, getAuthorizationHeader, profile]
);
React.useEffect(
() => {
// Load game only once
if (!game && isBuildComplete) {
loadGame();
}
},
[game, loadGame, isBuildComplete]
);
const onOpen = () => {
if (!buildUrl) return;
Window.openExternalURL(buildUrl);
};
const onCopy = () => {
if (!buildUrl) return;
// TODO: use Clipboard.js, after it's been reworked to use this API and handle text.
navigator.clipboard.writeText(buildUrl);
setShowCopiedInfoBar(true);
};
const onShare = async () => {
if (!buildUrl || !navigator.share) return;
// We are on mobile (or on browsers supporting sharing using the system dialog).
const shareData = {
title: 'My GDevelop game',
text: 'Try the game I just created with #gdevelop',
url: buildUrl,
};
try {
await navigator.share(shareData);
} catch (err) {
console.error("Couldn't share the game", err);
}
};
React.useEffect(
() => {
if (exportStep === 'done') {
setIsShareDialogOpen(true);
}
},
[exportStep]
);
const onGameUpdate = React.useCallback(
async (
partialGameChange: PartialGameChange,
i18n: I18nType
): Promise<boolean> => {
if (!profile || !game || !build) return false;
const { id } = profile;
try {
setIsGameLoading(true);
const updatedGame = await updateGame(
getAuthorizationHeader,
id,
game.id,
{
gameName: project.getName(),
description: project.getDescription(),
categories: project.getCategories().toJSArray(),
playWithGamepad: project.isPlayableWithGamepad(),
playWithKeyboard: project.isPlayableWithKeyboard(),
playWithMobile: project.isPlayableWithMobile(),
orientation: project.getOrientation(),
publicWebBuildId: build.id,
thumbnailUrl: getWebBuildThumbnailUrl(project, build.id),
discoverable: partialGameChange.discoverable,
}
);
setGame(updatedGame);
const { userSlug, gameSlug } = partialGameChange;
if (userSlug && gameSlug && userSlug === profile.username) {
try {
await setGameSlug(
getAuthorizationHeader,
id,
game.id,
userSlug,
gameSlug
);
setSlug({ username: userSlug, gameSlug: gameSlug, createdAt: 0 });
} catch (error) {
console.error(
'Unable to update the game slug:',
error.response || error.message
);
showErrorBox({
message:
i18n._(
t`Unable to update the game slug. A slug must be 6 to 30 characters long and only contains letters, digits or dashes.`
) +
' ' +
i18n._(t`Verify your internet connection or try again later.`),
rawError: error,
errorId: 'game-slug-update-error',
});
return false;
}
}
} catch (err) {
showErrorBox({
message: i18n._(
t`There was an error updating your game. Verify that your internet connection is working or try again later.`
),
rawError: err,
errorId: 'update-game-error',
});
console.error('Unable to update the game', err);
return false;
} finally {
setIsGameLoading(false);
}
return true;
},
[game, getAuthorizationHeader, profile, build, project]
);
if (!build && !exportStep) return null;
const dialogActions = [
<FlatButton
key="close"
label={<Trans>Close</Trans>}
primary={false}
onClick={() => setIsShareDialogOpen(false)}
/>,
// Ensure there is a game loaded, meaning the user owns the game.
game && buildUrl && !isBuildPublished && (
<RaisedButton
key="publish"
label={<Trans>Verify and Publish to Liluo.io</Trans>}
primary
onClick={() => setIsOnlineGamePropertiesDialogOpen(true)}
/>
),
];
return (
<I18n>
{({ i18n }) => (
<>
{exportPending && (
<>
<Text>
<Trans>Just a few seconds while we generate the link...</Trans>
</Text>
<LinearProgress />
</>
)}
{isShareDialogOpen && (
<Dialog
title={<Trans>Share your game</Trans>}
actions={dialogActions}
open
onRequestClose={() => setIsShareDialogOpen(false)}
>
{buildUrl && !isGameLoading ? (
<Column noMargin>
<TextFieldWithButtonLayout
noFloatingLabelText
renderTextField={() => (
<TextField
value={buildUrl}
readOnly
fullWidth
endAdornment={
<IconButton
onClick={onCopy}
tooltip={t`Copy`}
edge="end"
>
<Copy />
</IconButton>
}
/>
)}
renderButton={style => (
<RaisedButton
primary
label={<Trans>Open</Trans>}
onClick={onOpen}
style={style}
/>
)}
/>
{isBuildPublished && navigator.share && (
<Line justifyContent="flex-end">
<FlatButton
label={<Trans>Share</Trans>}
onClick={onShare}
icon={<Share />}
/>
</Line>
)}
{isBuildPublished && !navigator.share && (
<Line justifyContent="space-between">
<Column justifyContent="center">
<AlertMessage kind="info">
<Trans>
Your game is published! Share it with the community!
</Trans>
</AlertMessage>
</Column>
<Column justifyContent="flex-end">
<Line>
<FacebookShareButton
url={buildUrl}
style={styles.icon}
quote={`Try the game I just created with GDevelop.io`}
hashtag="#gdevelop"
>
<FacebookIcon size={32} round />
</FacebookShareButton>
<RedditShareButton
url={buildUrl}
title={`Try the game I just created with r/gdevelop`}
style={styles.icon}
>
<RedditIcon size={32} round />
</RedditShareButton>
<TwitterShareButton
title={`Try the game I just created with GDevelop.io`}
hashtags={['gdevelop']}
url={buildUrl}
style={styles.icon}
>
<TwitterIcon size={32} round />
</TwitterShareButton>
<WhatsappShareButton
title={`Try the game I just created with GDevelop.io`}
url={buildUrl}
style={styles.icon}
>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
<EmailShareButton
subject="My GDevelop game"
body="Try the game I just created with GDevelop.io"
url={buildUrl}
style={styles.icon}
>
<EmailIcon size={32} round />
</EmailShareButton>
</Line>
</Column>
</Line>
)}
{!isBuildPublished && game && (
<Line>
<AlertMessage kind="info">
<Trans>
This link is private so you can share it with friends
and testers. When you're ready you can update your
Liluo.io game page.
</Trans>
</AlertMessage>
</Line>
)}
</Column>
) : (
<Column alignItems="center">
<CircularProgress />
</Column>
)}
<InfoBar
message={<Trans>Copied to clipboard!</Trans>}
visible={showCopiedInfoBar}
hide={() => setShowCopiedInfoBar(false)}
/>
</Dialog>
)}
{game && build && isOnlineGamePropertiesDialogOpen && (
<OnlineGamePropertiesDialog
project={project}
onSaveProject={onSaveProject}
buildId={build.id}
onClose={() => setIsOnlineGamePropertiesDialogOpen(false)}
onApply={async partialGameChange => {
const isGameUpdated = await onGameUpdate(
partialGameChange,
i18n
);
if (isGameUpdated) {
setIsOnlineGamePropertiesDialogOpen(false);
}
}}
game={game}
slug={slug}
isLoading={isGameLoading}
/>
)}
</>
)}
</I18n>
);
};
export default OnlineGameLink;

View File

@@ -0,0 +1,161 @@
// @flow
import { t, Trans } from '@lingui/macro';
import * as React from 'react';
import { type Game, type GameSlug } from '../../../Utils/GDevelopServices/Game';
import FlatButton from '../../../UI/FlatButton';
import Dialog from '../../../UI/Dialog';
import {
cleanUpGameSlug,
PublicGameProperties,
} from '../../../GameDashboard/PublicGameProperties';
import {
applyPublicPropertiesToProject,
type PartialGameChange,
} from '../../../GameDashboard/PublicGamePropertiesDialog';
import { getWebBuildThumbnailUrl } from '../../../Utils/GDevelopServices/Build';
import RaisedButtonWithSplitMenu from '../../../UI/RaisedButtonWithSplitMenu';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
type Props = {|
project: gdProject,
onSaveProject: () => Promise<void>,
buildId: string,
game: Game,
slug: ?GameSlug,
onClose: () => void,
onApply: PartialGameChange => Promise<void>,
isLoading: boolean,
|};
export const OnlineGamePropertiesDialog = ({
project,
onSaveProject,
buildId,
game,
slug,
onClose,
onApply,
isLoading,
}: Props) => {
const { profile } = React.useContext(AuthenticatedUserContext);
const [name, setName] = React.useState<string>(project.getName());
const [categories, setCategories] = React.useState<string[]>(
project.getCategories().toJSArray()
);
const [description, setDescription] = React.useState<string>(
project.getDescription()
);
const [authorIds, setAuthorIds] = React.useState<string[]>(
project.getAuthorIds().toJSArray()
);
const [playWithKeyboard, setPlayableWithKeyboard] = React.useState<boolean>(
project.isPlayableWithKeyboard()
);
const [playWithGamepad, setPlayableWithGamepad] = React.useState<boolean>(
project.isPlayableWithGamepad()
);
const [playWithMobile, setPlayableWithMobile] = React.useState<boolean>(
project.isPlayableWithMobile()
);
const [userSlug, setUserSlug] = React.useState<string>(
(slug && slug.username) || (profile && profile.username) || ''
);
const [gameSlug, setGameSlug] = React.useState<string>(
(slug && slug.gameSlug) || cleanUpGameSlug(project.getName())
);
const [orientation, setOrientation] = React.useState<string>(
project.getOrientation()
);
const [discoverable, setDiscoverable] = React.useState<boolean>(
!!game.discoverable
);
const thumbnailUrl = getWebBuildThumbnailUrl(project, buildId);
const saveProjectAndPublish = async () => {
await onSaveProject();
await onPublish();
};
const onPublish = async () => {
// Update the project with the new properties before updating the game.
if (
applyPublicPropertiesToProject(project, {
name,
categories: categories || [],
description: description || '',
authorIds,
playWithKeyboard: !!playWithKeyboard,
playWithGamepad: !!playWithGamepad,
playWithMobile: !!playWithMobile,
orientation: orientation || 'default',
})
) {
await onApply({ discoverable, userSlug, gameSlug });
}
};
return (
<Dialog
title={<Trans>Verify your game info before publishing</Trans>}
onRequestClose={onClose}
actions={[
<FlatButton
label={<Trans>Back</Trans>}
key="back"
primary={false}
onClick={onClose}
disabled={isLoading}
/>,
<RaisedButtonWithSplitMenu
label={<Trans>Save project and publish</Trans>}
key="publish"
primary
onClick={() => {
saveProjectAndPublish();
}}
disabled={isLoading}
buildMenuTemplate={i18n => [
{
label: i18n._(t`Publish without saving project`),
click: onPublish,
},
]}
/>,
]}
cannotBeDismissed={isLoading}
open
>
<PublicGameProperties
name={name}
setName={setName}
categories={categories}
setCategories={setCategories}
description={description}
setDescription={setDescription}
project={project}
authorIds={authorIds}
setAuthorIds={setAuthorIds}
setPlayableWithKeyboard={setPlayableWithKeyboard}
playWithKeyboard={playWithKeyboard}
setPlayableWithGamepad={setPlayableWithGamepad}
playWithGamepad={playWithGamepad}
setPlayableWithMobile={setPlayableWithMobile}
playWithMobile={playWithMobile}
setOrientation={setOrientation}
orientation={orientation}
userSlug={userSlug}
setUserSlug={setUserSlug}
gameSlug={gameSlug}
setGameSlug={setGameSlug}
discoverable={discoverable}
setDiscoverable={setDiscoverable}
displayThumbnail
thumbnailUrl={thumbnailUrl}
/>
</Dialog>
);
};
export default OnlineGamePropertiesDialog;

View File

@@ -0,0 +1,28 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Text from '../../../UI/Text';
import { Column, Line } from '../../../UI/Grid';
import OnlineGameLink from './OnlineGameLink';
const ExplanationHeader = () => (
<Column noMargin alignItems="center" justifyContent="center">
<Line>
<Text align="center">
<Trans>
Generate a unique link, playable from any computer or mobile phone's
browser.
</Trans>
</Text>
</Line>
</Column>
);
const onlineWebExporter = {
key: 'onlinewebexport',
tabName: 'Web',
name: <Trans>Web</Trans>,
helpPage: '/publishing/web',
};
export { onlineWebExporter, ExplanationHeader, OnlineGameLink };

View File

@@ -17,10 +17,9 @@ import {
type ExportPipeline,
type ExportPipelineContext,
} from '../ExportPipeline.flow';
import { type BuildStep } from '../Builds/BuildStepsProgress';
import {
ExplanationHeader,
WebProjectLink,
OnlineGameLink,
} from '../GenericExporters/OnlineWebExport';
const path = optionalRequire('path');
const os = optionalRequire('os');
@@ -67,17 +66,19 @@ export const localOnlineWebExportPipeline: ExportPipeline<
renderLaunchButtonLabel: () => <Trans>Generate link</Trans>,
renderCustomStepsProgress: (
build: ?Build,
errored: boolean,
exportStep: BuildStep,
getGameThumbnailUrl: (buildId: string) => ?string
) => (
<WebProjectLink
renderCustomStepsProgress: ({
build,
project,
onSaveProject,
errored,
exportStep,
}) => (
<OnlineGameLink
build={build}
project={project}
onSaveProject={onSaveProject}
errored={errored}
exportStep={exportStep}
getGameThumbnailUrl={getGameThumbnailUrl}
/>
),

View File

@@ -0,0 +1,68 @@
{
"А": "A",
"Б": "B",
"В": "V",
"Г": "G",
"Д": "D",
"Е": "E",
"Ё": "E",
"Ж": "Zh",
"З": "Z",
"И": "I",
"Й": "J",
"К": "K",
"Л": "L",
"М": "M",
"Н": "N",
"О": "O",
"П": "P",
"Р": "R",
"С": "S",
"Т": "T",
"У": "U",
"Ф": "F",
"Х": "H",
"Ц": "Ts",
"Ч": "Ch",
"Ш": "Sh",
"Щ": "Shch",
"Ы": "Y",
"Э": "E",
"Ю": "Yu",
"Я": "Ya",
"а": "a",
"б": "b",
"в": "v",
"г": "g",
"д": "d",
"е": "e",
"ё": "e",
"ж": "zh",
"з": "z",
"и": "i",
"й": "j",
"к": "k",
"л": "l",
"м": "m",
"н": "n",
"о": "o",
"п": "p",
"р": "r",
"с": "s",
"т": "t",
"у": "u",
"ф": "f",
"х": "h",
"ц": "ts",
"ч": "ch",
"ш": "sh",
"щ": "shch",
"ы": "y",
"э": "e",
"ю": "yu",
"я": "ya",
"Ъ": "",
"ъ": "",
"Ь": "",
"ь": ""
}

View File

@@ -1,45 +1,22 @@
// @flow
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { Card, CardActions, CardHeader, Chip, Paper } from '@material-ui/core';
import { Card, CardActions, CardHeader, Chip } from '@material-ui/core';
import * as React from 'react';
import { Column, Line, Spacer } from '../UI/Grid';
import RaisedButton from '../UI/RaisedButton';
import { getGameUrl, type Game } from '../Utils/GDevelopServices/Game';
import TimelineIcon from '@material-ui/icons/Timeline';
import PlaylistPlayIcon from '@material-ui/icons/PlaylistPlay';
import TuneIcon from '@material-ui/icons/Tune';
import { ResponsiveLineStackLayout } from '../UI/Layout';
import Window from '../Utils/Window';
import FlatButton from '../UI/FlatButton';
import EmptyMessage from '../UI/EmptyMessage';
const styles = {
image: {
display: 'block',
objectFit: 'cover',
},
thumbnail: {
width: 240,
height: 135,
},
};
import { GameThumbnail } from './GameThumbnail';
type Props = {|
game: Game,
isCurrentGame: boolean,
onOpenDetails: () => void,
onOpenBuilds: () => void,
onOpenAnalytics: () => void,
onOpenGameManager: () => void,
|};
export const GameCard = ({
game,
isCurrentGame,
onOpenDetails,
onOpenBuilds,
onOpenAnalytics,
}: Props) => {
export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => {
const openGameUrl = () => {
const url = getGameUrl(game);
if (!url) return;
@@ -49,31 +26,11 @@ export const GameCard = ({
<I18n>
{({ i18n }) => (
<Card key={game.id}>
<Line>
{game.thumbnailUrl ? (
<img
src={game.thumbnailUrl}
style={{
...styles.image,
...styles.thumbnail,
}}
alt={game.gameName}
title={game.gameName}
/>
) : (
<Paper
variant="outlined"
style={{
...styles.thumbnail,
whiteSpace: 'normal',
display: 'flex',
}}
>
<EmptyMessage>
<Trans>No thumbnail set</Trans>
</EmptyMessage>
</Paper>
)}
<ResponsiveLineStackLayout>
<GameThumbnail
gameName={game.gameName}
thumbnailUrl={game.thumbnailUrl}
/>
<Column expand>
<CardHeader
title={game.gameName}
@@ -95,7 +52,13 @@ export const GameCard = ({
<Spacer />
<Chip
size="small"
label={<Trans>Published on Liluo</Trans>}
label={
game.discoverable ? (
<Trans>Discoverable on Liluo.io</Trans>
) : (
<Trans>Published on Liluo.io</Trans>
)
}
/>
</>
)}
@@ -110,30 +73,19 @@ export const GameCard = ({
>
{game.publicWebBuildId && (
<RaisedButton
label={<Trans>Open</Trans>}
label={<Trans>Open in browser</Trans>}
onClick={openGameUrl}
primary
/>
)}
<FlatButton
icon={<TuneIcon />}
label={<Trans>Details</Trans>}
onClick={onOpenDetails}
/>
<FlatButton
icon={<PlaylistPlayIcon />}
label={<Trans>Builds</Trans>}
onClick={onOpenBuilds}
/>
<FlatButton
icon={<TimelineIcon />}
label={<Trans>Analytics</Trans>}
onClick={onOpenAnalytics}
<RaisedButton
label={<Trans>Manage game</Trans>}
onClick={onOpenGameManager}
primary
/>
</ResponsiveLineStackLayout>
</CardActions>
</Column>
</Line>
</ResponsiveLineStackLayout>
</Card>
)}
</I18n>

View File

@@ -1,6 +1,5 @@
// @flow
import { Trans } from '@lingui/macro';
import { t } from '@lingui/macro';
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
@@ -13,6 +12,7 @@ import {
deleteGame,
getPublicGame,
setGameUserAcls,
setGameSlug,
getAclsFromUserIds,
getCategoryName,
} from '../Utils/GDevelopServices/Game';
@@ -41,13 +41,14 @@ import PlaceholderLoader from '../UI/PlaceholderLoader';
import {
PublicGamePropertiesDialog,
type PartialGameChange,
} from '../ProjectManager/PublicGamePropertiesDialog';
} from './PublicGamePropertiesDialog';
import TextField from '../UI/TextField';
import KeyboardIcon from '@material-ui/icons/Keyboard';
import SportsEsportsIcon from '@material-ui/icons/SportsEsports';
import SmartphoneIcon from '@material-ui/icons/Smartphone';
import Crown from '../UI/CustomSvgIcons/Crown';
import { showErrorBox } from '../UI/Messages/MessageBox';
import { showErrorBox, showWarningBox } from '../UI/Messages/MessageBox';
import LeaderboardAdmin from './LeaderboardAdmin';
const styles = {
tableRowStatColumn: {
@@ -55,7 +56,11 @@ const styles = {
},
};
export type GamesDetailsTab = 'details' | 'builds' | 'analytics';
export type GamesDetailsTab =
| 'details'
| 'builds'
| 'analytics'
| 'leaderboards';
type Props = {|
game: Game,
@@ -81,10 +86,16 @@ export const GameDetailsDialog = ({
const [gameRollingMetrics, setGameMetrics] = React.useState<?GameMetrics>(
null
);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [
gameUnregisterErrorText,
setGameUnregisterErrorText,
] = React.useState<?string>(null);
const [gameRollingMetricsError, setGameMetricsError] = React.useState<?Error>(
null
);
const [isGameMetricsLoading, setIsGameMetricsLoading] = React.useState(false);
const [isGameUpdating, setIsGameUpdating] = React.useState(false);
const yesterdayIsoDate = formatISO(subDays(new Date(), 1), {
representation: 'date',
@@ -152,51 +163,113 @@ export const GameDetailsDialog = ({
[loadPublicGame]
);
const handleGameUpdated = React.useCallback(
(updatedGame: Game) => {
// Set Public Game to null to show the loader.
// It will be refetched thanks to loadPublicGame, because Game is updated.
setPublicGame(null);
onGameUpdated(updatedGame);
},
[onGameUpdated]
);
const updateGameFromProject = async (
partialGameChange: PartialGameChange,
i18n: I18nType
) => {
if (!project || !profile) return;
): Promise<boolean> => {
if (!project || !profile) return false;
const { id } = profile;
// Set public game to null as it will be refetched automatically by the callback above.
setPublicGame(null);
const ownerIds = partialGameChange.ownerIds;
if (!ownerIds || !ownerIds.length) {
showWarningBox(
i18n._(
t`You must select at least one user to be the owner of the game.`
),
{ delayToNextTick: true }
);
return false;
}
try {
setIsGameUpdating(true);
const gameId = project.getProjectUuid();
const updatedGame = await updateGame(getAuthorizationHeader, id, gameId, {
authorName: project.getAuthor() || 'Unspecified publisher',
gameName: project.getName() || 'Untitle game',
gameName: project.getName() || 'Untitled game',
categories: project.getCategories().toJSArray() || [],
description: project.getDescription() || '',
playWithKeyboard: project.isPlayableWithKeyboard(),
playWithGamepad: project.isPlayableWithGamepad(),
playWithMobile: project.isPlayableWithMobile(),
orientation: project.getOrientation(),
// The thumbnailUrl is updated only when a build is made public.
discoverable: partialGameChange.discoverable,
});
if (
partialGameChange.userSlug &&
partialGameChange.gameSlug &&
partialGameChange.userSlug === profile.username
) {
try {
await setGameSlug(
getAuthorizationHeader,
id,
gameId,
partialGameChange.userSlug,
partialGameChange.gameSlug
);
} catch (error) {
console.error(
'Unable to update the game slug:',
error.response || error.message
);
showErrorBox({
message:
i18n._(
t`Unable to update the game slug. A slug must be 6 to 30 characters long and only contains letters, digits or dashes.`
) +
' ' +
i18n._(t`Verify your internet connection or try again later.`),
rawError: error,
errorId: 'game-slug-update-error',
});
setIsGameUpdating(false);
return false;
}
}
try {
const authorAcls = getAclsFromUserIds(
project.getAuthorIds().toJSArray()
);
const ownerAcls = getAclsFromUserIds(partialGameChange.ownerIds);
const ownerAcls = getAclsFromUserIds(ownerIds);
await setGameUserAcls(getAuthorizationHeader, id, gameId, {
ownership: ownerAcls,
author: authorAcls,
});
} catch (error) {
console.error('Unable to update the game owners or authors:', error);
console.error(
'Unable to update the game owners or authors:',
error.response || error.message
);
showErrorBox({
message:
i18n._(t`Unable to update the game owners or authors.`) +
i18n._(
t`Unable to update the game owners or authors. Have you removed yourself from the owners?`
) +
' ' +
i18n._(t`Verify your internet connection or try again later.`),
rawError: error,
errorId: 'game-acls-update-error',
});
setIsGameUpdating(false);
return false;
}
onGameUpdated(updatedGame);
handleGameUpdated(updatedGame);
} catch (error) {
console.error('Unable to update the game:', error);
console.error(
'Unable to update the game:',
error.response || error.message
);
showErrorBox({
message:
i18n._(t`Unable to update the game details.`) +
@@ -205,18 +278,39 @@ export const GameDetailsDialog = ({
rawError: error,
errorId: 'game-details-update-error',
});
setIsGameUpdating(false);
return false;
}
setIsGameUpdating(false);
return true;
};
const unregisterGame = async () => {
const unregisterGame = async (i18n: I18nType) => {
if (!profile) return;
const { id } = profile;
setGameUnregisterErrorText(null);
setIsLoading(true);
try {
setIsGameUpdating(true);
await deleteGame(getAuthorizationHeader, id, game.id);
onGameDeleted();
} catch (error) {
console.error('Unable to delete the game:', error);
if (
error.response &&
error.response.data &&
error.response.data.code === 'game-deletion/leaderboards-exist'
) {
setGameUnregisterErrorText(
i18n._(
t`You cannot unregister a game that has active leaderboards. To delete them, go in the Leaderboards tab, and delete them one by one.`
)
);
}
} finally {
setIsGameUpdating(false);
setIsLoading(false);
}
};
@@ -226,8 +320,7 @@ export const GameDetailsDialog = ({
const { id } = profile;
try {
// Set public game to null as it will be refetched automatically by the callback above.
setPublicGame(null);
setIsGameUpdating(true);
const updatedGame = await updateGame(
getAuthorizationHeader,
id,
@@ -236,12 +329,14 @@ export const GameDetailsDialog = ({
publicWebBuildId: null,
}
);
onGameUpdated(updatedGame);
handleGameUpdated(updatedGame);
} catch (err) {
console.error('Unable to update the game', err);
} finally {
setIsGameUpdating(false);
}
},
[game, getAuthorizationHeader, profile, onGameUpdated]
[game, getAuthorizationHeader, profile, handleGameUpdated]
);
const authorUsernames =
@@ -268,25 +363,41 @@ export const GameDetailsDialog = ({
}
open
noMargin
onRequestClose={onClose}
flexColumnBody
fullHeight={currentTab === 'leaderboards'}
onRequestClose={() => {
if (!isLoading) onClose();
}}
maxWidth="md"
actions={[
<FlatButton
label={<Trans>Close</Trans>}
disabled={isLoading}
onClick={onClose}
key="close"
/>,
]}
secondaryActions={[
<HelpButton key="help" helpPagePath="/interface/games-dashboard" />,
<HelpButton
key="help"
helpPagePath={
currentTab === 'leaderboards'
? '/interface/games-dashboard/leaderboard-administration'
: '/interface/games-dashboard'
}
/>,
]}
>
<Tabs value={currentTab} onChange={setCurrentTab}>
<Tab label={<Trans>Details</Trans>} value="details" />
<Tab label={<Trans>Builds</Trans>} value="builds" />
<Tab label={<Trans>Analytics</Trans>} value="analytics" />
<Tab label={<Trans>Leaderboards</Trans>} value="leaderboards" />
</Tabs>
<Line>
<Line expand>
{currentTab === 'leaderboards' ? (
<LeaderboardAdmin gameId={game.id} onLoading={setIsLoading} />
) : null}
{currentTab === 'details' ? (
publicGameError ? (
<PlaceholderError onRetry={loadPublicGame}>
@@ -437,9 +548,10 @@ export const GameDetailsDialog = ({
if (!answer) return;
unregisterGame();
unregisterGame(i18n);
}}
label={<Trans>Unregister this game</Trans>}
disabled={isGameUpdating}
/>
<Spacer />
{publicGame.publicWebBuildId && (
@@ -447,14 +559,15 @@ export const GameDetailsDialog = ({
<RaisedButton
onClick={() => {
const answer = Window.showConfirmDialog(
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo unique game URL not accessible anymore. \n\nYou can decide anytime to publish it again.'
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo.io unique game URL not accessible anymore. \n\nYou can decide at any time to publish it again.'
);
if (!answer) return;
unpublishGame();
}}
label={<Trans>Unpublish from Liluo</Trans>}
label={<Trans>Unpublish from Liluo.io</Trans>}
disabled={isGameUpdating}
/>
<Spacer />
</>
@@ -463,9 +576,14 @@ export const GameDetailsDialog = ({
primary
onClick={() => setIsPublicGamePropertiesDialogOpen(true)}
label={<Trans>Edit game details</Trans>}
disabled={!isGameOpenedAsProject}
disabled={!isGameOpenedAsProject || isGameUpdating}
/>
</Line>
{gameUnregisterErrorText ? (
<PlaceholderError kind="error">
{gameUnregisterErrorText}
</PlaceholderError>
) : null}
</ColumnStackLayout>
)
) : null}
@@ -504,10 +622,8 @@ export const GameDetailsDialog = ({
<Trans>Last week sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame &&
publicGame.metrics &&
publicGame.metrics.lastWeekSessionsCount
? publicGame.metrics.lastWeekSessionsCount
{publicGame && publicGame.cachedLastWeekSessionsCount
? publicGame.cachedLastWeekSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
@@ -516,10 +632,8 @@ export const GameDetailsDialog = ({
<Trans>Last year sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame &&
publicGame.metrics &&
publicGame.metrics.lastYearSessionsCount
? publicGame.metrics.lastYearSessionsCount
{publicGame && publicGame.cachedLastYearSessionsCount
? publicGame.cachedLastYearSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
@@ -648,16 +762,21 @@ export const GameDetailsDialog = ({
)
) : null}
</Line>
{publicGame && project && (
{publicGame && project && isPublicGamePropertiesDialogOpen && (
<PublicGamePropertiesDialog
open={isPublicGamePropertiesDialogOpen}
project={project}
publicGame={publicGame}
onApply={partialGameChange => {
setIsPublicGamePropertiesDialogOpen(false);
updateGameFromProject(partialGameChange, i18n);
onApply={async partialGameChange => {
const isGameUpdated = await updateGameFromProject(
partialGameChange,
i18n
);
if (isGameUpdated) {
setIsPublicGamePropertiesDialogOpen(false);
}
}}
onClose={() => setIsPublicGamePropertiesDialogOpen(false)}
isLoading={isGameUpdating}
/>
)}
</Dialog>

View File

@@ -0,0 +1,48 @@
// @flow
import { Trans } from '@lingui/macro';
import { Paper } from '@material-ui/core';
import * as React from 'react';
import EmptyMessage from '../UI/EmptyMessage';
const styles = {
image: {
display: 'block',
objectFit: 'cover',
},
thumbnail: {
// 16/9 format
width: 272,
height: 153,
},
};
type Props = {|
thumbnailUrl?: string,
gameName: string,
|};
export const GameThumbnail = ({ thumbnailUrl, gameName }: Props) =>
thumbnailUrl ? (
<img
src={thumbnailUrl}
style={{
...styles.image,
...styles.thumbnail,
}}
alt={gameName}
title={gameName}
/>
) : (
<Paper
variant="outlined"
style={{
...styles.thumbnail,
whiteSpace: 'normal',
display: 'flex',
}}
>
<EmptyMessage>
<Trans>No thumbnail set</Trans>
</EmptyMessage>
</Paper>
);

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