Compare commits

...

53 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
Florian Rival
3ad7585f92 Add a linter rule to avoid importing prop-types
Don't show in changelog
2022-03-24 11:48:07 +01:00
AlexandreS
acb69c447d Improve events sheet search with new shortcuts and put back the "Edit variable" row in variable editor 2022-03-23 15:40:57 +01:00
Elairyx
fa928b35cb Add autocompletion for the "Simulate control" action of the Top Down movement behavior (#3767) 2022-03-21 23:15:02 +01:00
Florian Rival
67810b79ce Refactor some redundant code in Text Input tests (#3768)
Don't show in changelog
2022-03-21 23:08:54 +01:00
Clément Pasteau
0a77e0264d Bump version to 5.0.130 2022-03-21 11:53:52 +01:00
Clément Pasteau
4cc8d15f4b Fix invalid game thumbnail URL 2022-03-21 11:42:36 +01:00
240 changed files with 10631 additions and 2426 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

@@ -58,7 +58,7 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
});
};
it('creates the DOM element', async () => {
const setupObjectAndGetDomElementContainer = async () => {
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
loadScene(runtimeScene);
@@ -70,10 +70,6 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
const object = makeTextInputRuntimeObject(runtimeScene);
runtimeScene.addObject(object);
// Check the default size.
expect(object.getWidth()).to.be(300);
expect(object.getHeight()).to.be(30);
// Check that the DOM element was created
const gameDomElementContainer = runtimeGame
.getRenderer()
@@ -83,6 +79,20 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
'Expected getDomElementContainer to return a valid container.'
);
return { runtimeScene, gameDomElementContainer, object };
};
it('creates the DOM element', async () => {
const {
runtimeScene,
gameDomElementContainer,
object,
} = await setupObjectAndGetDomElementContainer();
// Check the default size.
expect(object.getWidth()).to.be(300);
expect(object.getHeight()).to.be(30);
expect(gameDomElementContainer.hasChildNodes()).to.be(true);
const inputElement = gameDomElementContainer.querySelector('input');
@@ -98,25 +108,11 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
});
it('destroys the DOM element when the scene is paused/resumed/stopped', async () => {
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
loadScene(runtimeScene);
// Make sure the renderer is created (to test the real DOM element creation/update)
const gameContainer = document.createElement('div');
runtimeGame.getRenderer().createStandardCanvas(gameContainer);
const object = makeTextInputRuntimeObject(runtimeScene);
runtimeScene.addObject(object);
// Check that the DOM element was created
const gameDomElementContainer = runtimeGame
.getRenderer()
.getDomElementContainer();
if (!gameDomElementContainer)
throw new Error(
'Expected getDomElementContainer to return a valid container.'
);
const {
runtimeScene,
gameDomElementContainer,
object,
} = await setupObjectAndGetDomElementContainer();
expect(gameDomElementContainer.querySelector('input')).not.to.be(null);
@@ -129,28 +125,17 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
runtimeScene.unloadScene();
expect(gameDomElementContainer.querySelector('input')).to.be(null);
// Clean up - not mandatory but to avoid overloading the testing browser.
runtimeScene.unloadScene();
});
it('changes the DOM element when the object type is updated', async () => {
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
loadScene(runtimeScene);
// Make sure the renderer is created (to test the real DOM element creation/update)
const gameContainer = document.createElement('div');
runtimeGame.getRenderer().createStandardCanvas(gameContainer);
const object = makeTextInputRuntimeObject(runtimeScene);
runtimeScene.addObject(object);
// Check that the DOM element was created
const gameDomElementContainer = runtimeGame
.getRenderer()
.getDomElementContainer();
if (!gameDomElementContainer)
throw new Error(
'Expected getDomElementContainer to return a valid container.'
);
const {
runtimeScene,
gameDomElementContainer,
object,
} = await setupObjectAndGetDomElementContainer();
expect(gameDomElementContainer.querySelector('input')).not.to.be(null);
@@ -171,25 +156,11 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
});
it('hides the DOM element when the object or layer is hidden', async () => {
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
loadScene(runtimeScene);
// Make sure the renderer is created (to test the real DOM element creation/update)
const gameContainer = document.createElement('div');
runtimeGame.getRenderer().createStandardCanvas(gameContainer);
const object = makeTextInputRuntimeObject(runtimeScene);
runtimeScene.addObject(object);
// Check that the DOM element was created
const gameDomElementContainer = runtimeGame
.getRenderer()
.getDomElementContainer();
if (!gameDomElementContainer)
throw new Error(
'Expected getDomElementContainer to return a valid container.'
);
const {
runtimeScene,
gameDomElementContainer,
object,
} = await setupObjectAndGetDomElementContainer();
const inputElement = gameDomElementContainer.querySelector('input');
if (!inputElement) throw new Error('Expected input element to be found');
@@ -224,25 +195,11 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
});
it('hides the DOM element when the object is far from the camera', async () => {
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
loadScene(runtimeScene);
// Make sure the renderer is created (to test the real DOM element creation/update)
const gameContainer = document.createElement('div');
runtimeGame.getRenderer().createStandardCanvas(gameContainer);
const object = makeTextInputRuntimeObject(runtimeScene);
runtimeScene.addObject(object);
// Check that the DOM element was created
const gameDomElementContainer = runtimeGame
.getRenderer()
.getDomElementContainer();
if (!gameDomElementContainer)
throw new Error(
'Expected getDomElementContainer to return a valid container.'
);
const {
runtimeScene,
gameDomElementContainer,
object,
} = await setupObjectAndGetDomElementContainer();
const inputElement = gameDomElementContainer.querySelector('input');
if (!inputElement) throw new Error('Expected input element to be found');

View File

@@ -105,7 +105,9 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
"res/conditions/keyboard.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
.AddParameter("string", _("Key"))
.AddParameter("stringWithSelector",
_("Key"),
"[\"Left\", \"Right\", \"Up\", \"Down\"]")
.MarkAsAdvanced()
.SetFunctionName("SimulateControl")
.SetIncludeFile(
@@ -143,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."),
@@ -530,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",
@@ -122,6 +122,10 @@
"Trans"
],
"message": "Please import Trans from @lingui/macro"
},
{
"name": "prop-types",
"message": "Don't import anything from prop-types - use Flow/TypeScript types instead."
}
]
}

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

