mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
47 Commits
fix/ghost-
...
v5.0.132
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bb3abdb1fa | ||
![]() |
cdddcafa68 | ||
![]() |
14175c334e | ||
![]() |
6bd4dff03e | ||
![]() |
ceec39f6a2 | ||
![]() |
5abf80a0c9 | ||
![]() |
7f955d8703 | ||
![]() |
781dd42ccb | ||
![]() |
e584fa952d | ||
![]() |
5681667dde | ||
![]() |
d16f04f4a2 | ||
![]() |
e9b464beba | ||
![]() |
7597dbe0d1 | ||
![]() |
4777f0a824 | ||
![]() |
5b2532f8f3 | ||
![]() |
2c43de5120 | ||
![]() |
124ce1101d | ||
![]() |
eb3d6c2670 | ||
![]() |
f737fa479f | ||
![]() |
f93b3bc3b4 | ||
![]() |
5c6eb2dadb | ||
![]() |
3e6ca186f8 | ||
![]() |
860e9d36e4 | ||
![]() |
c8b461cc5f | ||
![]() |
36cdc5720a | ||
![]() |
a7cd53b921 | ||
![]() |
01a25400ff | ||
![]() |
2b484c0cf1 | ||
![]() |
13204e4b53 | ||
![]() |
c6d6466d54 | ||
![]() |
6cf737ac70 | ||
![]() |
5b8cdbbace | ||
![]() |
a7df37a0dc | ||
![]() |
7721f56dde | ||
![]() |
08ada4f595 | ||
![]() |
caa46a2fb0 | ||
![]() |
660cdd753c | ||
![]() |
3b9a1354ee | ||
![]() |
02fce4132a | ||
![]() |
41241d75e1 | ||
![]() |
fc3e73f360 | ||
![]() |
230493039c | ||
![]() |
50326e4d21 | ||
![]() |
cec7960a7d | ||
![]() |
cec901d4c2 | ||
![]() |
29f0567140 | ||
![]() |
94af33c815 |
@@ -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
1
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
/Binaries/.embuild*
|
||||
/Binaries/build*
|
||||
/Binaries/embuild*
|
||||
/emsdk
|
||||
*.dll
|
||||
*.exe
|
||||
*.a
|
||||
|
25
.gitpod.yml
Normal file
25
.gitpod.yml
Normal 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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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());
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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);
|
||||
|
@@ -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"))
|
||||
|
@@ -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);
|
||||
|
@@ -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",
|
||||
|
@@ -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"),
|
||||
|
@@ -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"),
|
||||
|
@@ -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
|
||||
|
@@ -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 ¶meterType) {
|
||||
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";
|
||||
|
@@ -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()),
|
||||
|
@@ -77,8 +77,6 @@ bool SerializerElement::GetBoolAttribute(const gd::String& name,
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Bool attribute \"" << name << "\" not found, returning "
|
||||
<< defaultValue;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
@@ -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") {
|
||||
|
@@ -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);
|
||||
});
|
||||
|
258
Extensions/Leaderboards/JsExtension.js
Normal file
258
Extensions/Leaderboards/JsExtension.js
Normal 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 [];
|
||||
},
|
||||
};
|
645
Extensions/Leaderboards/leaderboardstools.ts
Normal file
645
Extensions/Leaderboards/leaderboardstools.ts
Normal 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
234
Extensions/Leaderboards/sha256.d.ts
vendored
Normal 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
|
||||
);
|
||||
}
|
21
Extensions/Leaderboards/sha256.js
Normal file
21
Extensions/Leaderboards/sha256.js
Normal file
File diff suppressed because one or more lines are too long
68
Extensions/Leaderboards/tests/leaderboardstools.spec.js
Normal file
68
Extensions/Leaderboards/tests/leaderboardstools.spec.js
Normal 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(
|
||||
'aPιΥÉᚱnÀⅯeThatᎥsTooⅬonᏀToBeՏaѵÊĐThisPartAppears'
|
||||
)
|
||||
).to.be('aEAeThatsTooonToBeaEThisPartAp');
|
||||
});
|
||||
});
|
||||
});
|
@@ -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")
|
||||
|
@@ -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);
|
||||
|
@@ -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")
|
||||
|
@@ -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",
|
||||
|
@@ -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(' ', {
|
||||
|
@@ -145,6 +145,28 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.SetIncludeFile(
|
||||
"TopDownMovementBehavior/TopDownMovementRuntimeBehavior.h");
|
||||
|
||||
aut.AddScopedCondition("IsUsingControl",
|
||||
_("Control pressed or simulated"),
|
||||
_("A control was applied from a default control or simulated by an action."),
|
||||
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
|
||||
_("Controls"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Key"),
|
||||
"[\"Left\", \"Right\", \"Up\", \"Down\", \"Stick\"]")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddExpression("StickAngle",
|
||||
_("Stick angle"),
|
||||
_("Return the angle of the simulated stick input (in degrees)"),
|
||||
_("Controls"),
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior");
|
||||
|
||||
aut.AddCondition("IsMoving",
|
||||
_("Is moving"),
|
||||
_("Check if the object is moving."),
|
||||
@@ -532,6 +554,30 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.SetIncludeFile(
|
||||
"TopDownMovementBehavior/TopDownMovementRuntimeBehavior.h");
|
||||
|
||||
aut.AddScopedAction("SetVelocityX",
|
||||
_("Speed on the X axis"),
|
||||
_("Change the speed on the X axis of the movement"),
|
||||
_("the speed on the X axis of the movement"),
|
||||
_("Movement"),
|
||||
"CppPlatform/Extensions/topdownmovementicon24.png",
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddScopedAction("SetVelocityY",
|
||||
_("Speed on the Y axis"),
|
||||
_("Change the speed on the Y axis of the movement"),
|
||||
_("the speed on the Y axis of the movement"),
|
||||
_("Movement"),
|
||||
"CppPlatform/Extensions/topdownmovementicon24.png",
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddExpressionAndConditionAndAction("number",
|
||||
"MovementAngleOffset",
|
||||
_("Movement angle offset"),
|
||||
|
@@ -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");
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
|
@@ -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");
|
||||
|
@@ -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(
|
||||
|
@@ -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"
|
||||
|
@@ -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");
|
||||
|
@@ -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");
|
||||
|
@@ -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.
|
||||
|
@@ -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)
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -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,
|
||||
|
@@ -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.
|
||||
|
3
GDJS/Runtime/types/global-types.d.ts
vendored
3
GDJS/Runtime/types/global-types.d.ts
vendored
@@ -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;
|
||||
|
||||
|
@@ -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',
|
||||
|
871
GDJS/tests/games/count-instances/Instances Count test.json
Normal file
871
GDJS/tests/games/count-instances/Instances Count test.json
Normal 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": []
|
||||
}
|
BIN
GDJS/tests/games/count-instances/assets/Blue laser (08).png
Normal file
BIN
GDJS/tests/games/count-instances/assets/Blue laser (08).png
Normal file
Binary file not shown.
After Width: | Height: | Size: 882 B |
BIN
GDJS/tests/games/count-instances/assets/Green laser (01).png
Normal file
BIN
GDJS/tests/games/count-instances/assets/Green laser (01).png
Normal file
Binary file not shown.
After Width: | Height: | Size: 709 B |
@@ -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',
|
||||
|
@@ -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);
|
||||
|
140
GDJS/tests/tests/objecttools.js
Normal file
140
GDJS/tests/tests/objecttools.js
Normal 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);
|
||||
});
|
||||
});
|
76
GDevelop.js/TestUtils/CodeGenerationHelpers.js
Normal file
76
GDevelop.js/TestUtils/CodeGenerationHelpers.js
Normal 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,
|
||||
};
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
@@ -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
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
22
GDevelop.js/package-lock.json
generated
22
GDevelop.js/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
36
newIDE/app/package-lock.json
generated
36
newIDE/app/package-lock.json
generated
@@ -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=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -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",
|
||||
|
7
newIDE/app/public/JsPlatform/Extensions/leaderboard.svg
Normal file
7
newIDE/app/public/JsPlatform/Extensions/leaderboard.svg
Normal 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 |
@@ -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={() => {
|
||||
|
@@ -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>
|
||||
)
|
||||
|
@@ -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]
|
||||
);
|
||||
};
|
@@ -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);
|
||||
}}
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -162,7 +162,7 @@ export const ExtensionDependenciesEditor = ({
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<IconButton
|
||||
tooltip={t`Remove`}
|
||||
tooltip={t`Delete`}
|
||||
onClick={() => {
|
||||
eventsFunctionsExtension.removeDependencyAt(index);
|
||||
forceUpdate();
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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,
|
||||
|
@@ -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. */
|
||||
}
|
||||
|
@@ -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
|
||||
> {
|
||||
|
@@ -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];
|
||||
|
@@ -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;
|
231
newIDE/app/src/EventsSheet/ParameterFields/LeaderboardIdField.js
Normal file
231
newIDE/app/src/EventsSheet/ParameterFields/LeaderboardIdField.js
Normal 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} />;
|
@@ -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`,
|
||||
|
@@ -92,6 +92,9 @@ import {
|
||||
addCreateBadgePreHookIfNotClaimed,
|
||||
TRIVIAL_FIRST_EVENT,
|
||||
} from '../Utils/GDevelopServices/Badge';
|
||||
import LeaderboardContext, {
|
||||
type LeaderboardState,
|
||||
} from '../Leaderboard/LeaderboardContext';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const zoomLevel = { min: 1, max: 50 };
|
||||
@@ -117,6 +120,7 @@ type Props = {|
|
||||
extensionName: string,
|
||||
eventsFunction: gdEventsFunction
|
||||
) => void,
|
||||
onBeginCreateEventsFunction: () => void,
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
|};
|
||||
|
||||
@@ -124,6 +128,7 @@ type ComponentProps = {|
|
||||
...Props,
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
preferences: Preferences,
|
||||
leaderboardsManager: ?LeaderboardState,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
@@ -1090,6 +1095,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
eventsList.insertEvent(event, eventsList.getEventsCount())
|
||||
);
|
||||
|
||||
this.props.onBeginCreateEventsFunction();
|
||||
|
||||
this.setState({
|
||||
serializedEventsToExtract: serializeToJSObject(eventsList),
|
||||
});
|
||||
@@ -1563,11 +1570,13 @@ const EventsSheet = (props, ref) => {
|
||||
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const leaderboardsManager = React.useContext(LeaderboardContext);
|
||||
return (
|
||||
<EventsSheetComponentWithoutHandle
|
||||
ref={component}
|
||||
authenticatedUser={authenticatedUser}
|
||||
preferences={preferences}
|
||||
leaderboardsManager={leaderboardsManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@@ -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}
|
||||
/>
|
||||
),
|
||||
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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',
|
||||
};
|
@@ -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;
|
@@ -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;
|
@@ -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 };
|
@@ -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}
|
||||
/>
|
||||
),
|
||||
|
||||
|
68
newIDE/app/src/GameDashboard/CyrillicToLatin.json
Normal file
68
newIDE/app/src/GameDashboard/CyrillicToLatin.json
Normal 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",
|
||||
"Ъ": "",
|
||||
"ъ": "",
|
||||
"Ь": "",
|
||||
"ь": ""
|
||||
}
|
@@ -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>
|
||||
|
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import * as React from 'react';
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
deleteGame,
|
||||
getPublicGame,
|
||||
setGameUserAcls,
|
||||
setGameSlug,
|
||||
getAclsFromUserIds,
|
||||
getCategoryName,
|
||||
} from '../Utils/GDevelopServices/Game';
|
||||
@@ -41,13 +41,14 @@ import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import {
|
||||
PublicGamePropertiesDialog,
|
||||
type PartialGameChange,
|
||||
} from '../ProjectManager/PublicGamePropertiesDialog';
|
||||
} from './PublicGamePropertiesDialog';
|
||||
import TextField from '../UI/TextField';
|
||||
import KeyboardIcon from '@material-ui/icons/Keyboard';
|
||||
import SportsEsportsIcon from '@material-ui/icons/SportsEsports';
|
||||
import SmartphoneIcon from '@material-ui/icons/Smartphone';
|
||||
import Crown from '../UI/CustomSvgIcons/Crown';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import { showErrorBox, showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import LeaderboardAdmin from './LeaderboardAdmin';
|
||||
|
||||
const styles = {
|
||||
tableRowStatColumn: {
|
||||
@@ -55,7 +56,11 @@ const styles = {
|
||||
},
|
||||
};
|
||||
|
||||
export type GamesDetailsTab = 'details' | 'builds' | 'analytics';
|
||||
export type GamesDetailsTab =
|
||||
| 'details'
|
||||
| 'builds'
|
||||
| 'analytics'
|
||||
| 'leaderboards';
|
||||
|
||||
type Props = {|
|
||||
game: Game,
|
||||
@@ -81,10 +86,16 @@ export const GameDetailsDialog = ({
|
||||
const [gameRollingMetrics, setGameMetrics] = React.useState<?GameMetrics>(
|
||||
null
|
||||
);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [
|
||||
gameUnregisterErrorText,
|
||||
setGameUnregisterErrorText,
|
||||
] = React.useState<?string>(null);
|
||||
const [gameRollingMetricsError, setGameMetricsError] = React.useState<?Error>(
|
||||
null
|
||||
);
|
||||
const [isGameMetricsLoading, setIsGameMetricsLoading] = React.useState(false);
|
||||
const [isGameUpdating, setIsGameUpdating] = React.useState(false);
|
||||
|
||||
const yesterdayIsoDate = formatISO(subDays(new Date(), 1), {
|
||||
representation: 'date',
|
||||
@@ -152,51 +163,113 @@ export const GameDetailsDialog = ({
|
||||
[loadPublicGame]
|
||||
);
|
||||
|
||||
const handleGameUpdated = React.useCallback(
|
||||
(updatedGame: Game) => {
|
||||
// Set Public Game to null to show the loader.
|
||||
// It will be refetched thanks to loadPublicGame, because Game is updated.
|
||||
setPublicGame(null);
|
||||
onGameUpdated(updatedGame);
|
||||
},
|
||||
[onGameUpdated]
|
||||
);
|
||||
|
||||
const updateGameFromProject = async (
|
||||
partialGameChange: PartialGameChange,
|
||||
i18n: I18nType
|
||||
) => {
|
||||
if (!project || !profile) return;
|
||||
): Promise<boolean> => {
|
||||
if (!project || !profile) return false;
|
||||
const { id } = profile;
|
||||
|
||||
// Set public game to null as it will be refetched automatically by the callback above.
|
||||
setPublicGame(null);
|
||||
const ownerIds = partialGameChange.ownerIds;
|
||||
if (!ownerIds || !ownerIds.length) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`You must select at least one user to be the owner of the game.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsGameUpdating(true);
|
||||
const gameId = project.getProjectUuid();
|
||||
const updatedGame = await updateGame(getAuthorizationHeader, id, gameId, {
|
||||
authorName: project.getAuthor() || 'Unspecified publisher',
|
||||
gameName: project.getName() || 'Untitle game',
|
||||
gameName: project.getName() || 'Untitled game',
|
||||
categories: project.getCategories().toJSArray() || [],
|
||||
description: project.getDescription() || '',
|
||||
playWithKeyboard: project.isPlayableWithKeyboard(),
|
||||
playWithGamepad: project.isPlayableWithGamepad(),
|
||||
playWithMobile: project.isPlayableWithMobile(),
|
||||
orientation: project.getOrientation(),
|
||||
// The thumbnailUrl is updated only when a build is made public.
|
||||
discoverable: partialGameChange.discoverable,
|
||||
});
|
||||
if (
|
||||
partialGameChange.userSlug &&
|
||||
partialGameChange.gameSlug &&
|
||||
partialGameChange.userSlug === profile.username
|
||||
) {
|
||||
try {
|
||||
await setGameSlug(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
gameId,
|
||||
partialGameChange.userSlug,
|
||||
partialGameChange.gameSlug
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Unable to update the game slug:',
|
||||
error.response || error.message
|
||||
);
|
||||
showErrorBox({
|
||||
message:
|
||||
i18n._(
|
||||
t`Unable to update the game slug. A slug must be 6 to 30 characters long and only contains letters, digits or dashes.`
|
||||
) +
|
||||
' ' +
|
||||
i18n._(t`Verify your internet connection or try again later.`),
|
||||
rawError: error,
|
||||
errorId: 'game-slug-update-error',
|
||||
});
|
||||
setIsGameUpdating(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const authorAcls = getAclsFromUserIds(
|
||||
project.getAuthorIds().toJSArray()
|
||||
);
|
||||
const ownerAcls = getAclsFromUserIds(partialGameChange.ownerIds);
|
||||
const ownerAcls = getAclsFromUserIds(ownerIds);
|
||||
await setGameUserAcls(getAuthorizationHeader, id, gameId, {
|
||||
ownership: ownerAcls,
|
||||
author: authorAcls,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Unable to update the game owners or authors:', error);
|
||||
console.error(
|
||||
'Unable to update the game owners or authors:',
|
||||
error.response || error.message
|
||||
);
|
||||
showErrorBox({
|
||||
message:
|
||||
i18n._(t`Unable to update the game owners or authors.`) +
|
||||
i18n._(
|
||||
t`Unable to update the game owners or authors. Have you removed yourself from the owners?`
|
||||
) +
|
||||
' ' +
|
||||
i18n._(t`Verify your internet connection or try again later.`),
|
||||
rawError: error,
|
||||
errorId: 'game-acls-update-error',
|
||||
});
|
||||
setIsGameUpdating(false);
|
||||
return false;
|
||||
}
|
||||
onGameUpdated(updatedGame);
|
||||
handleGameUpdated(updatedGame);
|
||||
} catch (error) {
|
||||
console.error('Unable to update the game:', error);
|
||||
console.error(
|
||||
'Unable to update the game:',
|
||||
error.response || error.message
|
||||
);
|
||||
showErrorBox({
|
||||
message:
|
||||
i18n._(t`Unable to update the game details.`) +
|
||||
@@ -205,18 +278,39 @@ export const GameDetailsDialog = ({
|
||||
rawError: error,
|
||||
errorId: 'game-details-update-error',
|
||||
});
|
||||
setIsGameUpdating(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsGameUpdating(false);
|
||||
return true;
|
||||
};
|
||||
|
||||
const unregisterGame = async () => {
|
||||
const unregisterGame = async (i18n: I18nType) => {
|
||||
if (!profile) return;
|
||||
const { id } = profile;
|
||||
|
||||
setGameUnregisterErrorText(null);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
setIsGameUpdating(true);
|
||||
await deleteGame(getAuthorizationHeader, id, game.id);
|
||||
onGameDeleted();
|
||||
} catch (error) {
|
||||
console.error('Unable to delete the game:', error);
|
||||
if (
|
||||
error.response &&
|
||||
error.response.data &&
|
||||
error.response.data.code === 'game-deletion/leaderboards-exist'
|
||||
) {
|
||||
setGameUnregisterErrorText(
|
||||
i18n._(
|
||||
t`You cannot unregister a game that has active leaderboards. To delete them, go in the Leaderboards tab, and delete them one by one.`
|
||||
)
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsGameUpdating(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -226,8 +320,7 @@ export const GameDetailsDialog = ({
|
||||
|
||||
const { id } = profile;
|
||||
try {
|
||||
// Set public game to null as it will be refetched automatically by the callback above.
|
||||
setPublicGame(null);
|
||||
setIsGameUpdating(true);
|
||||
const updatedGame = await updateGame(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
@@ -236,12 +329,14 @@ export const GameDetailsDialog = ({
|
||||
publicWebBuildId: null,
|
||||
}
|
||||
);
|
||||
onGameUpdated(updatedGame);
|
||||
handleGameUpdated(updatedGame);
|
||||
} catch (err) {
|
||||
console.error('Unable to update the game', err);
|
||||
} finally {
|
||||
setIsGameUpdating(false);
|
||||
}
|
||||
},
|
||||
[game, getAuthorizationHeader, profile, onGameUpdated]
|
||||
[game, getAuthorizationHeader, profile, handleGameUpdated]
|
||||
);
|
||||
|
||||
const authorUsernames =
|
||||
@@ -268,25 +363,41 @@ export const GameDetailsDialog = ({
|
||||
}
|
||||
open
|
||||
noMargin
|
||||
onRequestClose={onClose}
|
||||
flexColumnBody
|
||||
fullHeight={currentTab === 'leaderboards'}
|
||||
onRequestClose={() => {
|
||||
if (!isLoading) onClose();
|
||||
}}
|
||||
maxWidth="md"
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Close</Trans>}
|
||||
disabled={isLoading}
|
||||
onClick={onClose}
|
||||
key="close"
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath="/interface/games-dashboard" />,
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath={
|
||||
currentTab === 'leaderboards'
|
||||
? '/interface/games-dashboard/leaderboard-administration'
|
||||
: '/interface/games-dashboard'
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
<Tab label={<Trans>Details</Trans>} value="details" />
|
||||
<Tab label={<Trans>Builds</Trans>} value="builds" />
|
||||
<Tab label={<Trans>Analytics</Trans>} value="analytics" />
|
||||
<Tab label={<Trans>Leaderboards</Trans>} value="leaderboards" />
|
||||
</Tabs>
|
||||
<Line>
|
||||
<Line expand>
|
||||
{currentTab === 'leaderboards' ? (
|
||||
<LeaderboardAdmin gameId={game.id} onLoading={setIsLoading} />
|
||||
) : null}
|
||||
{currentTab === 'details' ? (
|
||||
publicGameError ? (
|
||||
<PlaceholderError onRetry={loadPublicGame}>
|
||||
@@ -437,9 +548,10 @@ export const GameDetailsDialog = ({
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
unregisterGame();
|
||||
unregisterGame(i18n);
|
||||
}}
|
||||
label={<Trans>Unregister this game</Trans>}
|
||||
disabled={isGameUpdating}
|
||||
/>
|
||||
<Spacer />
|
||||
{publicGame.publicWebBuildId && (
|
||||
@@ -447,14 +559,15 @@ export const GameDetailsDialog = ({
|
||||
<RaisedButton
|
||||
onClick={() => {
|
||||
const answer = Window.showConfirmDialog(
|
||||
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo unique game URL not accessible anymore. \n\nYou can decide anytime to publish it again.'
|
||||
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo.io unique game URL not accessible anymore. \n\nYou can decide at any time to publish it again.'
|
||||
);
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
unpublishGame();
|
||||
}}
|
||||
label={<Trans>Unpublish from Liluo</Trans>}
|
||||
label={<Trans>Unpublish from Liluo.io</Trans>}
|
||||
disabled={isGameUpdating}
|
||||
/>
|
||||
<Spacer />
|
||||
</>
|
||||
@@ -463,9 +576,14 @@ export const GameDetailsDialog = ({
|
||||
primary
|
||||
onClick={() => setIsPublicGamePropertiesDialogOpen(true)}
|
||||
label={<Trans>Edit game details</Trans>}
|
||||
disabled={!isGameOpenedAsProject}
|
||||
disabled={!isGameOpenedAsProject || isGameUpdating}
|
||||
/>
|
||||
</Line>
|
||||
{gameUnregisterErrorText ? (
|
||||
<PlaceholderError kind="error">
|
||||
{gameUnregisterErrorText}
|
||||
</PlaceholderError>
|
||||
) : null}
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
) : null}
|
||||
@@ -504,10 +622,8 @@ export const GameDetailsDialog = ({
|
||||
<Trans>Last week sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame &&
|
||||
publicGame.metrics &&
|
||||
publicGame.metrics.lastWeekSessionsCount
|
||||
? publicGame.metrics.lastWeekSessionsCount
|
||||
{publicGame && publicGame.cachedLastWeekSessionsCount
|
||||
? publicGame.cachedLastWeekSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
@@ -516,10 +632,8 @@ export const GameDetailsDialog = ({
|
||||
<Trans>Last year sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame &&
|
||||
publicGame.metrics &&
|
||||
publicGame.metrics.lastYearSessionsCount
|
||||
? publicGame.metrics.lastYearSessionsCount
|
||||
{publicGame && publicGame.cachedLastYearSessionsCount
|
||||
? publicGame.cachedLastYearSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
@@ -648,16 +762,21 @@ export const GameDetailsDialog = ({
|
||||
)
|
||||
) : null}
|
||||
</Line>
|
||||
{publicGame && project && (
|
||||
{publicGame && project && isPublicGamePropertiesDialogOpen && (
|
||||
<PublicGamePropertiesDialog
|
||||
open={isPublicGamePropertiesDialogOpen}
|
||||
project={project}
|
||||
publicGame={publicGame}
|
||||
onApply={partialGameChange => {
|
||||
setIsPublicGamePropertiesDialogOpen(false);
|
||||
updateGameFromProject(partialGameChange, i18n);
|
||||
onApply={async partialGameChange => {
|
||||
const isGameUpdated = await updateGameFromProject(
|
||||
partialGameChange,
|
||||
i18n
|
||||
);
|
||||
if (isGameUpdated) {
|
||||
setIsPublicGamePropertiesDialogOpen(false);
|
||||
}
|
||||
}}
|
||||
onClose={() => setIsPublicGamePropertiesDialogOpen(false)}
|
||||
isLoading={isGameUpdating}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
|
48
newIDE/app/src/GameDashboard/GameThumbnail.js
Normal file
48
newIDE/app/src/GameDashboard/GameThumbnail.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Paper } from '@material-ui/core';
|
||||
import * as React from 'react';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
|
||||
const styles = {
|
||||
image: {
|
||||
display: 'block',
|
||||
objectFit: 'cover',
|
||||
},
|
||||
thumbnail: {
|
||||
// 16/9 format
|
||||
width: 272,
|
||||
height: 153,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
thumbnailUrl?: string,
|
||||
gameName: string,
|
||||
|};
|
||||
|
||||
export const GameThumbnail = ({ thumbnailUrl, gameName }: Props) =>
|
||||
thumbnailUrl ? (
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
style={{
|
||||
...styles.image,
|
||||
...styles.thumbnail,
|
||||
}}
|
||||
alt={gameName}
|
||||
title={gameName}
|
||||
/>
|
||||
) : (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
style={{
|
||||
...styles.thumbnail,
|
||||
whiteSpace: 'normal',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<EmptyMessage>
|
||||
<Trans>No thumbnail set</Trans>
|
||||
</EmptyMessage>
|
||||
</Paper>
|
||||
);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user