@@ -18,6 +18,7 @@ import {
import RaisedButton from '../UI/RaisedButton';
import { ColumnStackLayout } from '../UI/Layout';
import {
shouldBrowsePrevious,
shouldCloseOrCancel,
shouldValidate,
} from '../UI/KeyboardShortcuts/InteractionKeys';
@@ -37,6 +38,7 @@ type Props = {|
export type SearchPanelInterface = {|
focus: () => void,
markSearchResultsDirty: () => void,
isSearchOngoing: () => boolean,
|};
const SearchPanel = (
@@ -54,19 +56,6 @@ const SearchPanel = (
) => {
const searchTextField = React.useRef<?TextField>(null);
const focusSearchField = React.useCallback((): void => {
if (searchTextField.current) searchTextField.current.focus();
}, []);
const markSearchResultsDirty = React.useCallback((): void => {
setSearchResultsDirty(true);
}, []);
React.useImperativeHandle(ref, () => ({
focus: focusSearchField,
markSearchResultsDirty,
}));
const [searchText, setSearchText] = React.useState<string>('');
const [replaceText, setReplaceText] = React.useState<string>('');
const [matchCase, setMatchCase] = React.useState<boolean>(false);
@@ -89,6 +78,27 @@ const SearchPanel = (
'search-and-replace' | 'search-in-event-sentences'
>('search-and-replace');
const isSearchOngoing = React.useCallback(
(): boolean => {
return !!searchText && !searchResultsDirty;
},
[searchText, searchResultsDirty]
);
const focusSearchField = React.useCallback((): void => {
if (searchTextField.current) searchTextField.current.focus();
}, []);
const markSearchResultsDirty = React.useCallback((): void => {
setSearchResultsDirty(true);
}, []);
React.useImperativeHandle(ref, () => ({
isSearchOngoing,
focus: focusSearchField,
markSearchResultsDirty,
}));
React.useEffect(
() => {
setSearchResultsDirty(true);
@@ -171,7 +181,9 @@ const SearchPanel = (
setSearchText(searchText);
}}
onKeyPress={event => {
if (shouldValidate(event)) {
if (shouldBrowsePrevious(event)) {
onGoToPreviousSearchResult();
} else if (shouldValidate(event)) {
if (!searchResultsDirty) {
onGoToNextSearchResult();
} else {

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 = {|
@@ -317,6 +322,14 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
_toggleSearchPanel = () => {
this.setState(
state => {
if (
state.showSearchPanel &&
this._searchPanel &&
this._searchPanel.isSearchOngoing()
) {
this._searchPanel.focus();
return;
}
const show = !state.showSearchPanel;
if (!show) {
if (this._eventSearcher) this._eventSearcher.reset();
@@ -1082,6 +1095,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
eventsList.insertEvent(event, eventsList.getEventsCount())
);
this.props.onBeginCreateEventsFunction();
this.setState({
serializedEventsToExtract: serializeToJSObject(eventsList),
});
@@ -1555,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>

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