mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
63 Commits
fix-loader
...
move-insta
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3e4826bafc | ||
![]() |
d914f9f5be | ||
![]() |
ab8c90dd41 | ||
![]() |
8882841a8d | ||
![]() |
3d357950f9 | ||
![]() |
ef5d2651c0 | ||
![]() |
1dd4bb9b7a | ||
![]() |
682e6f6b03 | ||
![]() |
b468c28ae8 | ||
![]() |
9fc9452a08 | ||
![]() |
3b176b7152 | ||
![]() |
f1d1a9b66b | ||
![]() |
c0d2f491a8 | ||
![]() |
aeecce2ea8 | ||
![]() |
647ee149ff | ||
![]() |
22313e148a | ||
![]() |
31f2d7ce2e | ||
![]() |
4a0efec6c2 | ||
![]() |
5f555df5c1 | ||
![]() |
53a611a1b9 | ||
![]() |
14511b23af | ||
![]() |
fa2371274d | ||
![]() |
0aea8dfa0f | ||
![]() |
81ca18098d | ||
![]() |
b6e44a022f | ||
![]() |
1a8eee2477 | ||
![]() |
d0ef92da03 | ||
![]() |
9c98cb3b3b | ||
![]() |
3681542056 | ||
![]() |
7c0bf135d7 | ||
![]() |
9a31dd046c | ||
![]() |
74401a1f9c | ||
![]() |
cedc6ea3e9 | ||
![]() |
5261f5a431 | ||
![]() |
1a6cf8d69a | ||
![]() |
e93b38fee4 | ||
![]() |
22c7215071 | ||
![]() |
a221990c57 | ||
![]() |
85f6e74a5c | ||
![]() |
6577432b27 | ||
![]() |
16d94b5e38 | ||
![]() |
88a2060364 | ||
![]() |
2d0ffee102 | ||
![]() |
aa30f3c465 | ||
![]() |
cfcb4b557f | ||
![]() |
d8db679a1d | ||
![]() |
01503d46c1 | ||
![]() |
02d44bbba4 | ||
![]() |
b6d8170a00 | ||
![]() |
554c4c8f58 | ||
![]() |
9e29146841 | ||
![]() |
189e971cd2 | ||
![]() |
deab962081 | ||
![]() |
e2281dfd82 | ||
![]() |
44daf709e4 | ||
![]() |
c9e5272367 | ||
![]() |
63584d171f | ||
![]() |
092efbe462 | ||
![]() |
fc86b4e2dd | ||
![]() |
3415626552 | ||
![]() |
fd2e87cc5c | ||
![]() |
ecc8c3176d | ||
![]() |
1c855226a8 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -3,6 +3,7 @@ Extensions/ParticleSystem/SPARK/* linguist-vendored
|
||||
Extensions/PhysicsBehavior/Box2D/* linguist-vendored
|
||||
Extensions/PhysicsBehavior/box2djs/* linguist-vendored
|
||||
Extensions/Physics2Behavior/box2d.js linguist-vendored
|
||||
Extensions/Physics3DBehavior/*.wasm-compat.js linguist-vendored
|
||||
Extensions/BBText/pixi-multistyle-text/* linguist-vendored
|
||||
Extensions/P2P/A_peer.js linguist-vendored
|
||||
Extensions/Multiplayer/peer.js linguist-vendored
|
||||
|
@@ -365,6 +365,12 @@ class GD_CORE_API InitialInstance {
|
||||
* the same initial instance between serialization.
|
||||
*/
|
||||
InitialInstance& ResetPersistentUuid();
|
||||
|
||||
/**
|
||||
* \brief Reset the persistent UUID used to recognize
|
||||
* the same initial instance between serialization.
|
||||
*/
|
||||
const gd::String& GetPersistentUuid() { return persistentUuid; };
|
||||
///@}
|
||||
|
||||
private:
|
||||
|
@@ -936,6 +936,9 @@ void Project::UnserializeAndInsertExtensionsFrom(
|
||||
const gd::SerializerElement &eventsFunctionsExtensionsElement) {
|
||||
eventsFunctionsExtensionsElement.ConsiderAsArrayOf(
|
||||
"eventsFunctionsExtension");
|
||||
|
||||
std::map<gd::String, size_t> extensionNameToElementIndex;
|
||||
|
||||
// First, only unserialize behaviors and objects names.
|
||||
// As event based objects can contains custom behaviors and custom objects,
|
||||
// this allows them to reference EventBasedBehavior and EventBasedObject
|
||||
@@ -946,6 +949,7 @@ void Project::UnserializeAndInsertExtensionsFrom(
|
||||
const SerializerElement& eventsFunctionsExtensionElement =
|
||||
eventsFunctionsExtensionsElement.GetChild(i);
|
||||
const gd::String& name = eventsFunctionsExtensionElement.GetStringAttribute("name");
|
||||
extensionNameToElementIndex[name] = i;
|
||||
|
||||
gd::EventsFunctionsExtension& eventsFunctionsExtension =
|
||||
HasEventsFunctionsExtensionNamed(name)
|
||||
@@ -959,11 +963,25 @@ void Project::UnserializeAndInsertExtensionsFrom(
|
||||
// Then unserialize functions, behaviors and objects content.
|
||||
for (gd::String &extensionName :
|
||||
GetUnserializingOrderExtensionNames(eventsFunctionsExtensionsElement)) {
|
||||
size_t extensionIndex = GetEventsFunctionsExtensionPosition(extensionName);
|
||||
const SerializerElement &eventsFunctionsExtensionElement =
|
||||
eventsFunctionsExtensionsElement.GetChild(extensionIndex);
|
||||
|
||||
eventsFunctionsExtensions.at(extensionIndex)
|
||||
size_t extensionIndex = GetEventsFunctionsExtensionPosition(extensionName);
|
||||
if (extensionIndex == gd::String::npos) {
|
||||
// Should never happen because the extension was added in the first pass.
|
||||
gd::LogError("Can't find extension " + extensionName + " in the list of extensions in second pass of unserialization.");
|
||||
continue;
|
||||
}
|
||||
auto& partiallyLoadedExtension = eventsFunctionsExtensions.at(extensionIndex);
|
||||
|
||||
if (extensionNameToElementIndex.find(extensionName) == extensionNameToElementIndex.end()) {
|
||||
// Should never happen because the extension element is present.
|
||||
gd::LogError("Can't find extension element to unserialize for " + extensionName + " in second pass of unserialization.");
|
||||
continue;
|
||||
}
|
||||
size_t elementIndex = extensionNameToElementIndex[extensionName];
|
||||
const SerializerElement &eventsFunctionsExtensionElement =
|
||||
eventsFunctionsExtensionsElement.GetChild(elementIndex);
|
||||
|
||||
partiallyLoadedExtension
|
||||
->UnserializeExtensionImplementationFrom(
|
||||
*this, eventsFunctionsExtensionElement);
|
||||
}
|
||||
@@ -971,16 +989,24 @@ void Project::UnserializeAndInsertExtensionsFrom(
|
||||
|
||||
std::vector<gd::String> Project::GetUnserializingOrderExtensionNames(
|
||||
const gd::SerializerElement &eventsFunctionsExtensionsElement) {
|
||||
eventsFunctionsExtensionsElement.ConsiderAsArrayOf(
|
||||
"eventsFunctionsExtension");
|
||||
|
||||
// Some extension have custom objects, which have child objects coming from other extension.
|
||||
// These child objects must be loaded completely before the parent custom obejct can be unserialized.
|
||||
// This implies: an order on the extension unserialization (and no cycles).
|
||||
|
||||
// At the beginning, everything is yet to be loaded.
|
||||
std::map<gd::String, size_t> extensionNameToElementIndex;
|
||||
std::vector<gd::String> remainingExtensionNames(
|
||||
eventsFunctionsExtensions.size());
|
||||
for (std::size_t i = 0; i < eventsFunctionsExtensions.size(); ++i) {
|
||||
remainingExtensionNames[i] = eventsFunctionsExtensions.at(i)->GetName();
|
||||
eventsFunctionsExtensionsElement.GetChildrenCount());
|
||||
for (std::size_t i = 0; i < eventsFunctionsExtensionsElement.GetChildrenCount(); ++i) {
|
||||
const SerializerElement& eventsFunctionsExtensionElement =
|
||||
eventsFunctionsExtensionsElement.GetChild(i);
|
||||
const gd::String& name = eventsFunctionsExtensionElement.GetStringAttribute("name");
|
||||
|
||||
remainingExtensionNames[i] = name;
|
||||
extensionNameToElementIndex[name] = i;
|
||||
}
|
||||
|
||||
// Helper allowing to find if an extension has an object that depends on
|
||||
@@ -1029,10 +1055,10 @@ std::vector<gd::String> Project::GetUnserializingOrderExtensionNames(
|
||||
foundAnyExtension = false;
|
||||
for (std::size_t i = 0; i < remainingExtensionNames.size(); ++i) {
|
||||
auto extensionName = remainingExtensionNames[i];
|
||||
size_t extensionIndex =
|
||||
GetEventsFunctionsExtensionPosition(extensionName);
|
||||
|
||||
size_t elementIndex = extensionNameToElementIndex[extensionName];
|
||||
const SerializerElement &eventsFunctionsExtensionElement =
|
||||
eventsFunctionsExtensionsElement.GetChild(extensionIndex);
|
||||
eventsFunctionsExtensionsElement.GetChild(elementIndex);
|
||||
|
||||
if (!isDependentFromRemainingExtensions(
|
||||
eventsFunctionsExtensionElement)) {
|
||||
|
@@ -1073,23 +1073,25 @@ class GD_CORE_API Project {
|
||||
return wholeProjectDiagnosticReport;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* Initialize from another game. Used by copy-ctor and assign-op.
|
||||
* Don't forget to update me if members were changed!
|
||||
*/
|
||||
void Init(const gd::Project& project);
|
||||
|
||||
/**
|
||||
* @brief Get the project extensions names in the order they have to be
|
||||
* unserialized.
|
||||
*
|
||||
* Child-objects need the event-based objects they use to be loaded completely
|
||||
* before they are unserialized.
|
||||
*
|
||||
* \warning This is only public to allow testing - don't use it in the editor.
|
||||
*/
|
||||
std::vector<gd::String> GetUnserializingOrderExtensionNames(
|
||||
static std::vector<gd::String> GetUnserializingOrderExtensionNames(
|
||||
const gd::SerializerElement& eventsFunctionsExtensionsElement);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Initialize from another game. Used by copy-ctor and assign-op.
|
||||
* Don't forget to update me if members were changed!
|
||||
*/
|
||||
void Init(const gd::Project& project);
|
||||
|
||||
/**
|
||||
* Create an object configuration of the given type.
|
||||
*
|
||||
|
@@ -51,7 +51,9 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
currentValue = element.GetChild("value").GetStringValue();
|
||||
type = element.GetChild("type").GetStringValue();
|
||||
if (type == "Number") {
|
||||
gd::String unitName = element.GetChild("unit").GetStringValue();
|
||||
gd::String unitName = element.HasChild("unit")
|
||||
? element.GetChild("unit").GetStringValue()
|
||||
: "";
|
||||
measurementUnit =
|
||||
gd::MeasurementUnit::HasDefaultMeasurementUnitNamed(unitName)
|
||||
? measurementUnit =
|
||||
|
312
Core/tests/Project-GetUnserializingOrderExtensionNames.cpp
Normal file
312
Core/tests/Project-GetUnserializingOrderExtensionNames.cpp
Normal file
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
/**
|
||||
* @file Tests covering common features of GDevelop Core.
|
||||
*/
|
||||
#include <algorithm>
|
||||
|
||||
#include "DummyPlatform.h"
|
||||
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
|
||||
#include "GDCore/Extensions/Platform.h"
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/Project/Object.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/String.h"
|
||||
#include "catch.hpp"
|
||||
|
||||
TEST_CASE("Project::GetUnserializingOrderExtensionNames", "[common]") {
|
||||
SECTION("Unserialization order is correct when nothing to load") {
|
||||
gd::SerializerElement emptyElement;
|
||||
|
||||
std::vector<gd::String> orderedNames =
|
||||
gd::Project::GetUnserializingOrderExtensionNames(emptyElement);
|
||||
REQUIRE(orderedNames.size() == 0);
|
||||
}
|
||||
SECTION("One extension with no dependencies") {
|
||||
gd::SerializerElement extensionsElement = gd::Serializer::FromJSON(
|
||||
R"([
|
||||
{
|
||||
"author": "",
|
||||
"category": "Input",
|
||||
"extensionNamespace": "",
|
||||
"fullName": "3D character keyboard mapper",
|
||||
"helpPath": "",
|
||||
"iconUrl": "fake-icon-url",
|
||||
"name": "Extension1",
|
||||
"previewIconUrl": "fake-preview-icon-url",
|
||||
"shortDescription": "3D platformer and 3D shooter keyboard controls.",
|
||||
"version": "1.0.0",
|
||||
"description": "3D platformer and 3D shooter keyboard controls.",
|
||||
"tags": [],
|
||||
"authorIds": [],
|
||||
"dependencies": [],
|
||||
"globalVariables": [],
|
||||
"sceneVariables": [],
|
||||
"eventsFunctions": [],
|
||||
"eventsBasedBehaviors": [],
|
||||
"eventsBasedObjects": []
|
||||
}
|
||||
])");
|
||||
|
||||
std::vector<gd::String> orderedNames =
|
||||
gd::Project::GetUnserializingOrderExtensionNames(extensionsElement);
|
||||
REQUIRE(orderedNames.size() == 1);
|
||||
REQUIRE(orderedNames[0] == "Extension1");
|
||||
}
|
||||
|
||||
SECTION("One extension with a dependency outside the loaded extensions") {
|
||||
gd::SerializerElement extensionsElement = gd::Serializer::FromJSON(
|
||||
R"([
|
||||
{
|
||||
"author": "",
|
||||
"category": "Input",
|
||||
"extensionNamespace": "",
|
||||
"fullName": "3D character keyboard mapper",
|
||||
"helpPath": "",
|
||||
"iconUrl": "fake-icon-url",
|
||||
"name": "Extension1DependsOtherExtension",
|
||||
"previewIconUrl": "fake-preview-icon-url",
|
||||
"shortDescription": "3D platformer and 3D shooter keyboard controls.",
|
||||
"version": "1.0.0",
|
||||
"description": "3D platformer and 3D shooter keyboard controls.",
|
||||
"tags": [],
|
||||
"authorIds": [],
|
||||
"dependencies": [],
|
||||
"globalVariables": [],
|
||||
"sceneVariables": [],
|
||||
"eventsFunctions": [],
|
||||
"eventsBasedBehaviors": [],
|
||||
"eventsBasedObjects": [
|
||||
{
|
||||
"areaMaxX": 64,
|
||||
"areaMaxY": 64,
|
||||
"areaMaxZ": 64,
|
||||
"areaMinX": 0,
|
||||
"areaMinY": 0,
|
||||
"areaMinZ": 0,
|
||||
"defaultName": "Joystick",
|
||||
"description": "Joystick for touchscreens.",
|
||||
"fullName": "Multitouch Joystick",
|
||||
"name": "SpriteMultitouchJoystick",
|
||||
"eventsFunctions": [],
|
||||
"propertyDescriptors": [],
|
||||
"objects": [
|
||||
{
|
||||
"name": "Thumb",
|
||||
"type": "OtherExtension::Whatever"
|
||||
}
|
||||
],
|
||||
"objectsFolderStructure": {
|
||||
"folderName": "__ROOT",
|
||||
"children": []
|
||||
},
|
||||
"objectsGroups": [],
|
||||
"layers": [],
|
||||
"instances": []
|
||||
}
|
||||
]
|
||||
}
|
||||
])");
|
||||
|
||||
std::vector<gd::String> orderedNames =
|
||||
gd::Project::GetUnserializingOrderExtensionNames(extensionsElement);
|
||||
REQUIRE(orderedNames.size() == 1);
|
||||
REQUIRE(orderedNames[0] == "Extension1DependsOtherExtension");
|
||||
}
|
||||
|
||||
SECTION("4 extensions with dependencies on each others") {
|
||||
gd::SerializerElement extensionsElement = gd::Serializer::FromJSON(
|
||||
R"([
|
||||
{
|
||||
"author": "",
|
||||
"category": "Input",
|
||||
"extensionNamespace": "",
|
||||
"fullName": "3D character keyboard mapper",
|
||||
"helpPath": "",
|
||||
"iconUrl": "fake-icon-url",
|
||||
"name": "Extension4DependsOn1And3",
|
||||
"previewIconUrl": "fake-preview-icon-url",
|
||||
"shortDescription": "3D platformer and 3D shooter keyboard controls.",
|
||||
"version": "1.0.0",
|
||||
"description": "3D platformer and 3D shooter keyboard controls.",
|
||||
"tags": [],
|
||||
"authorIds": [],
|
||||
"dependencies": [],
|
||||
"globalVariables": [],
|
||||
"sceneVariables": [],
|
||||
"eventsFunctions": [],
|
||||
"eventsBasedBehaviors": [],
|
||||
"eventsBasedObjects": [
|
||||
{
|
||||
"areaMaxX": 64,
|
||||
"areaMaxY": 64,
|
||||
"areaMaxZ": 64,
|
||||
"areaMinX": 0,
|
||||
"areaMinY": 0,
|
||||
"areaMinZ": 0,
|
||||
"defaultName": "Joystick",
|
||||
"description": "Joystick for touchscreens.",
|
||||
"fullName": "Multitouch Joystick",
|
||||
"name": "SpriteMultitouchJoystick",
|
||||
"eventsFunctions": [],
|
||||
"propertyDescriptors": [],
|
||||
"objects": [
|
||||
{
|
||||
"name": "Thumb",
|
||||
"type": "OtherExtension::Whatever"
|
||||
},
|
||||
{
|
||||
"name": "Thumb2",
|
||||
"type": "Extension1DependsNothing::Whatever"
|
||||
}
|
||||
],
|
||||
"objectsFolderStructure": {
|
||||
"folderName": "__ROOT",
|
||||
"children": []
|
||||
},
|
||||
"objectsGroups": [],
|
||||
"layers": [],
|
||||
"instances": []
|
||||
},
|
||||
{
|
||||
"areaMaxX": 64,
|
||||
"areaMaxY": 64,
|
||||
"areaMaxZ": 64,
|
||||
"areaMinX": 0,
|
||||
"areaMinY": 0,
|
||||
"areaMinZ": 0,
|
||||
"defaultName": "Joystick",
|
||||
"description": "Joystick for touchscreens.",
|
||||
"fullName": "Multitouch Joystick",
|
||||
"name": "SpriteMultitouchJoystick",
|
||||
"eventsFunctions": [],
|
||||
"propertyDescriptors": [],
|
||||
"objects": [
|
||||
{
|
||||
"name": "Thumb",
|
||||
"type": "OtherExtension::Whatever"
|
||||
},
|
||||
{
|
||||
"name": "Thumb2",
|
||||
"type": "Extension3DependingOn2::Whatever"
|
||||
}
|
||||
],
|
||||
"objectsFolderStructure": {
|
||||
"folderName": "__ROOT",
|
||||
"children": []
|
||||
},
|
||||
"objectsGroups": [],
|
||||
"layers": [],
|
||||
"instances": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"author": "",
|
||||
"category": "Input",
|
||||
"extensionNamespace": "",
|
||||
"fullName": "3D character keyboard mapper",
|
||||
"helpPath": "",
|
||||
"iconUrl": "fake-icon-url",
|
||||
"name": "Extension3DependingOn2",
|
||||
"previewIconUrl": "fake-preview-icon-url",
|
||||
"shortDescription": "3D platformer and 3D shooter keyboard controls.",
|
||||
"version": "1.0.0",
|
||||
"description": "3D platformer and 3D shooter keyboard controls.",
|
||||
"tags": [],
|
||||
"authorIds": [],
|
||||
"dependencies": [],
|
||||
"globalVariables": [],
|
||||
"sceneVariables": [],
|
||||
"eventsFunctions": [],
|
||||
"eventsBasedBehaviors": [],
|
||||
"eventsBasedObjects": [
|
||||
{
|
||||
"areaMaxX": 64,
|
||||
"areaMaxY": 64,
|
||||
"areaMaxZ": 64,
|
||||
"areaMinX": 0,
|
||||
"areaMinY": 0,
|
||||
"areaMinZ": 0,
|
||||
"defaultName": "Joystick",
|
||||
"description": "Joystick for touchscreens.",
|
||||
"fullName": "Multitouch Joystick",
|
||||
"name": "SpriteMultitouchJoystick",
|
||||
"eventsFunctions": [],
|
||||
"propertyDescriptors": [],
|
||||
"objects": [
|
||||
{
|
||||
"name": "Thumb",
|
||||
"type": "OtherExtension::Whatever"
|
||||
},
|
||||
{
|
||||
"name": "Thumb2",
|
||||
"type": "Extension2DependsNothing::Whatever"
|
||||
}
|
||||
],
|
||||
"objectsFolderStructure": {
|
||||
"folderName": "__ROOT",
|
||||
"children": []
|
||||
},
|
||||
"objectsGroups": [],
|
||||
"layers": [],
|
||||
"instances": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"author": "",
|
||||
"category": "Input",
|
||||
"extensionNamespace": "",
|
||||
"fullName": "3D character keyboard mapper",
|
||||
"helpPath": "",
|
||||
"iconUrl": "fake-icon-url",
|
||||
"name": "Extension2DependsNothing",
|
||||
"previewIconUrl": "fake-preview-icon-url",
|
||||
"shortDescription": "3D platformer and 3D shooter keyboard controls.",
|
||||
"version": "1.0.0",
|
||||
"description": "3D platformer and 3D shooter keyboard controls.",
|
||||
"tags": [],
|
||||
"authorIds": [],
|
||||
"dependencies": [],
|
||||
"globalVariables": [],
|
||||
"sceneVariables": [],
|
||||
"eventsFunctions": [],
|
||||
"eventsBasedBehaviors": [],
|
||||
"eventsBasedObjects": []
|
||||
},
|
||||
{
|
||||
"author": "",
|
||||
"category": "Input",
|
||||
"extensionNamespace": "",
|
||||
"fullName": "3D character keyboard mapper",
|
||||
"helpPath": "",
|
||||
"iconUrl": "fake-icon-url",
|
||||
"name": "Extension1DependsNothing",
|
||||
"previewIconUrl": "fake-preview-icon-url",
|
||||
"shortDescription": "3D platformer and 3D shooter keyboard controls.",
|
||||
"version": "1.0.0",
|
||||
"description": "3D platformer and 3D shooter keyboard controls.",
|
||||
"tags": [],
|
||||
"authorIds": [],
|
||||
"dependencies": [],
|
||||
"globalVariables": [],
|
||||
"sceneVariables": [],
|
||||
"eventsFunctions": [],
|
||||
"eventsBasedBehaviors": [],
|
||||
"eventsBasedObjects": []
|
||||
}
|
||||
])");
|
||||
|
||||
std::vector<gd::String> orderedNames =
|
||||
gd::Project::GetUnserializingOrderExtensionNames(extensionsElement);
|
||||
REQUIRE(orderedNames.size() == 4);
|
||||
REQUIRE(orderedNames[0] == "Extension2DependsNothing");
|
||||
REQUIRE(orderedNames[1] == "Extension1DependsNothing");
|
||||
REQUIRE(orderedNames[2] == "Extension3DependingOn2");
|
||||
REQUIRE(orderedNames[3] == "Extension4DependsOn1And3");
|
||||
}
|
||||
}
|
@@ -30,6 +30,14 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
getObjectPositionFrom3DRendererObject() {
|
||||
return {
|
||||
x: this._threeObject3D.position.x - this._object.getWidth() / 2,
|
||||
y: this._threeObject3D.position.y - this._object.getHeight() / 2,
|
||||
z: this._threeObject3D.position.z - this._object.getDepth() / 2,
|
||||
};
|
||||
}
|
||||
|
||||
updateRotation() {
|
||||
this._threeObject3D.rotation.set(
|
||||
gdjs.toRad(this._object.getRotationX()),
|
||||
|
@@ -695,7 +695,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsDynamic',
|
||||
_('Is dynamic'),
|
||||
_('Test if an object is dynamic.'),
|
||||
_('Check if an object is dynamic.'),
|
||||
_('_PARAM0_ is dynamic'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -727,7 +727,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsStatic',
|
||||
_('Is static'),
|
||||
_('Test if an object is static.'),
|
||||
_('Check if an object is static.'),
|
||||
_('_PARAM0_ is static'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -759,7 +759,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsKinematic',
|
||||
_('Is kinematic'),
|
||||
_('Test if an object is kinematic.'),
|
||||
_('Check if an object is kinematic.'),
|
||||
_('_PARAM0_ is kinematic'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -790,9 +790,9 @@ module.exports = {
|
||||
aut
|
||||
.addCondition(
|
||||
'IsBullet',
|
||||
_('Is treat as bullet'),
|
||||
_('Test if an object is being treat as a bullet.'),
|
||||
_('_PARAM0_ is bullet'),
|
||||
_('Is treated as a bullet'),
|
||||
_('Check if the object is being treated as a bullet.'),
|
||||
_('_PARAM0_ is treated as a bullet'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
'res/physics32.png'
|
||||
@@ -825,7 +825,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'HasFixedRotation',
|
||||
_('Has fixed rotation'),
|
||||
_('Test if an object has fixed rotation.'),
|
||||
_('Check if an object has fixed rotation.'),
|
||||
_('_PARAM0_ has fixed rotation'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -859,7 +859,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsSleepingAllowed',
|
||||
_('Is sleeping allowed'),
|
||||
_('Test if an object can sleep.'),
|
||||
_('Check if an object can sleep.'),
|
||||
_('_PARAM0_ can sleep'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -898,7 +898,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsSleeping',
|
||||
_('Is sleeping'),
|
||||
_('Test if an object is sleeping.'),
|
||||
_('Check if an object is sleeping.'),
|
||||
_('_PARAM0_ is sleeping'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -1267,7 +1267,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'LayerEnabled',
|
||||
_('Layer enabled'),
|
||||
_('Test if an object has a specific layer enabled.'),
|
||||
_('Check if an object has a specific layer enabled.'),
|
||||
_('_PARAM0_ has layer _PARAM2_ enabled'),
|
||||
_('Filtering'),
|
||||
'res/physics32.png',
|
||||
@@ -1303,7 +1303,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'MaskEnabled',
|
||||
_('Mask enabled'),
|
||||
_('Test if an object has a specific mask enabled.'),
|
||||
_('Check if an object has a specific mask enabled.'),
|
||||
_('_PARAM0_ has mask _PARAM2_ enabled'),
|
||||
_('Filtering'),
|
||||
'res/physics32.png',
|
||||
@@ -1897,7 +1897,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'JointFirstObject',
|
||||
_('Joint first object'),
|
||||
_('Test if an object is the first object on a joint.'),
|
||||
_('Check if an object is the first object on a joint.'),
|
||||
_('_PARAM0_ is the first object for joint _PARAM2_'),
|
||||
_('Joints'),
|
||||
'res/physics32.png',
|
||||
@@ -1913,7 +1913,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'JointSecondObject',
|
||||
_('Joint second object'),
|
||||
_('Test if an object is the second object on a joint.'),
|
||||
_('Check if an object is the second object on a joint.'),
|
||||
_('_PARAM0_ is the second object for joint _PARAM2_'),
|
||||
_('Joints'),
|
||||
'res/physics32.png',
|
||||
@@ -2382,7 +2382,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'RevoluteJointLimitsEnabled',
|
||||
_('Revolute joint limits enabled'),
|
||||
_('Test if a revolute joint limits are enabled.'),
|
||||
_('Check if a revolute joint limits are enabled.'),
|
||||
_('Limits for revolute joint _PARAM2_ are enabled'),
|
||||
_('Joints/Revolute'),
|
||||
'JsPlatform/Extensions/revolute_joint24.png',
|
||||
@@ -2461,7 +2461,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'RevoluteJointMotorEnabled',
|
||||
_('Revolute joint motor enabled'),
|
||||
_('Test if a revolute joint motor is enabled.'),
|
||||
_('Check if a revolute joint motor is enabled.'),
|
||||
_('Motor of revolute joint _PARAM2_ is enabled'),
|
||||
_('Joints/Revolute'),
|
||||
'JsPlatform/Extensions/revolute_joint24.png',
|
||||
@@ -2700,7 +2700,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'PrismaticJointLimitsEnabled',
|
||||
_('Prismatic joint limits enabled'),
|
||||
_('Test if a prismatic joint limits are enabled.'),
|
||||
_('Check if a prismatic joint limits are enabled.'),
|
||||
_('Limits for prismatic joint _PARAM2_ are enabled'),
|
||||
_('Joints/Prismatic'),
|
||||
'JsPlatform/Extensions/prismatic_joint24.png',
|
||||
@@ -2779,7 +2779,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'PrismaticJointMotorEnabled',
|
||||
_('Prismatic joint motor enabled'),
|
||||
_('Test if a prismatic joint motor is enabled.'),
|
||||
_('Check if a prismatic joint motor is enabled.'),
|
||||
_('Motor for prismatic joint _PARAM2_ is enabled'),
|
||||
_('Joints/Prismatic'),
|
||||
'JsPlatform/Extensions/prismatic_joint24.png',
|
||||
@@ -3459,7 +3459,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'WheelJointMotorEnabled',
|
||||
_('Wheel joint motor enabled'),
|
||||
_('Test if a wheel joint motor is enabled.'),
|
||||
_('Check if a wheel joint motor is enabled.'),
|
||||
_('Motor for wheel joint _PARAM2_ is enabled'),
|
||||
_('Joints/Wheel'),
|
||||
'JsPlatform/Extensions/wheel_joint24.png',
|
||||
@@ -4203,7 +4203,7 @@ module.exports = {
|
||||
.getCodeExtraInformation()
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.setFunctionName('gdjs.physics2.objectsCollide');
|
||||
.setFunctionName('gdjs.physics2.areObjectsColliding');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
|
@@ -960,10 +960,10 @@ namespace gdjs {
|
||||
|
||||
updateObjectFromBody() {
|
||||
// Copy transform from body to the GD object.
|
||||
// It's possible the behavior was either deactivated or the object deleted
|
||||
// just before this doStepPreEvents (for example, another behavior deleted
|
||||
// the object during its own doStepPreEvents). If the body is null, we just
|
||||
// don't do anything (but still run the physics simulation - this is independent).
|
||||
// The body is null when the behavior was either deactivated or the object deleted.
|
||||
// It would be useless to try to recreate it as updateBodyFromObject already does it.
|
||||
// If the body is null, we just don't do anything
|
||||
// (but still run the physics simulation - this is independent).
|
||||
if (this._body !== null) {
|
||||
this.owner.setX(
|
||||
this._body.GetPosition().get_x() * this._sharedData.worldScale -
|
||||
|
@@ -1,6 +1,6 @@
|
||||
namespace gdjs {
|
||||
export namespace physics2 {
|
||||
export const objectsCollide = function (
|
||||
export const areObjectsColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
|
2442
Extensions/Physics3DBehavior/JsExtension.js
Normal file
2442
Extensions/Physics3DBehavior/JsExtension.js
Normal file
File diff suppressed because it is too large
Load Diff
1881
Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts
Normal file
1881
Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts
Normal file
File diff suppressed because it is too large
Load Diff
90
Extensions/Physics3DBehavior/Physics3DTools.ts
Normal file
90
Extensions/Physics3DBehavior/Physics3DTools.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
namespace gdjs {
|
||||
export namespace physics3d {
|
||||
export const areObjectsColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName2: string,
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics3DRuntimeBehavior.areObjectsColliding,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
export const haveObjectsStartedColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName2: string,
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics3DRuntimeBehavior.hasCollisionStartedBetween,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
export const haveObjectsStoppedColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName2: string,
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics3DRuntimeBehavior.hasCollisionStoppedBetween,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
type BehaviorNamePair = { character: string; physics: string };
|
||||
|
||||
const isOnPlatformAdapter = (
|
||||
characterObject: gdjs.RuntimeObject,
|
||||
physicsObject: gdjs.RuntimeObject,
|
||||
behaviorNamePair: BehaviorNamePair
|
||||
): boolean => {
|
||||
const characterBehavior = characterObject.getBehavior(
|
||||
behaviorNamePair.character
|
||||
) as gdjs.PhysicsCharacter3DRuntimeBehavior;
|
||||
const physicsBehavior = physicsObject.getBehavior(
|
||||
behaviorNamePair.physics
|
||||
) as gdjs.Physics3DRuntimeBehavior;
|
||||
if (!characterBehavior || !physicsBehavior) {
|
||||
return false;
|
||||
}
|
||||
return characterBehavior.isOnFloorObject(physicsBehavior);
|
||||
};
|
||||
|
||||
const behaviorNamePair: BehaviorNamePair = { character: '', physics: '' };
|
||||
|
||||
export const isOnPlatform = (
|
||||
characterObjectsLists: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
characterBehaviorName: string,
|
||||
physicsObjectsLists: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
physicsBehaviorName: string,
|
||||
inverted: boolean
|
||||
) => {
|
||||
behaviorNamePair.character = characterBehaviorName;
|
||||
behaviorNamePair.physics = physicsBehaviorName;
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
isOnPlatformAdapter,
|
||||
characterObjectsLists,
|
||||
physicsObjectsLists,
|
||||
inverted,
|
||||
behaviorNamePair
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
1430
Extensions/Physics3DBehavior/PhysicsCharacter3DRuntimeBehavior.ts
Normal file
1430
Extensions/Physics3DBehavior/PhysicsCharacter3DRuntimeBehavior.ts
Normal file
File diff suppressed because it is too large
Load Diff
3
Extensions/Physics3DBehavior/README.md
Normal file
3
Extensions/Physics3DBehavior/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## Physics 3D Behaviors for GDevelop
|
||||
|
||||
This is the 3D physics engine for GDevelop, based on [Jolt Physics](https://github.com/jrouwe/JoltPhysics.js/) (WebAssembly, version 0.30.0).
|
4874
Extensions/Physics3DBehavior/jolt-physics.d.ts
vendored
Normal file
4874
Extensions/Physics3DBehavior/jolt-physics.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2481
Extensions/Physics3DBehavior/jolt-physics.wasm.js
Normal file
2481
Extensions/Physics3DBehavior/jolt-physics.wasm.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Extensions/Physics3DBehavior/jolt-physics.wasm.wasm
Normal file
BIN
Extensions/Physics3DBehavior/jolt-physics.wasm.wasm
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
describe('Physics3DRuntimeBehavior', () => {});
|
@@ -231,9 +231,13 @@ namespace gdjs {
|
||||
...super.getNetworkSyncData(),
|
||||
props: {
|
||||
cs: this._currentSpeed,
|
||||
|
||||
// TODO Try to remove these 3 fields from the synch
|
||||
// They are reset every frame and are not part of the state.
|
||||
rdx: this._requestedDeltaX,
|
||||
rdy: this._requestedDeltaY,
|
||||
ldy: this._lastDeltaY,
|
||||
|
||||
cfs: this._currentFallSpeed,
|
||||
cj: this._canJump,
|
||||
ldl: this._lastDirectionIsLeft,
|
||||
|
@@ -320,7 +320,7 @@ namespace gdjs {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
this._renderer.ensureUpToDate();
|
||||
}
|
||||
|
||||
|
@@ -216,17 +216,30 @@ bool ExporterHelper::ExportProjectForPixiPreview(
|
||||
// Strip the project (*after* generating events as the events may use stripped
|
||||
// things (objects groups...))
|
||||
gd::ProjectStripper::StripProjectForExport(exportedProject);
|
||||
exportedProject.SetFirstLayout(options.layoutName);
|
||||
|
||||
previousTime = LogTimeSpent("Data stripping", previousTime);
|
||||
|
||||
// Create the setup options passed to the gdjs.RuntimeGame
|
||||
gd::SerializerElement runtimeGameOptions;
|
||||
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);
|
||||
if (!options.externalLayoutName.empty()) {
|
||||
runtimeGameOptions.AddChild("injectExternalLayout")
|
||||
.SetValue(options.externalLayoutName);
|
||||
|
||||
auto &initialRuntimeGameStatus =
|
||||
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
|
||||
initialRuntimeGameStatus.AddChild("sceneName")
|
||||
.SetStringValue(options.layoutName);
|
||||
if (options.isInGameEdition) {
|
||||
initialRuntimeGameStatus.AddChild("isInGameEdition").SetBoolValue(true);
|
||||
}
|
||||
if (!options.externalLayoutName.empty()) {
|
||||
initialRuntimeGameStatus.AddChild("injectedExternalLayoutName")
|
||||
.SetValue(options.externalLayoutName);
|
||||
|
||||
if (options.isInGameEdition) {
|
||||
initialRuntimeGameStatus.AddChild("skipCreatingInstancesFromScene")
|
||||
.SetBoolValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
runtimeGameOptions.AddChild("projectDataOnlyExport")
|
||||
.SetBoolValue(options.projectDataOnlyExport);
|
||||
runtimeGameOptions.AddChild("nativeMobileApp")
|
||||
|
@@ -45,6 +45,7 @@ struct PreviewExportOptions {
|
||||
projectDataOnlyExport(false),
|
||||
fullLoadingScreen(false),
|
||||
isDevelopmentEnvironment(false),
|
||||
isInGameEdition(false),
|
||||
nonRuntimeScriptsCacheBurst(0),
|
||||
fallbackAuthorId(""),
|
||||
fallbackAuthorUsername(""),
|
||||
@@ -169,6 +170,14 @@ struct PreviewExportOptions {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set if the export is made for being edited in the editor.
|
||||
*/
|
||||
PreviewExportOptions &SetIsInGameEdition(bool enable) {
|
||||
isInGameEdition = enable;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief If set to a non zero value, the exported script URLs will have an
|
||||
* extra search parameter added (with the given value) to ensure browser cache
|
||||
@@ -291,6 +300,7 @@ struct PreviewExportOptions {
|
||||
bool projectDataOnlyExport;
|
||||
bool fullLoadingScreen;
|
||||
bool isDevelopmentEnvironment;
|
||||
bool isInGameEdition;
|
||||
unsigned int nonRuntimeScriptsCacheBurst;
|
||||
gd::String electronRemoteRequirePath;
|
||||
gd::String gdevelopResourceToken;
|
||||
|
@@ -342,8 +342,7 @@ namespace gdjs {
|
||||
newObject.setPosition(instanceData.x + xPos, instanceData.y + yPos);
|
||||
newObject.setAngle(instanceData.angle);
|
||||
if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(newObject)) {
|
||||
if (instanceData.z !== undefined)
|
||||
newObject.setZ(instanceData.z + zOffset);
|
||||
newObject.setZ((instanceData.z || 0) + zOffset);
|
||||
if (instanceData.rotationX !== undefined)
|
||||
newObject.setRotationX(instanceData.rotationX);
|
||||
if (instanceData.rotationY !== undefined)
|
||||
|
@@ -245,6 +245,8 @@ namespace gdjs {
|
||||
that.sendRuntimeGameDump();
|
||||
} else if (data.command === 'refresh') {
|
||||
that.sendRuntimeGameDump();
|
||||
} else if (data.command === 'getStatus') {
|
||||
that.sendRuntimeGameStatus();
|
||||
} else if (data.command === 'set') {
|
||||
that.set(data.path, data.newValue);
|
||||
} else if (data.command === 'call') {
|
||||
@@ -260,10 +262,89 @@ namespace gdjs {
|
||||
that.sendProfilerStarted();
|
||||
} else if (data.command === 'profiler.stop') {
|
||||
runtimeGame.stopCurrentSceneProfiler();
|
||||
} else if (data.command === 'instances.updated') {
|
||||
runtimeGame._editor.reloadInstances(data.payload);
|
||||
} else if (data.command === 'hotReload') {
|
||||
that._hotReloader.hotReload().then((logs) => {
|
||||
that.sendHotReloaderLogs(logs);
|
||||
// TODO: if fatal error, should probably reload. The editor should handle this
|
||||
// as it knows the current scene to show.
|
||||
});
|
||||
} else if (data.command === 'switchForInGameEdition') {
|
||||
if (!this._runtimegame.isInGameEdition()) return;
|
||||
|
||||
const sceneName = data.sceneName || null;
|
||||
const externalLayoutName = data.externalLayoutName || null;
|
||||
if (!sceneName) {
|
||||
logger.warn('No scene name specified, switchForInGameEdition aborted');
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeGameOptions = this._runtimegame.getAdditionalOptions();
|
||||
if (runtimeGameOptions.initialRuntimeGameStatus) {
|
||||
// Skip changing the scene if we're already on the state that is being requested.
|
||||
if (
|
||||
runtimeGameOptions.initialRuntimeGameStatus.sceneName ===
|
||||
sceneName &&
|
||||
runtimeGameOptions.initialRuntimeGameStatus
|
||||
.injectedExternalLayoutName === externalLayoutName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
runtimeGame
|
||||
.getSceneStack()
|
||||
.replace({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
skipCreatingInstancesFromScene: !!externalLayoutName,
|
||||
clear: true,
|
||||
});
|
||||
|
||||
// Update initialRuntimeGameStatus so that a hard reload
|
||||
// will come back to the same state, and so that we can check later
|
||||
// if the game is already on the state that is being requested.
|
||||
runtimeGameOptions.initialRuntimeGameStatus = {
|
||||
isPaused: runtimeGame.isPaused(),
|
||||
isInGameEdition: runtimeGame.isInGameEdition(),
|
||||
sceneName: sceneName,
|
||||
injectedExternalLayoutName: externalLayoutName,
|
||||
skipCreatingInstancesFromScene: !!externalLayoutName,
|
||||
};
|
||||
} else if (data.command === 'updateInstances') {
|
||||
// TODO: do an update/partial hot reload of the instances
|
||||
} else if (data.command === 'hardReload') {
|
||||
// This usually means that the preview was modified so much that an entire reload
|
||||
// is needed, or that the runtime itself could have been modified.
|
||||
try {
|
||||
const reloadUrl = new URL(location.href);
|
||||
|
||||
// Construct the initial status to be restored.
|
||||
const initialRuntimeGameStatus = this._runtimegame.getAdditionalOptions()
|
||||
.initialRuntimeGameStatus;
|
||||
const runtimeGameStatus: RuntimeGameStatus = {
|
||||
isPaused: this._runtimegame.isPaused(),
|
||||
isInGameEdition: this._runtimegame.isInGameEdition(),
|
||||
sceneName: initialRuntimeGameStatus?.sceneName || null,
|
||||
injectedExternalLayoutName:
|
||||
initialRuntimeGameStatus?.injectedExternalLayoutName || null,
|
||||
skipCreatingInstancesFromScene:
|
||||
initialRuntimeGameStatus?.skipCreatingInstancesFromScene || false,
|
||||
};
|
||||
|
||||
reloadUrl.searchParams.set(
|
||||
'runtimeGameStatus',
|
||||
JSON.stringify(runtimeGameStatus)
|
||||
);
|
||||
location.replace(reloadUrl);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Could not reload the game with the new initial status',
|
||||
error
|
||||
);
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
logger.info(
|
||||
'Unknown command "' + data.command + '" received by the debugger.'
|
||||
@@ -434,6 +515,20 @@ namespace gdjs {
|
||||
return true;
|
||||
}
|
||||
|
||||
sendRuntimeGameStatus(): void {
|
||||
const currentScene = this._runtimegame.getSceneStack().getCurrentScene();
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'status',
|
||||
payload: {
|
||||
isPaused: this._runtimegame.isPaused(),
|
||||
isInGameEdition: this._runtimegame.isInGameEdition(),
|
||||
sceneName: currentScene ? currentScene.getName() : null,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump all the relevant data from the {@link RuntimeGame} instance and send it to the server.
|
||||
*/
|
||||
@@ -543,26 +638,23 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the game is paused.
|
||||
*/
|
||||
sendGamePaused(): void {
|
||||
sendInstancesUpdated(
|
||||
objectUpdates: Array<{
|
||||
object: RuntimeObject3D;
|
||||
position: { x: number; y: number; z: number };
|
||||
}>,
|
||||
layoutName: string
|
||||
): void {
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'game.paused',
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the game is resumed.
|
||||
*/
|
||||
sendGameResumed(): void {
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'game.resumed',
|
||||
payload: null,
|
||||
command: 'instances.updated',
|
||||
payload: {
|
||||
layoutName,
|
||||
instances: objectUpdates.map((objectUpdate) => ({
|
||||
persistentUuid: objectUpdate.object.persistentUuid,
|
||||
position: objectUpdate.position,
|
||||
})),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -144,8 +144,9 @@ namespace gdjs {
|
||||
});
|
||||
}
|
||||
|
||||
hotReload(): Promise<HotReloaderLog[]> {
|
||||
async hotReload(): Promise<HotReloaderLog[]> {
|
||||
logger.info('Hot reload started');
|
||||
const wasPaused = this._runtimeGame.isPaused();
|
||||
this._runtimeGame.pause(true);
|
||||
this._logs = [];
|
||||
|
||||
@@ -168,62 +169,59 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Reload projectData and runtimeGameOptions stored by convention in data.js:
|
||||
return this._reloadScript('data.js').then(() => {
|
||||
const newProjectData: ProjectData = gdjs.projectData;
|
||||
await this._reloadScript('data.js');
|
||||
|
||||
const newRuntimeGameOptions: RuntimeGameOptions =
|
||||
gdjs.runtimeGameOptions;
|
||||
const newProjectData: ProjectData = gdjs.projectData;
|
||||
|
||||
const newScriptFiles = newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
|
||||
const projectDataOnlyExport = !!newRuntimeGameOptions.projectDataOnlyExport;
|
||||
const newRuntimeGameOptions: RuntimeGameOptions = gdjs.runtimeGameOptions;
|
||||
|
||||
// Reload the changed scripts, which will have the side effects of re-running
|
||||
// the new scripts, potentially replacing the code of the free functions from
|
||||
// extensions (which is fine) and registering updated behaviors (which will
|
||||
// need to be re-instantiated in runtime objects).
|
||||
return this.reloadScriptFiles(
|
||||
const newScriptFiles = newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
|
||||
const projectDataOnlyExport = !!newRuntimeGameOptions.projectDataOnlyExport;
|
||||
|
||||
// Reload the changed scripts, which will have the side effects of re-running
|
||||
// the new scripts, potentially replacing the code of the free functions from
|
||||
// extensions (which is fine) and registering updated behaviors (which will
|
||||
// need to be re-instantiated in runtime objects).
|
||||
try {
|
||||
await this.reloadScriptFiles(
|
||||
newProjectData,
|
||||
oldScriptFiles,
|
||||
newScriptFiles,
|
||||
projectDataOnlyExport
|
||||
)
|
||||
.then(() => {
|
||||
const changedRuntimeBehaviors = this._computeChangedRuntimeBehaviors(
|
||||
oldBehaviorConstructors,
|
||||
gdjs.behaviorsTypes.items
|
||||
);
|
||||
return this._hotReloadRuntimeGame(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
this._runtimeGame
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorTarget = error.target;
|
||||
if (errorTarget instanceof HTMLScriptElement) {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message: 'Unable to reload script: ' + errorTarget.src,
|
||||
});
|
||||
} else {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message:
|
||||
'Unexpected error happened while hot-reloading: ' +
|
||||
error.message,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(
|
||||
'Hot reload finished with logs:',
|
||||
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
|
||||
);
|
||||
this._runtimeGame.pause(false);
|
||||
return this._logs;
|
||||
);
|
||||
|
||||
const changedRuntimeBehaviors = this._computeChangedRuntimeBehaviors(
|
||||
oldBehaviorConstructors,
|
||||
gdjs.behaviorsTypes.items
|
||||
);
|
||||
await this._hotReloadRuntimeGame(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
this._runtimeGame
|
||||
);
|
||||
} catch (error) {
|
||||
const errorTarget = error.target;
|
||||
if (errorTarget instanceof HTMLScriptElement) {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message: 'Unable to reload script: ' + errorTarget.src,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message:
|
||||
'Unexpected error happened while hot-reloading: ' + error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'Hot reload finished with logs:',
|
||||
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
|
||||
);
|
||||
this._runtimeGame.pause(wasPaused);
|
||||
return this._logs;
|
||||
}
|
||||
|
||||
_computeChangedRuntimeBehaviors(
|
||||
|
@@ -6,8 +6,6 @@
|
||||
namespace gdjs {
|
||||
export namespace evtTools {
|
||||
export namespace sound {
|
||||
const logger = new gdjs.Logger('Audio events');
|
||||
|
||||
export const getGlobalVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): float {
|
||||
@@ -64,9 +62,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) sound.stop();
|
||||
else {
|
||||
logger.error(`Cannot stop non-existing sound on channel ${channel}.`);
|
||||
}
|
||||
};
|
||||
|
||||
export const pauseSoundOnChannel = function (
|
||||
@@ -78,11 +73,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) sound.pause();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot pause non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const continueSoundOnChannel = function (
|
||||
@@ -95,10 +85,6 @@ namespace gdjs {
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) {
|
||||
if (!sound.playing()) sound.play();
|
||||
} else {
|
||||
logger.error(
|
||||
`Cannot continue playing non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,9 +109,6 @@ namespace gdjs {
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) return sound.paused();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing sound on channel ${channel} is paused.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -140,9 +123,6 @@ namespace gdjs {
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) return sound.stopped();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing sound on channel ${channel} is stopped.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -157,9 +137,6 @@ namespace gdjs {
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) return sound.getVolume() * 100;
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the volume of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
@@ -174,11 +151,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) sound.setVolume(volume / 100);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the volume of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSoundOnChannelPlayingOffset = function (
|
||||
@@ -191,9 +163,6 @@ namespace gdjs {
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) return sound.getSeek();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the playing offset of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
@@ -208,11 +177,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) sound.setSeek(playingOffset);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the playing offset of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSoundOnChannelPitch = function (
|
||||
@@ -225,9 +189,6 @@ namespace gdjs {
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) return sound.getRate();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -242,11 +203,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) sound.setRate(pitch);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const preloadSound = (
|
||||
@@ -304,11 +260,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) music.stop();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot stop a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const pauseMusicOnChannel = function (
|
||||
@@ -320,11 +271,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) music.pause();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot pause a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const continueMusicOnChannel = function (
|
||||
@@ -337,10 +283,6 @@ namespace gdjs {
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) {
|
||||
if (!music.playing()) music.play();
|
||||
} else {
|
||||
logger.error(
|
||||
`Cannot stop a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -365,9 +307,6 @@ namespace gdjs {
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) return music.paused();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing music on channel ${channel} is paused.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -382,9 +321,6 @@ namespace gdjs {
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) return music.stopped();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing music on channel ${channel} is stopped.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -399,9 +335,6 @@ namespace gdjs {
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) return music.getVolume() * 100;
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the volume of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
@@ -416,11 +349,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) music.setVolume(volume / 100);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the volume of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMusicOnChannelPlayingOffset = function (
|
||||
@@ -433,9 +361,6 @@ namespace gdjs {
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) return music.getSeek();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the playing offset of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
@@ -450,11 +375,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) music.setSeek(playingOffset);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the playing offset of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMusicOnChannelPitch = function (
|
||||
@@ -467,9 +387,6 @@ namespace gdjs {
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) return music.getRate();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
@@ -484,11 +401,6 @@ namespace gdjs {
|
||||
.getSoundManager()
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) music.setRate(pitch);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const preloadMusic = (
|
||||
@@ -521,10 +433,6 @@ namespace gdjs {
|
||||
.getSoundOnChannel(channel);
|
||||
if (sound) {
|
||||
sound.fade(sound.getVolume(), toVolume / 100, timeOfFade * 1000);
|
||||
} else {
|
||||
logger.error(
|
||||
`Cannot fade the volume of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
export const fadeMusicVolume = (
|
||||
@@ -539,10 +447,6 @@ namespace gdjs {
|
||||
.getMusicOnChannel(channel);
|
||||
if (music) {
|
||||
music.fade(music.getVolume(), toVolume / 100, timeOfFade * 1000);
|
||||
} else {
|
||||
logger.error(
|
||||
`Cannot fade the volume of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -62,7 +62,7 @@ namespace gdjs {
|
||||
/**
|
||||
* Create the canvas on which the game will be rendered, inside the specified DOM element, and
|
||||
* setup the rendering of the game.
|
||||
* If you want to use your own canvas, use `initializeForCanvas` instead.
|
||||
* If you want to use your own canvas, use `initializeRenderers` and `initializeCanvas` instead.
|
||||
*
|
||||
* @param parentElement The parent element to which the canvas will be added.
|
||||
*/
|
||||
@@ -72,20 +72,18 @@ namespace gdjs {
|
||||
const gameCanvas = document.createElement('canvas');
|
||||
parentElement.appendChild(gameCanvas);
|
||||
|
||||
this.initializeForCanvas(gameCanvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the rendering of the game to use a canvas that was already created.
|
||||
* @param gameCanvas The canvas to use.
|
||||
*/
|
||||
initializeForCanvas(gameCanvas: HTMLCanvasElement): void {
|
||||
this._throwIfDisposed();
|
||||
this.initializeRenderers(gameCanvas);
|
||||
this.initializeCanvas(gameCanvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the rendering of the game for the given canvas.
|
||||
*
|
||||
* In most cases, you can use `createStandardCanvas` instead to initialize the game.
|
||||
*/
|
||||
initializeRenderers(gameCanvas: HTMLCanvasElement): void {
|
||||
this._throwIfDisposed();
|
||||
|
||||
if (typeof THREE !== 'undefined') {
|
||||
this._threeRenderer = new THREE.WebGLRenderer({
|
||||
canvas: gameCanvas,
|
||||
@@ -136,6 +134,10 @@ namespace gdjs {
|
||||
delete this._pixiRenderer.plugins.accessibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the game canvas so that it covers the size required by the game
|
||||
* and has a container for DOM elements required by the game.
|
||||
*/
|
||||
initializeCanvas(gameCanvas: HTMLCanvasElement): void {
|
||||
// Add the renderer view element to the DOM
|
||||
this._gameCanvas = gameCanvas;
|
||||
@@ -537,6 +539,8 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Add the standard events handler.
|
||||
*
|
||||
* The game canvas must have been initialized before calling this.
|
||||
*/
|
||||
bindStandardEvents(
|
||||
manager: gdjs.InputManager,
|
||||
|
@@ -41,14 +41,54 @@ namespace gdjs {
|
||||
return supportedCompressionMethods;
|
||||
};
|
||||
|
||||
/**
|
||||
* The desired status of the game, used for previews or in-game edition.
|
||||
* Either stored in the options generated by the preview or in the URL
|
||||
* in case of a hard reload.
|
||||
*/
|
||||
export type RuntimeGameStatus = {
|
||||
isPaused: boolean;
|
||||
isInGameEdition: boolean;
|
||||
sceneName: string | null;
|
||||
injectedExternalLayoutName: string | null;
|
||||
skipCreatingInstancesFromScene: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the desired status of the game from the URL. Only useful for previews
|
||||
* when hard reloaded.
|
||||
*/
|
||||
const readRuntimeGameStatusFromUrl = (): RuntimeGameStatus | null => {
|
||||
try {
|
||||
const url = new URL(location.href);
|
||||
const runtimeGameStatus = url.searchParams.get('runtimeGameStatus');
|
||||
if (!runtimeGameStatus) return null;
|
||||
|
||||
const parsedRuntimeGameStatus = JSON.parse(runtimeGameStatus);
|
||||
return {
|
||||
isPaused: !!parsedRuntimeGameStatus.isPaused,
|
||||
isInGameEdition: !!parsedRuntimeGameStatus.isInGameEdition,
|
||||
sceneName: '' + parsedRuntimeGameStatus.sceneName,
|
||||
injectedExternalLayoutName:
|
||||
'' + parsedRuntimeGameStatus.injectedExternalLayoutName,
|
||||
skipCreatingInstancesFromScene: !!parsedRuntimeGameStatus.skipCreatingInstancesFromScene,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/** Options given to the game at startup. */
|
||||
export type RuntimeGameOptions = {
|
||||
/** if true, force fullscreen. */
|
||||
forceFullscreen?: boolean;
|
||||
|
||||
/** if true, game is run as a preview launched from an editor. */
|
||||
isPreview?: boolean;
|
||||
/** The name of the external layout to create in the scene at position 0;0. */
|
||||
injectExternalLayout?: string;
|
||||
|
||||
/** if set, the status of the game to be restored. */
|
||||
initialRuntimeGameStatus?: RuntimeGameStatus;
|
||||
|
||||
/** Script files, used for hot-reloading. */
|
||||
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
|
||||
/** if true, export is a partial preview without events. */
|
||||
@@ -118,6 +158,239 @@ namespace gdjs {
|
||||
environment?: 'dev';
|
||||
};
|
||||
|
||||
class RuntimeEditor {
|
||||
_game: RuntimeGame;
|
||||
_pointer = new THREE.Vector2();
|
||||
// TODO: Use array
|
||||
_selectedObjectData: {
|
||||
intersect: THREE.Intersection;
|
||||
camera: THREE.Camera;
|
||||
scene: THREE.Scene;
|
||||
} | null = null;
|
||||
_outlinePasses: Record<string, THREE_ADDONS.OutlinePass> = {};
|
||||
_raycaster = new THREE.Raycaster()
|
||||
_editionAbortController: AbortController = new AbortController();
|
||||
_clearCurrentPasses: (() => void)[] = [];
|
||||
_currentTransformControls: THREE_ADDONS.TransformControls | null = null;
|
||||
_shouldIgnoreNextClick: boolean = false;
|
||||
|
||||
constructor(game: RuntimeGame) {
|
||||
this._game = game;
|
||||
}
|
||||
|
||||
onPointerMove(event) {
|
||||
// calculate pointer position in normalized device coordinates
|
||||
// (-1 to +1) for both components
|
||||
|
||||
this._pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
this._pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
}
|
||||
|
||||
selectObject() {
|
||||
if (this._shouldIgnoreNextClick) {
|
||||
this._shouldIgnoreNextClick = false;
|
||||
return;
|
||||
}
|
||||
const firstIntersectsByLayer = this.getFirstIntersectsOnEachLayer(false);
|
||||
if (!firstIntersectsByLayer) return;
|
||||
let closestIntersect;
|
||||
for (const intersect of Object.values(firstIntersectsByLayer)) {
|
||||
if (
|
||||
!closestIntersect ||
|
||||
intersect.intersect.distance < closestIntersect.intersect.distance
|
||||
) {
|
||||
closestIntersect = intersect;
|
||||
}
|
||||
}
|
||||
this._selectedObjectData = closestIntersect;
|
||||
if (!this._selectedObjectData) return;
|
||||
|
||||
if (
|
||||
this._currentTransformControls &&
|
||||
this._currentTransformControls.camera ===
|
||||
this._selectedObjectData.camera
|
||||
) {
|
||||
this._currentTransformControls.detach();
|
||||
this._currentTransformControls.attach(
|
||||
this._selectedObjectData.intersect.object
|
||||
);
|
||||
} else {
|
||||
if (this._currentTransformControls) {
|
||||
this._currentTransformControls.detach();
|
||||
this._currentTransformControls = null;
|
||||
}
|
||||
this._currentTransformControls = new THREE_ADDONS.TransformControls(
|
||||
this._selectedObjectData.camera,
|
||||
this._game.getRenderer().getCanvas() || undefined
|
||||
);
|
||||
this._currentTransformControls.addEventListener('mouseDown', () => {
|
||||
this._shouldIgnoreNextClick = true;
|
||||
});
|
||||
this._currentTransformControls.addEventListener(
|
||||
'dragging-changed',
|
||||
(e) => {
|
||||
if (!this._selectedObjectData) return;
|
||||
if (e.value) {
|
||||
// Ignore if the user starts dragging
|
||||
return;
|
||||
}
|
||||
const rendererObject = this._selectedObjectData.intersect.object;
|
||||
const object = this._game.getObjectFromRenderer(rendererObject);
|
||||
if (!object) return;
|
||||
if (object instanceof gdjs.RuntimeObject3D) {
|
||||
this._game.sendRuntimeObjectsUpdated([
|
||||
{
|
||||
object,
|
||||
position: object
|
||||
.getRenderer()
|
||||
.getObjectPositionFrom3DRendererObject(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
);
|
||||
this._currentTransformControls.scale.y = -1;
|
||||
this._currentTransformControls.attach(
|
||||
this._selectedObjectData.intersect.object
|
||||
);
|
||||
this._selectedObjectData.scene.add(this._currentTransformControls);
|
||||
}
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
const canvas = this._game.getRenderer().getCanvas();
|
||||
this._editionAbortController = new AbortController();
|
||||
canvas?.addEventListener('pointermove', this.onPointerMove.bind(this), {
|
||||
signal: this._editionAbortController.signal,
|
||||
});
|
||||
canvas?.addEventListener('click', this.selectObject.bind(this), {
|
||||
signal: this._editionAbortController.signal,
|
||||
});
|
||||
}
|
||||
cleanListeners() {
|
||||
this._editionAbortController.abort();
|
||||
}
|
||||
activate(enable: boolean) {
|
||||
if (enable) this.setupListeners();
|
||||
else {
|
||||
this.cleanListeners();
|
||||
if (this._currentTransformControls) {
|
||||
this._currentTransformControls.detach();
|
||||
this._currentTransformControls = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reloadInstances(payload: {
|
||||
layoutName: string;
|
||||
instances: Array<{
|
||||
persistentUuid: string;
|
||||
position: { x: number; y: number; z: number };
|
||||
}>;
|
||||
}) {
|
||||
const currentScene = this._game.getSceneStack().getCurrentScene();
|
||||
if (!currentScene || currentScene.getName() !== payload.layoutName) {
|
||||
return;
|
||||
}
|
||||
// TODO: Might be worth indexing instances data and runtime objects by their
|
||||
// persistentUuid (See HotReloader.indexByPersistentUuid).
|
||||
currentScene.getAdhocListOfAllInstances().forEach((runtimeObject) => {
|
||||
const instance = payload.instances.find(
|
||||
(instance) => instance.persistentUuid === runtimeObject.persistentUuid
|
||||
);
|
||||
if (instance) {
|
||||
runtimeObject.setX(instance.position.x);
|
||||
runtimeObject.setY(instance.position.y);
|
||||
if (runtimeObject instanceof gdjs.RuntimeObject3D) {
|
||||
runtimeObject.setZ(instance.position.z);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getFirstIntersectsOnEachLayer(highlightObject: boolean) {
|
||||
if (highlightObject) {
|
||||
this._clearCurrentPasses.forEach((callback) => callback());
|
||||
this._clearCurrentPasses.length = 0;
|
||||
}
|
||||
const layerNames = new Array();
|
||||
const currentScene = this._game.getSceneStack().getCurrentScene();
|
||||
if (!currentScene) return;
|
||||
const threeRenderer = this._game.getRenderer().getThreeRenderer();
|
||||
if (!threeRenderer) return;
|
||||
|
||||
currentScene.getAllLayerNames(layerNames);
|
||||
const firstIntersectsByLayer: {
|
||||
[layerName: string]: {
|
||||
intersect: THREE.Intersection;
|
||||
camera: THREE.Camera;
|
||||
scene: THREE.Scene;
|
||||
};
|
||||
} = layerNames.reduce((acc, layerName) => {
|
||||
const runtimeLayerRender = currentScene
|
||||
.getLayer(layerName)
|
||||
.getRenderer();
|
||||
const threeCamera = runtimeLayerRender.getThreeCamera();
|
||||
const threeScene = runtimeLayerRender.getThreeScene();
|
||||
if (!threeCamera || !threeScene) return acc;
|
||||
|
||||
if (!this._outlinePasses[layerName]) {
|
||||
this._outlinePasses[layerName] = new THREE_ADDONS.OutlinePass(
|
||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||
threeScene,
|
||||
threeCamera
|
||||
);
|
||||
|
||||
runtimeLayerRender.addPostProcessingPass(this._outlinePasses[layerName]);
|
||||
}
|
||||
const outlinePass = this._outlinePasses[layerName];
|
||||
|
||||
// Note that raycasting is done by Three.js, which means it could slow down
|
||||
// if lots of 3D objects are shown. We consider that if this needs improvements,
|
||||
// this must be handled by the game engine culling
|
||||
this._raycaster.setFromCamera(this._pointer, threeCamera);
|
||||
const intersects = this._raycaster.intersectObjects(threeScene.children);
|
||||
|
||||
const firstIntersect = intersects.filter((intersect) => {
|
||||
let isObjectChildOfTransformControls = false;
|
||||
intersect.object.traverseAncestors((ancestor) => {
|
||||
if (ancestor === this._currentTransformControls) {
|
||||
isObjectChildOfTransformControls = true;
|
||||
}
|
||||
});
|
||||
return !isObjectChildOfTransformControls;
|
||||
})[0];
|
||||
|
||||
if (
|
||||
firstIntersect &&
|
||||
highlightObject &&
|
||||
(!this._currentTransformControls ||
|
||||
!this._currentTransformControls.dragging)
|
||||
) {
|
||||
// TODO: OutlinePass currently wrongly highlights the transform controls helper.
|
||||
// (See https://discourse.threejs.org/t/outlinepass-with-transform-control/18722)
|
||||
|
||||
outlinePass.edgeStrength = 6.0;
|
||||
outlinePass.edgeGlow = 0;
|
||||
outlinePass.edgeThickness = 1.0;
|
||||
outlinePass.pulsePeriod = 0;
|
||||
outlinePass.selectedObjects = [firstIntersect.object];
|
||||
}
|
||||
acc[layerName] = {
|
||||
intersect: firstIntersect,
|
||||
camera: threeCamera,
|
||||
scene: threeScene,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
return firstIntersectsByLayer;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.getFirstIntersectsOnEachLayer(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a game being played.
|
||||
*/
|
||||
@@ -169,10 +442,6 @@ namespace gdjs {
|
||||
//Inputs :
|
||||
_inputManager: InputManager;
|
||||
|
||||
/**
|
||||
* Allow to specify an external layout to insert in the first scene.
|
||||
*/
|
||||
_injectExternalLayout: any;
|
||||
_options: RuntimeGameOptions;
|
||||
|
||||
/**
|
||||
@@ -187,6 +456,7 @@ namespace gdjs {
|
||||
_sessionMetricsInitialized: boolean = false;
|
||||
_disableMetrics: boolean = false;
|
||||
_isPreview: boolean;
|
||||
_isInGameEdition: boolean;
|
||||
|
||||
/**
|
||||
* The capture manager, used to manage captures (screenshots, videos, etc...).
|
||||
@@ -196,12 +466,27 @@ namespace gdjs {
|
||||
/** True if the RuntimeGame has been disposed and should not be used anymore. */
|
||||
_wasDisposed: boolean = false;
|
||||
|
||||
_editor: RuntimeEditor;
|
||||
|
||||
/**
|
||||
* @param data The object (usually stored in data.json) containing the full project data
|
||||
* @param
|
||||
*/
|
||||
constructor(data: ProjectData, options?: RuntimeGameOptions) {
|
||||
this._options = options || {};
|
||||
|
||||
this._isPreview = this._options.isPreview || false;
|
||||
if (this._isPreview) {
|
||||
// Check if we need to restore the state from the URL, which is used
|
||||
// when a preview is hard reloaded (search for `hardReload`).
|
||||
const runtimeGameStatusFromUrl = readRuntimeGameStatusFromUrl();
|
||||
if (runtimeGameStatusFromUrl) {
|
||||
this._options.initialRuntimeGameStatus = runtimeGameStatusFromUrl;
|
||||
}
|
||||
}
|
||||
this._isInGameEdition =
|
||||
this._options.initialRuntimeGameStatus?.isInGameEdition || false;
|
||||
|
||||
this._variables = new gdjs.VariablesContainer(data.variables);
|
||||
this._variablesByExtensionName = new Map<
|
||||
string,
|
||||
@@ -225,7 +510,7 @@ namespace gdjs {
|
||||
getGlobalResourceNames(data),
|
||||
data.layouts
|
||||
);
|
||||
|
||||
this._editor = new RuntimeEditor(this);
|
||||
this._effectsManager = new gdjs.EffectsManager();
|
||||
this._maxFPS = this._data.properties.maxFPS;
|
||||
this._minFPS = this._data.properties.minFPS;
|
||||
@@ -250,7 +535,6 @@ namespace gdjs {
|
||||
);
|
||||
this._sceneStack = new gdjs.SceneStack(this);
|
||||
this._inputManager = new gdjs.InputManager();
|
||||
this._injectExternalLayout = this._options.injectExternalLayout || '';
|
||||
this._debuggerClient = gdjs.DebuggerClient
|
||||
? new gdjs.DebuggerClient(this)
|
||||
: null;
|
||||
@@ -260,7 +544,6 @@ namespace gdjs {
|
||||
this._options.captureOptions || {}
|
||||
)
|
||||
: null;
|
||||
this._isPreview = this._options.isPreview || false;
|
||||
this._sessionId = null;
|
||||
this._playerId = null;
|
||||
|
||||
@@ -709,9 +992,9 @@ namespace gdjs {
|
||||
if (this._paused === enable) return;
|
||||
|
||||
this._paused = enable;
|
||||
this._editor.activate(enable);
|
||||
if (this._debuggerClient) {
|
||||
if (this._paused) this._debuggerClient.sendGamePaused();
|
||||
else this._debuggerClient.sendGameResumed();
|
||||
this._debuggerClient.sendRuntimeGameStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,11 +1155,16 @@ namespace gdjs {
|
||||
await loadAssets(onProgress);
|
||||
|
||||
await loadingScreen.unload();
|
||||
this.pause(false);
|
||||
|
||||
if (!this._isInGameEdition) {
|
||||
this.pause(false);
|
||||
}
|
||||
}
|
||||
|
||||
private _getFirstSceneName(): string {
|
||||
const firstSceneName = this._data.firstLayout;
|
||||
const firstSceneName =
|
||||
this._options.initialRuntimeGameStatus?.sceneName ||
|
||||
this._data.firstLayout;
|
||||
return this.hasScene(firstSceneName)
|
||||
? firstSceneName
|
||||
: // There is always at least a scene
|
||||
@@ -896,10 +1184,15 @@ namespace gdjs {
|
||||
this._forceGameResolutionUpdate();
|
||||
|
||||
// Load the first scene
|
||||
this._sceneStack.push(
|
||||
this._getFirstSceneName(),
|
||||
this._injectExternalLayout
|
||||
);
|
||||
this._sceneStack.push({
|
||||
sceneName: this._getFirstSceneName(),
|
||||
externalLayoutName:
|
||||
this._options.initialRuntimeGameStatus
|
||||
?.injectedExternalLayoutName || undefined,
|
||||
skipCreatingInstancesFromScene:
|
||||
this._options.initialRuntimeGameStatus
|
||||
?.skipCreatingInstancesFromScene || false,
|
||||
});
|
||||
this._watermark.displayAtStartup();
|
||||
|
||||
//Uncomment to profile the first x frames of the game.
|
||||
@@ -917,11 +1210,29 @@ namespace gdjs {
|
||||
this._setupGameVisibilityEvents();
|
||||
|
||||
// The standard game loop
|
||||
let lastFrameSceneName: string | null = null;
|
||||
let accumulatedElapsedTime = 0;
|
||||
this._hasJustResumed = false;
|
||||
this._renderer.startGameLoop((lastCallElapsedTime) => {
|
||||
try {
|
||||
if (this._debuggerClient) {
|
||||
// Watch the scene name to automatically update debugger when a scene is changed.
|
||||
const currentScene = this.getSceneStack().getCurrentScene();
|
||||
if (
|
||||
currentScene &&
|
||||
currentScene.getName() !== lastFrameSceneName
|
||||
) {
|
||||
lastFrameSceneName = currentScene.getName();
|
||||
this._debuggerClient.sendRuntimeGameStatus();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._paused) {
|
||||
this._editor.render();
|
||||
// The game is paused for edition: the game loop continues to run,
|
||||
// but the game logic is not executed.
|
||||
this._sceneStack.renderWithoutStep();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1273,6 +1584,22 @@ namespace gdjs {
|
||||
return this._isPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game loop is paused, for debugging/edition purposes.
|
||||
* @returns true if the current game is paused
|
||||
*/
|
||||
isPaused(): boolean {
|
||||
return this._paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should display in-game edition tools or not.
|
||||
* @returns true if the current game is being edited.
|
||||
*/
|
||||
isInGameEdition(): boolean {
|
||||
return this._isInGameEdition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should call GDevelop development APIs or not.
|
||||
*
|
||||
@@ -1319,6 +1646,38 @@ namespace gdjs {
|
||||
: embeddedResourceName;
|
||||
}
|
||||
|
||||
getObjectFromRenderer(
|
||||
renderer: THREE.Object3D | gdjs.RendererObjectInterface
|
||||
): RuntimeObject | null {
|
||||
const currentScene = this.getSceneStack().getCurrentScene();
|
||||
if (!currentScene) return null;
|
||||
// TODO: Should we instead have an index on the runtime object persistentUuid?
|
||||
// In that case, it would require to store the persistentUuid on the renderer object.
|
||||
const allInstances = currentScene.getAdhocListOfAllInstances();
|
||||
const object = allInstances.find(
|
||||
renderer instanceof THREE.Object3D
|
||||
? (instance) => instance.get3DRendererObject() === renderer
|
||||
: (instance) => instance.getRendererObject() === renderer
|
||||
);
|
||||
|
||||
return object || null;
|
||||
}
|
||||
|
||||
sendRuntimeObjectsUpdated(
|
||||
runtimeObjectUpdates: Array<{
|
||||
object: RuntimeObject3D;
|
||||
position: { x: number; y: number; z: number };
|
||||
}>
|
||||
) {
|
||||
if (!this._debuggerClient) return;
|
||||
const currentScene = this.getSceneStack().getCurrentScene();
|
||||
if (!currentScene) return;
|
||||
this._debuggerClient.sendInstancesUpdated(
|
||||
runtimeObjectUpdates,
|
||||
currentScene.getName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of resources that are embedded to passed one.
|
||||
* @param resourceName The name of resource to find embedded resources of.
|
||||
|
@@ -121,10 +121,15 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Load the runtime scene from the given scene.
|
||||
* @param sceneData An object containing the scene data.
|
||||
*
|
||||
* @param sceneAndExtensionsData The data of the scene and extension variables to be loaded.
|
||||
* @param options Options to change what is loaded.
|
||||
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
|
||||
*/
|
||||
loadFromScene(sceneAndExtensionsData: SceneAndExtensionsData | null) {
|
||||
loadFromScene(
|
||||
sceneAndExtensionsData: SceneAndExtensionsData | null,
|
||||
options?: { skipCreatingInstances?: boolean }
|
||||
) {
|
||||
if (!sceneAndExtensionsData) {
|
||||
logger.error('loadFromScene was called without a scene');
|
||||
return;
|
||||
@@ -184,14 +189,16 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
//Create initial instances of objects
|
||||
this.createObjectsFrom(
|
||||
sceneData.instances,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
/*trackByPersistentUuid=*/
|
||||
true
|
||||
);
|
||||
if (!options || !options.skipCreatingInstances) {
|
||||
this.createObjectsFrom(
|
||||
sceneData.instances,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
/*trackByPersistentUuid=*/
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Set up the default z order (for objects created from events)
|
||||
this._setLayerDefaultZOrders();
|
||||
@@ -358,7 +365,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Step and render the scene.
|
||||
* Step (execute the game logic) and render the scene.
|
||||
* @param elapsedTime In milliseconds
|
||||
* @return true if the game loop should continue, false if a scene change/push/pop
|
||||
* or a game stop was requested.
|
||||
@@ -418,6 +425,21 @@ namespace gdjs {
|
||||
if (this._profiler) {
|
||||
this._profiler.end('callbacks and extensions (post-events)');
|
||||
}
|
||||
|
||||
this.render();
|
||||
this._isJustResumed = false;
|
||||
if (this._profiler) {
|
||||
this._profiler.end('render');
|
||||
}
|
||||
if (this._profiler) {
|
||||
this._profiler.endFrame();
|
||||
}
|
||||
return !!this.getRequestedChange();
|
||||
}
|
||||
/**
|
||||
* Render the scene (but do not execute the game logic).
|
||||
*/
|
||||
render() {
|
||||
if (this._profiler) {
|
||||
this._profiler.begin('objects (pre-render, effects update)');
|
||||
}
|
||||
@@ -447,21 +469,6 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
this._isJustResumed = false;
|
||||
this.render();
|
||||
if (this._profiler) {
|
||||
this._profiler.end('render');
|
||||
}
|
||||
if (this._profiler) {
|
||||
this._profiler.endFrame();
|
||||
}
|
||||
return !!this.getRequestedChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the PIXI container associated to the runtimeScene.
|
||||
*/
|
||||
render() {
|
||||
this._renderer.render();
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,16 @@ namespace gdjs {
|
||||
const logger = new gdjs.Logger('Scene stack');
|
||||
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
|
||||
|
||||
interface PushSceneOptions {
|
||||
sceneName: string;
|
||||
externalLayoutName?: string;
|
||||
skipCreatingInstancesFromScene?: boolean;
|
||||
};
|
||||
|
||||
interface ReplaceSceneOptions extends PushSceneOptions {
|
||||
clear: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hold the stack of scenes ({@link gdjs.RuntimeScene}) being played.
|
||||
*/
|
||||
@@ -113,15 +123,28 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pause the scene currently being played and start the new scene that is specified.
|
||||
* If `externalLayoutName` is set, also instantiate the objects from this external layout.
|
||||
* Pause the scene currently being played and start the new scene that is specified in `options.sceneName`.
|
||||
* If `options.externalLayoutName` is set, also instantiate the objects from this external layout.
|
||||
*
|
||||
* @param options Contains the scene name and optional external layout name to instantiate.
|
||||
* @param deprecatedExternalLayoutName Deprecated, use `options.externalLayoutName` instead.
|
||||
*/
|
||||
push(
|
||||
newSceneName: string,
|
||||
externalLayoutName?: string
|
||||
options: PushSceneOptions | string,
|
||||
deprecatedExternalLayoutName?: string
|
||||
): gdjs.RuntimeScene | null {
|
||||
this._throwIfDisposed();
|
||||
console.log({options, deprecatedExternalLayoutName})
|
||||
|
||||
const sceneName =
|
||||
typeof options === 'string' ? options : options.sceneName;
|
||||
const skipCreatingInstancesFromScene =
|
||||
typeof options === 'string' ? false : options.skipCreatingInstancesFromScene;
|
||||
const externalLayoutName =
|
||||
deprecatedExternalLayoutName ||
|
||||
(typeof options === 'string' ? undefined : options.externalLayoutName);
|
||||
|
||||
// Tell the scene it's being paused
|
||||
const currentScene = this._stack[this._stack.length - 1];
|
||||
@@ -131,35 +154,43 @@ namespace gdjs {
|
||||
|
||||
// Avoid a risk of displaying an intermediate loading screen
|
||||
// during 1 frame.
|
||||
if (this._runtimeGame.areSceneAssetsReady(newSceneName)) {
|
||||
return this._loadNewScene(newSceneName, externalLayoutName);
|
||||
if (this._runtimeGame.areSceneAssetsReady(sceneName)) {
|
||||
return this._loadNewScene({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
skipCreatingInstancesFromScene,
|
||||
});
|
||||
}
|
||||
|
||||
this._isNextLayoutLoading = true;
|
||||
this._runtimeGame.loadSceneAssets(newSceneName).then(() => {
|
||||
this._loadNewScene(newSceneName);
|
||||
this._runtimeGame.loadSceneAssets(sceneName).then(() => {
|
||||
this._loadNewScene({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
skipCreatingInstancesFromScene,
|
||||
});
|
||||
this._isNextLayoutLoading = false;
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
private _loadNewScene(
|
||||
newSceneName: string,
|
||||
externalLayoutName?: string
|
||||
): gdjs.RuntimeScene {
|
||||
private _loadNewScene(options: PushSceneOptions): gdjs.RuntimeScene {
|
||||
this._throwIfDisposed();
|
||||
|
||||
// Load the new one
|
||||
const newScene = new gdjs.RuntimeScene(this._runtimeGame);
|
||||
newScene.loadFromScene(
|
||||
this._runtimeGame.getSceneAndExtensionsData(newSceneName)
|
||||
this._runtimeGame.getSceneAndExtensionsData(options.sceneName),
|
||||
{
|
||||
skipCreatingInstances: options.skipCreatingInstancesFromScene,
|
||||
}
|
||||
);
|
||||
this._wasFirstSceneLoaded = true;
|
||||
|
||||
// Optionally create the objects from an external layout.
|
||||
if (externalLayoutName) {
|
||||
if (options.externalLayoutName) {
|
||||
const externalLayoutData = this._runtimeGame.getExternalLayoutData(
|
||||
externalLayoutName
|
||||
options.externalLayoutName
|
||||
);
|
||||
if (externalLayoutData) {
|
||||
newScene.createObjectsFrom(
|
||||
@@ -177,10 +208,15 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the specified scene, replacing the one currently being played.
|
||||
* If `clear` is set to true, all running scenes are also removed from the stack of scenes.
|
||||
* Start the scene in `options.sceneName`, replacing the one currently being played.
|
||||
* If `options.clear` is set to true, all running scenes are also removed from the stack of scenes.
|
||||
*
|
||||
* @param options Contains the scene name and optional external layout name to instantiate.
|
||||
* @param deprecatedClear Deprecated, use `options.clear` instead.
|
||||
*/
|
||||
replace(newSceneName: string, clear?: boolean): gdjs.RuntimeScene | null {
|
||||
replace(options: ReplaceSceneOptions | string, deprecatedClear?: boolean): gdjs.RuntimeScene | null {
|
||||
const clear = deprecatedClear || typeof options === 'string' ? false : options.clear;
|
||||
|
||||
this._throwIfDisposed();
|
||||
if (!!clear) {
|
||||
// Unload all the scenes
|
||||
@@ -199,7 +235,7 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.push(newSceneName);
|
||||
return this.push(options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
4
GDJS/Runtime/types/global-three-addons.d.ts
vendored
4
GDJS/Runtime/types/global-three-addons.d.ts
vendored
@@ -2,7 +2,9 @@ import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
|
||||
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
|
||||
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
|
||||
import { Pass } from 'three/examples/jsm/postprocessing/Pass';
|
||||
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
|
||||
@@ -21,7 +23,9 @@ declare global {
|
||||
GLTF,
|
||||
DRACOLoader,
|
||||
SkeletonUtils,
|
||||
TransformControls,
|
||||
EffectComposer,
|
||||
OutlinePass,
|
||||
Pass,
|
||||
RenderPass,
|
||||
ShaderPass,
|
||||
|
@@ -24,7 +24,8 @@ describe('gdjs.RuntimeGameRenderer canvas tests', () => {
|
||||
it('should correctly initialize external canvas and create domElementsContainer', () => {
|
||||
const gameCanvas = document.createElement('canvas');
|
||||
gameContainer.appendChild(gameCanvas);
|
||||
renderer.initializeForCanvas(gameCanvas);
|
||||
renderer.initializeRenderers(gameCanvas);
|
||||
renderer.initializeCanvas(gameCanvas);
|
||||
|
||||
const actualGameCanvas = renderer.getCanvas();
|
||||
const actualDomElementsContainer = renderer.getDomElementContainer();
|
||||
|
@@ -1373,6 +1373,7 @@ interface InitialInstance {
|
||||
double GetCustomDepth();
|
||||
|
||||
[Ref] InitialInstance ResetPersistentUuid();
|
||||
[Const, Value] DOMString GetPersistentUuid();
|
||||
|
||||
void UpdateCustomProperty(
|
||||
[Const] DOMString name,
|
||||
@@ -3843,6 +3844,7 @@ interface PreviewExportOptions {
|
||||
[Ref] PreviewExportOptions SetNativeMobileApp(boolean enable);
|
||||
[Ref] PreviewExportOptions SetFullLoadingScreen(boolean enable);
|
||||
[Ref] PreviewExportOptions SetIsDevelopmentEnvironment(boolean enable);
|
||||
[Ref] PreviewExportOptions SetIsInGameEdition(boolean enable);
|
||||
[Ref] PreviewExportOptions SetNonRuntimeScriptsCacheBurst(unsigned long value);
|
||||
[Ref] PreviewExportOptions SetElectronRemoteRequirePath([Const] DOMString electronRemoteRequirePath);
|
||||
[Ref] PreviewExportOptions SetGDevelopResourceToken([Const] DOMString gdevelopResourceToken);
|
||||
|
2
GDevelop.js/types.d.ts
vendored
2
GDevelop.js/types.d.ts
vendored
@@ -1145,6 +1145,7 @@ export class InitialInstance extends EmscriptenObject {
|
||||
setCustomDepth(depth: number): void;
|
||||
getCustomDepth(): number;
|
||||
resetPersistentUuid(): InitialInstance;
|
||||
getPersistentUuid(): string;
|
||||
updateCustomProperty(name: string, value: string, globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): void;
|
||||
getCustomProperties(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): MapStringPropertyDescriptor;
|
||||
getRawDoubleProperty(name: string): number;
|
||||
@@ -2850,6 +2851,7 @@ export class PreviewExportOptions extends EmscriptenObject {
|
||||
setNativeMobileApp(enable: boolean): PreviewExportOptions;
|
||||
setFullLoadingScreen(enable: boolean): PreviewExportOptions;
|
||||
setIsDevelopmentEnvironment(enable: boolean): PreviewExportOptions;
|
||||
setIsInGameEdition(enable: boolean): PreviewExportOptions;
|
||||
setNonRuntimeScriptsCacheBurst(value: number): PreviewExportOptions;
|
||||
setElectronRemoteRequirePath(electronRemoteRequirePath: string): PreviewExportOptions;
|
||||
setGDevelopResourceToken(gdevelopResourceToken: string): PreviewExportOptions;
|
||||
|
@@ -44,6 +44,7 @@ declare class gdInitialInstance {
|
||||
setCustomDepth(depth: number): void;
|
||||
getCustomDepth(): number;
|
||||
resetPersistentUuid(): gdInitialInstance;
|
||||
getPersistentUuid(): string;
|
||||
updateCustomProperty(name: string, value: string, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): void;
|
||||
getCustomProperties(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): gdMapStringPropertyDescriptor;
|
||||
getRawDoubleProperty(name: string): number;
|
||||
|
@@ -13,6 +13,7 @@ declare class gdPreviewExportOptions {
|
||||
setNativeMobileApp(enable: boolean): gdPreviewExportOptions;
|
||||
setFullLoadingScreen(enable: boolean): gdPreviewExportOptions;
|
||||
setIsDevelopmentEnvironment(enable: boolean): gdPreviewExportOptions;
|
||||
setIsInGameEdition(enable: boolean): gdPreviewExportOptions;
|
||||
setNonRuntimeScriptsCacheBurst(value: number): gdPreviewExportOptions;
|
||||
setElectronRemoteRequirePath(electronRemoteRequirePath: string): gdPreviewExportOptions;
|
||||
setGDevelopResourceToken(gdevelopResourceToken: string): gdPreviewExportOptions;
|
||||
|
16
README.md
16
README.md
@@ -30,17 +30,17 @@ GDevelop is a **full-featured, no-code, open-source** game development software.
|
||||
|
||||
GDevelop is composed of an **editor**, a **game engine**, an **ecosystem** of extensions as well as **online services** and commercial support.
|
||||
|
||||
| Directory | ℹ️ Description |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Core` | Core classes, describing the structure of a game and tools to implement the IDE and work with GDevelop games. |
|
||||
| `GDJS` | The game engine, written in TypeScript, using PixiJS and Three.js (WebGL), powering all GDevelop games. |
|
||||
| `GDevelop.js` | Bindings of `Core`, `GDJS` and `Extensions` to JavaScript (with WebAssembly), used by the IDE. |
|
||||
| `newIDE` | The game editor, written in JavaScript with React, Electron, PixiJS and Three.js. |
|
||||
| `Extensions` | Built-in extensions for the game engine, providing objects, behaviors, events and new features. All the [community extensions are on this repository](https://github.com/GDevelopApp/GDevelop-extensions). |
|
||||
| Directory | ℹ️ Description |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Core` | Core classes, describing the structure of a game and tools to implement the IDE and work with GDevelop games. |
|
||||
| `GDJS` | The game engine, written in TypeScript, using PixiJS and Three.js for 2D and 3D rendering (WebGL), powering all GDevelop games. |
|
||||
| `GDevelop.js` | Bindings of `Core`, `GDJS` and `Extensions` to JavaScript (with WebAssembly), used by the IDE. |
|
||||
| `newIDE` | The game editor, written in JavaScript with React, Electron, PixiJS and Three.js.js. |
|
||||
| `Extensions` | Built-in extensions for the game engine, providing objects, behaviors and new features. For example, this includes the physics engines running in WebAssembly (Box2D or Jolt Physics for 3D). All the [community extensions are on this repository](https://github.com/GDevelopApp/GDevelop-extensions). |
|
||||
|
||||
To learn more about GDevelop Architecture, read the [architecture overview here](Core/GDevelop-Architecture-Overview.md).
|
||||
|
||||
Pre-generated documentation of the Core library, C++ and TypeScript game engines is [available here](https://docs.gdevelop.io).
|
||||
Pre-generated documentation of the game engine is [available here](https://docs.gdevelop.io).
|
||||
|
||||
Status of the tests and builds: [](https://app.circleci.com/pipelines/github/4ian/GDevelop) [](https://gdevelop.semaphoreci.com/projects/GDevelop) [](https://ci.appveyor.com/project/4ian/gdevelop/branch/master) [](https://good-labs.github.io/greater-good-affirmation)
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,654 @@
|
||||
import {
|
||||
AdditiveBlending,
|
||||
Color,
|
||||
DoubleSide,
|
||||
HalfFloatType,
|
||||
Matrix4,
|
||||
MeshDepthMaterial,
|
||||
NoBlending,
|
||||
RGBADepthPacking,
|
||||
ShaderMaterial,
|
||||
UniformsUtils,
|
||||
Vector2,
|
||||
Vector3,
|
||||
WebGLRenderTarget
|
||||
} from 'three';
|
||||
import { Pass, FullScreenQuad } from './Pass.js';
|
||||
import { CopyShader } from '../shaders/CopyShader.js';
|
||||
|
||||
class OutlinePass extends Pass {
|
||||
|
||||
constructor( resolution, scene, camera, selectedObjects ) {
|
||||
|
||||
super();
|
||||
|
||||
this.renderScene = scene;
|
||||
this.renderCamera = camera;
|
||||
this.selectedObjects = selectedObjects !== undefined ? selectedObjects : [];
|
||||
this.visibleEdgeColor = new Color( 1, 1, 1 );
|
||||
this.hiddenEdgeColor = new Color( 0.1, 0.04, 0.02 );
|
||||
this.edgeGlow = 0.0;
|
||||
this.usePatternTexture = false;
|
||||
this.edgeThickness = 1.0;
|
||||
this.edgeStrength = 3.0;
|
||||
this.downSampleRatio = 2;
|
||||
this.pulsePeriod = 0;
|
||||
|
||||
this._visibilityCache = new Map();
|
||||
|
||||
|
||||
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
|
||||
|
||||
const resx = Math.round( this.resolution.x / this.downSampleRatio );
|
||||
const resy = Math.round( this.resolution.y / this.downSampleRatio );
|
||||
|
||||
this.renderTargetMaskBuffer = new WebGLRenderTarget( this.resolution.x, this.resolution.y );
|
||||
this.renderTargetMaskBuffer.texture.name = 'OutlinePass.mask';
|
||||
this.renderTargetMaskBuffer.texture.generateMipmaps = false;
|
||||
|
||||
this.depthMaterial = new MeshDepthMaterial();
|
||||
this.depthMaterial.side = DoubleSide;
|
||||
this.depthMaterial.depthPacking = RGBADepthPacking;
|
||||
this.depthMaterial.blending = NoBlending;
|
||||
|
||||
this.prepareMaskMaterial = this.getPrepareMaskMaterial();
|
||||
this.prepareMaskMaterial.side = DoubleSide;
|
||||
this.prepareMaskMaterial.fragmentShader = replaceDepthToViewZ( this.prepareMaskMaterial.fragmentShader, this.renderCamera );
|
||||
|
||||
this.renderTargetDepthBuffer = new WebGLRenderTarget( this.resolution.x, this.resolution.y, { type: HalfFloatType } );
|
||||
this.renderTargetDepthBuffer.texture.name = 'OutlinePass.depth';
|
||||
this.renderTargetDepthBuffer.texture.generateMipmaps = false;
|
||||
|
||||
this.renderTargetMaskDownSampleBuffer = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
this.renderTargetMaskDownSampleBuffer.texture.name = 'OutlinePass.depthDownSample';
|
||||
this.renderTargetMaskDownSampleBuffer.texture.generateMipmaps = false;
|
||||
|
||||
this.renderTargetBlurBuffer1 = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
this.renderTargetBlurBuffer1.texture.name = 'OutlinePass.blur1';
|
||||
this.renderTargetBlurBuffer1.texture.generateMipmaps = false;
|
||||
this.renderTargetBlurBuffer2 = new WebGLRenderTarget( Math.round( resx / 2 ), Math.round( resy / 2 ), { type: HalfFloatType } );
|
||||
this.renderTargetBlurBuffer2.texture.name = 'OutlinePass.blur2';
|
||||
this.renderTargetBlurBuffer2.texture.generateMipmaps = false;
|
||||
|
||||
this.edgeDetectionMaterial = this.getEdgeDetectionMaterial();
|
||||
this.renderTargetEdgeBuffer1 = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
this.renderTargetEdgeBuffer1.texture.name = 'OutlinePass.edge1';
|
||||
this.renderTargetEdgeBuffer1.texture.generateMipmaps = false;
|
||||
this.renderTargetEdgeBuffer2 = new WebGLRenderTarget( Math.round( resx / 2 ), Math.round( resy / 2 ), { type: HalfFloatType } );
|
||||
this.renderTargetEdgeBuffer2.texture.name = 'OutlinePass.edge2';
|
||||
this.renderTargetEdgeBuffer2.texture.generateMipmaps = false;
|
||||
|
||||
const MAX_EDGE_THICKNESS = 4;
|
||||
const MAX_EDGE_GLOW = 4;
|
||||
|
||||
this.separableBlurMaterial1 = this.getSeperableBlurMaterial( MAX_EDGE_THICKNESS );
|
||||
this.separableBlurMaterial1.uniforms[ 'texSize' ].value.set( resx, resy );
|
||||
this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = 1;
|
||||
this.separableBlurMaterial2 = this.getSeperableBlurMaterial( MAX_EDGE_GLOW );
|
||||
this.separableBlurMaterial2.uniforms[ 'texSize' ].value.set( Math.round( resx / 2 ), Math.round( resy / 2 ) );
|
||||
this.separableBlurMaterial2.uniforms[ 'kernelRadius' ].value = MAX_EDGE_GLOW;
|
||||
|
||||
// Overlay material
|
||||
this.overlayMaterial = this.getOverlayMaterial();
|
||||
|
||||
// copy material
|
||||
|
||||
const copyShader = CopyShader;
|
||||
|
||||
this.copyUniforms = UniformsUtils.clone( copyShader.uniforms );
|
||||
|
||||
this.materialCopy = new ShaderMaterial( {
|
||||
uniforms: this.copyUniforms,
|
||||
vertexShader: copyShader.vertexShader,
|
||||
fragmentShader: copyShader.fragmentShader,
|
||||
blending: NoBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
} );
|
||||
|
||||
this.enabled = true;
|
||||
this.needsSwap = false;
|
||||
|
||||
this._oldClearColor = new Color();
|
||||
this.oldClearAlpha = 1;
|
||||
|
||||
this.fsQuad = new FullScreenQuad( null );
|
||||
|
||||
this.tempPulseColor1 = new Color();
|
||||
this.tempPulseColor2 = new Color();
|
||||
this.textureMatrix = new Matrix4();
|
||||
|
||||
function replaceDepthToViewZ( string, camera ) {
|
||||
|
||||
const type = camera.isPerspectiveCamera ? 'perspective' : 'orthographic';
|
||||
|
||||
return string.replace( /DEPTH_TO_VIEW_Z/g, type + 'DepthToViewZ' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
this.renderTargetMaskBuffer.dispose();
|
||||
this.renderTargetDepthBuffer.dispose();
|
||||
this.renderTargetMaskDownSampleBuffer.dispose();
|
||||
this.renderTargetBlurBuffer1.dispose();
|
||||
this.renderTargetBlurBuffer2.dispose();
|
||||
this.renderTargetEdgeBuffer1.dispose();
|
||||
this.renderTargetEdgeBuffer2.dispose();
|
||||
|
||||
this.depthMaterial.dispose();
|
||||
this.prepareMaskMaterial.dispose();
|
||||
this.edgeDetectionMaterial.dispose();
|
||||
this.separableBlurMaterial1.dispose();
|
||||
this.separableBlurMaterial2.dispose();
|
||||
this.overlayMaterial.dispose();
|
||||
this.materialCopy.dispose();
|
||||
|
||||
this.fsQuad.dispose();
|
||||
|
||||
}
|
||||
|
||||
setSize( width, height ) {
|
||||
|
||||
this.renderTargetMaskBuffer.setSize( width, height );
|
||||
this.renderTargetDepthBuffer.setSize( width, height );
|
||||
|
||||
let resx = Math.round( width / this.downSampleRatio );
|
||||
let resy = Math.round( height / this.downSampleRatio );
|
||||
this.renderTargetMaskDownSampleBuffer.setSize( resx, resy );
|
||||
this.renderTargetBlurBuffer1.setSize( resx, resy );
|
||||
this.renderTargetEdgeBuffer1.setSize( resx, resy );
|
||||
this.separableBlurMaterial1.uniforms[ 'texSize' ].value.set( resx, resy );
|
||||
|
||||
resx = Math.round( resx / 2 );
|
||||
resy = Math.round( resy / 2 );
|
||||
|
||||
this.renderTargetBlurBuffer2.setSize( resx, resy );
|
||||
this.renderTargetEdgeBuffer2.setSize( resx, resy );
|
||||
|
||||
this.separableBlurMaterial2.uniforms[ 'texSize' ].value.set( resx, resy );
|
||||
|
||||
}
|
||||
|
||||
changeVisibilityOfSelectedObjects( bVisible ) {
|
||||
|
||||
const cache = this._visibilityCache;
|
||||
|
||||
function gatherSelectedMeshesCallBack( object ) {
|
||||
|
||||
if ( object.isMesh ) {
|
||||
|
||||
if ( bVisible === true ) {
|
||||
|
||||
object.visible = cache.get( object );
|
||||
|
||||
} else {
|
||||
|
||||
cache.set( object, object.visible );
|
||||
object.visible = bVisible;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
|
||||
|
||||
const selectedObject = this.selectedObjects[ i ];
|
||||
selectedObject.traverse( gatherSelectedMeshesCallBack );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
changeVisibilityOfNonSelectedObjects( bVisible ) {
|
||||
|
||||
const cache = this._visibilityCache;
|
||||
const selectedMeshes = [];
|
||||
|
||||
function gatherSelectedMeshesCallBack( object ) {
|
||||
|
||||
if ( object.isMesh ) selectedMeshes.push( object );
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
|
||||
|
||||
const selectedObject = this.selectedObjects[ i ];
|
||||
selectedObject.traverse( gatherSelectedMeshesCallBack );
|
||||
|
||||
}
|
||||
|
||||
function VisibilityChangeCallBack( object ) {
|
||||
|
||||
if ( object.isMesh || object.isSprite ) {
|
||||
|
||||
// only meshes and sprites are supported by OutlinePass
|
||||
|
||||
let bFound = false;
|
||||
|
||||
for ( let i = 0; i < selectedMeshes.length; i ++ ) {
|
||||
|
||||
const selectedObjectId = selectedMeshes[ i ].id;
|
||||
|
||||
if ( selectedObjectId === object.id ) {
|
||||
|
||||
bFound = true;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( bFound === false ) {
|
||||
|
||||
const visibility = object.visible;
|
||||
|
||||
if ( bVisible === false || cache.get( object ) === true ) {
|
||||
|
||||
object.visible = bVisible;
|
||||
|
||||
}
|
||||
|
||||
cache.set( object, visibility );
|
||||
|
||||
}
|
||||
|
||||
} else if ( object.isPoints || object.isLine ) {
|
||||
|
||||
// the visibilty of points and lines is always set to false in order to
|
||||
// not affect the outline computation
|
||||
|
||||
if ( bVisible === true ) {
|
||||
|
||||
object.visible = cache.get( object ); // restore
|
||||
|
||||
} else {
|
||||
|
||||
cache.set( object, object.visible );
|
||||
object.visible = bVisible;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.renderScene.traverse( VisibilityChangeCallBack );
|
||||
|
||||
}
|
||||
|
||||
updateTextureMatrix() {
|
||||
|
||||
this.textureMatrix.set( 0.5, 0.0, 0.0, 0.5,
|
||||
0.0, 0.5, 0.0, 0.5,
|
||||
0.0, 0.0, 0.5, 0.5,
|
||||
0.0, 0.0, 0.0, 1.0 );
|
||||
this.textureMatrix.multiply( this.renderCamera.projectionMatrix );
|
||||
this.textureMatrix.multiply( this.renderCamera.matrixWorldInverse );
|
||||
|
||||
}
|
||||
|
||||
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
|
||||
|
||||
if ( this.selectedObjects.length > 0 ) {
|
||||
|
||||
renderer.getClearColor( this._oldClearColor );
|
||||
this.oldClearAlpha = renderer.getClearAlpha();
|
||||
const oldAutoClear = renderer.autoClear;
|
||||
|
||||
renderer.autoClear = false;
|
||||
|
||||
if ( maskActive ) renderer.state.buffers.stencil.setTest( false );
|
||||
|
||||
renderer.setClearColor( 0xffffff, 1 );
|
||||
|
||||
// Make selected objects invisible
|
||||
this.changeVisibilityOfSelectedObjects( false );
|
||||
|
||||
const currentBackground = this.renderScene.background;
|
||||
this.renderScene.background = null;
|
||||
|
||||
// 1. Draw Non Selected objects in the depth buffer
|
||||
this.renderScene.overrideMaterial = this.depthMaterial;
|
||||
renderer.setRenderTarget( this.renderTargetDepthBuffer );
|
||||
renderer.clear();
|
||||
renderer.render( this.renderScene, this.renderCamera );
|
||||
|
||||
// Make selected objects visible
|
||||
this.changeVisibilityOfSelectedObjects( true );
|
||||
this._visibilityCache.clear();
|
||||
|
||||
// Update Texture Matrix for Depth compare
|
||||
this.updateTextureMatrix();
|
||||
|
||||
// Make non selected objects invisible, and draw only the selected objects, by comparing the depth buffer of non selected objects
|
||||
this.changeVisibilityOfNonSelectedObjects( false );
|
||||
this.renderScene.overrideMaterial = this.prepareMaskMaterial;
|
||||
this.prepareMaskMaterial.uniforms[ 'cameraNearFar' ].value.set( this.renderCamera.near, this.renderCamera.far );
|
||||
this.prepareMaskMaterial.uniforms[ 'depthTexture' ].value = this.renderTargetDepthBuffer.texture;
|
||||
this.prepareMaskMaterial.uniforms[ 'textureMatrix' ].value = this.textureMatrix;
|
||||
renderer.setRenderTarget( this.renderTargetMaskBuffer );
|
||||
renderer.clear();
|
||||
renderer.render( this.renderScene, this.renderCamera );
|
||||
this.renderScene.overrideMaterial = null;
|
||||
this.changeVisibilityOfNonSelectedObjects( true );
|
||||
this._visibilityCache.clear();
|
||||
|
||||
this.renderScene.background = currentBackground;
|
||||
|
||||
// 2. Downsample to Half resolution
|
||||
this.fsQuad.material = this.materialCopy;
|
||||
this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetMaskBuffer.texture;
|
||||
renderer.setRenderTarget( this.renderTargetMaskDownSampleBuffer );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
this.tempPulseColor1.copy( this.visibleEdgeColor );
|
||||
this.tempPulseColor2.copy( this.hiddenEdgeColor );
|
||||
|
||||
if ( this.pulsePeriod > 0 ) {
|
||||
|
||||
const scalar = ( 1 + 0.25 ) / 2 + Math.cos( performance.now() * 0.01 / this.pulsePeriod ) * ( 1.0 - 0.25 ) / 2;
|
||||
this.tempPulseColor1.multiplyScalar( scalar );
|
||||
this.tempPulseColor2.multiplyScalar( scalar );
|
||||
|
||||
}
|
||||
|
||||
// 3. Apply Edge Detection Pass
|
||||
this.fsQuad.material = this.edgeDetectionMaterial;
|
||||
this.edgeDetectionMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskDownSampleBuffer.texture;
|
||||
this.edgeDetectionMaterial.uniforms[ 'texSize' ].value.set( this.renderTargetMaskDownSampleBuffer.width, this.renderTargetMaskDownSampleBuffer.height );
|
||||
this.edgeDetectionMaterial.uniforms[ 'visibleEdgeColor' ].value = this.tempPulseColor1;
|
||||
this.edgeDetectionMaterial.uniforms[ 'hiddenEdgeColor' ].value = this.tempPulseColor2;
|
||||
renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
// 4. Apply Blur on Half res
|
||||
this.fsQuad.material = this.separableBlurMaterial1;
|
||||
this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
|
||||
this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
|
||||
this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = this.edgeThickness;
|
||||
renderer.setRenderTarget( this.renderTargetBlurBuffer1 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetBlurBuffer1.texture;
|
||||
this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionY;
|
||||
renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
// Apply Blur on quarter res
|
||||
this.fsQuad.material = this.separableBlurMaterial2;
|
||||
this.separableBlurMaterial2.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
|
||||
this.separableBlurMaterial2.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
|
||||
renderer.setRenderTarget( this.renderTargetBlurBuffer2 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
this.separableBlurMaterial2.uniforms[ 'colorTexture' ].value = this.renderTargetBlurBuffer2.texture;
|
||||
this.separableBlurMaterial2.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionY;
|
||||
renderer.setRenderTarget( this.renderTargetEdgeBuffer2 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
// Blend it additively over the input texture
|
||||
this.fsQuad.material = this.overlayMaterial;
|
||||
this.overlayMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskBuffer.texture;
|
||||
this.overlayMaterial.uniforms[ 'edgeTexture1' ].value = this.renderTargetEdgeBuffer1.texture;
|
||||
this.overlayMaterial.uniforms[ 'edgeTexture2' ].value = this.renderTargetEdgeBuffer2.texture;
|
||||
this.overlayMaterial.uniforms[ 'patternTexture' ].value = this.patternTexture;
|
||||
this.overlayMaterial.uniforms[ 'edgeStrength' ].value = this.edgeStrength;
|
||||
this.overlayMaterial.uniforms[ 'edgeGlow' ].value = this.edgeGlow;
|
||||
this.overlayMaterial.uniforms[ 'usePatternTexture' ].value = this.usePatternTexture;
|
||||
|
||||
|
||||
if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
|
||||
|
||||
renderer.setRenderTarget( readBuffer );
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
renderer.setClearColor( this._oldClearColor, this.oldClearAlpha );
|
||||
renderer.autoClear = oldAutoClear;
|
||||
|
||||
}
|
||||
|
||||
if ( this.renderToScreen ) {
|
||||
|
||||
this.fsQuad.material = this.materialCopy;
|
||||
this.copyUniforms[ 'tDiffuse' ].value = readBuffer.texture;
|
||||
renderer.setRenderTarget( null );
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getPrepareMaskMaterial() {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
uniforms: {
|
||||
'depthTexture': { value: null },
|
||||
'cameraNearFar': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'textureMatrix': { value: null }
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`#include <morphtarget_pars_vertex>
|
||||
#include <skinning_pars_vertex>
|
||||
|
||||
varying vec4 projTexCoord;
|
||||
varying vec4 vPosition;
|
||||
uniform mat4 textureMatrix;
|
||||
|
||||
void main() {
|
||||
|
||||
#include <skinbase_vertex>
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
#include <skinning_vertex>
|
||||
#include <project_vertex>
|
||||
|
||||
vPosition = mvPosition;
|
||||
|
||||
vec4 worldPosition = vec4( transformed, 1.0 );
|
||||
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
worldPosition = instanceMatrix * worldPosition;
|
||||
|
||||
#endif
|
||||
|
||||
worldPosition = modelMatrix * worldPosition;
|
||||
|
||||
projTexCoord = textureMatrix * worldPosition;
|
||||
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`#include <packing>
|
||||
varying vec4 vPosition;
|
||||
varying vec4 projTexCoord;
|
||||
uniform sampler2D depthTexture;
|
||||
uniform vec2 cameraNearFar;
|
||||
|
||||
void main() {
|
||||
|
||||
float depth = unpackRGBAToDepth(texture2DProj( depthTexture, projTexCoord ));
|
||||
float viewZ = - DEPTH_TO_VIEW_Z( depth, cameraNearFar.x, cameraNearFar.y );
|
||||
float depthTest = (-vPosition.z > viewZ) ? 1.0 : 0.0;
|
||||
gl_FragColor = vec4(0.0, depthTest, 1.0, 1.0);
|
||||
|
||||
}`
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
getEdgeDetectionMaterial() {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
uniforms: {
|
||||
'maskTexture': { value: null },
|
||||
'texSize': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'visibleEdgeColor': { value: new Vector3( 1.0, 1.0, 1.0 ) },
|
||||
'hiddenEdgeColor': { value: new Vector3( 1.0, 1.0, 1.0 ) },
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
uniform sampler2D maskTexture;
|
||||
uniform vec2 texSize;
|
||||
uniform vec3 visibleEdgeColor;
|
||||
uniform vec3 hiddenEdgeColor;
|
||||
|
||||
void main() {
|
||||
vec2 invSize = 1.0 / texSize;
|
||||
vec4 uvOffset = vec4(1.0, 0.0, 0.0, 1.0) * vec4(invSize, invSize);
|
||||
vec4 c1 = texture2D( maskTexture, vUv + uvOffset.xy);
|
||||
vec4 c2 = texture2D( maskTexture, vUv - uvOffset.xy);
|
||||
vec4 c3 = texture2D( maskTexture, vUv + uvOffset.yw);
|
||||
vec4 c4 = texture2D( maskTexture, vUv - uvOffset.yw);
|
||||
float diff1 = (c1.r - c2.r)*0.5;
|
||||
float diff2 = (c3.r - c4.r)*0.5;
|
||||
float d = length( vec2(diff1, diff2) );
|
||||
float a1 = min(c1.g, c2.g);
|
||||
float a2 = min(c3.g, c4.g);
|
||||
float visibilityFactor = min(a1, a2);
|
||||
vec3 edgeColor = 1.0 - visibilityFactor > 0.001 ? visibleEdgeColor : hiddenEdgeColor;
|
||||
gl_FragColor = vec4(edgeColor, 1.0) * vec4(d);
|
||||
}`
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
getSeperableBlurMaterial( maxRadius ) {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
defines: {
|
||||
'MAX_RADIUS': maxRadius,
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
'colorTexture': { value: null },
|
||||
'texSize': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'direction': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'kernelRadius': { value: 1.0 }
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`#include <common>
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D colorTexture;
|
||||
uniform vec2 texSize;
|
||||
uniform vec2 direction;
|
||||
uniform float kernelRadius;
|
||||
|
||||
float gaussianPdf(in float x, in float sigma) {
|
||||
return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 invSize = 1.0 / texSize;
|
||||
float sigma = kernelRadius/2.0;
|
||||
float weightSum = gaussianPdf(0.0, sigma);
|
||||
vec4 diffuseSum = texture2D( colorTexture, vUv) * weightSum;
|
||||
vec2 delta = direction * invSize * kernelRadius/float(MAX_RADIUS);
|
||||
vec2 uvOffset = delta;
|
||||
for( int i = 1; i <= MAX_RADIUS; i ++ ) {
|
||||
float x = kernelRadius * float(i) / float(MAX_RADIUS);
|
||||
float w = gaussianPdf(x, sigma);
|
||||
vec4 sample1 = texture2D( colorTexture, vUv + uvOffset);
|
||||
vec4 sample2 = texture2D( colorTexture, vUv - uvOffset);
|
||||
diffuseSum += ((sample1 + sample2) * w);
|
||||
weightSum += (2.0 * w);
|
||||
uvOffset += delta;
|
||||
}
|
||||
gl_FragColor = diffuseSum/weightSum;
|
||||
}`
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
getOverlayMaterial() {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
uniforms: {
|
||||
'maskTexture': { value: null },
|
||||
'edgeTexture1': { value: null },
|
||||
'edgeTexture2': { value: null },
|
||||
'patternTexture': { value: null },
|
||||
'edgeStrength': { value: 1.0 },
|
||||
'edgeGlow': { value: 1.0 },
|
||||
'usePatternTexture': { value: 0.0 }
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
uniform sampler2D maskTexture;
|
||||
uniform sampler2D edgeTexture1;
|
||||
uniform sampler2D edgeTexture2;
|
||||
uniform sampler2D patternTexture;
|
||||
uniform float edgeStrength;
|
||||
uniform float edgeGlow;
|
||||
uniform bool usePatternTexture;
|
||||
|
||||
void main() {
|
||||
vec4 edgeValue1 = texture2D(edgeTexture1, vUv);
|
||||
vec4 edgeValue2 = texture2D(edgeTexture2, vUv);
|
||||
vec4 maskColor = texture2D(maskTexture, vUv);
|
||||
vec4 patternColor = texture2D(patternTexture, 6.0 * vUv);
|
||||
float visibilityFactor = 1.0 - maskColor.g > 0.0 ? 1.0 : 0.5;
|
||||
vec4 edgeValue = edgeValue1 + edgeValue2 * edgeGlow;
|
||||
vec4 finalColor = edgeStrength * maskColor.r * edgeValue;
|
||||
if(usePatternTexture)
|
||||
finalColor += + visibilityFactor * (1.0 - maskColor.r) * (1.0 - patternColor.r);
|
||||
gl_FragColor = finalColor;
|
||||
}`,
|
||||
blending: AdditiveBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
transparent: true
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OutlinePass.BlurDirectionX = new Vector2( 1.0, 0.0 );
|
||||
OutlinePass.BlurDirectionY = new Vector2( 0.0, 1.0 );
|
||||
|
||||
export { OutlinePass };
|
@@ -5,14 +5,19 @@
|
||||
|
||||
export { GLTFLoader } from "./examples/jsm/loaders/GLTFLoader";
|
||||
export { DRACOLoader } from "./examples/jsm/loaders/DRACOLoader";
|
||||
|
||||
export * as SkeletonUtils from "./examples/jsm/utils/SkeletonUtils";
|
||||
|
||||
export { TransformControls } from "./examples/jsm/controls/TransformControls";
|
||||
|
||||
export { EffectComposer } from "./examples/jsm/postprocessing/EffectComposer";
|
||||
export { RenderPass } from "./examples/jsm/postprocessing/RenderPass";
|
||||
export { OutlinePass } from "./examples/jsm/postprocessing/OutlinePass";
|
||||
export { ShaderPass } from "./examples/jsm/postprocessing/ShaderPass";
|
||||
export { SMAAPass } from "./examples/jsm/postprocessing/SMAAPass";
|
||||
export { OutputPass } from "./examples/jsm/postprocessing/OutputPass";
|
||||
export { UnrealBloomPass } from "./examples/jsm/postprocessing/UnrealBloomPass";
|
||||
|
||||
export { BrightnessContrastShader } from "./examples/jsm/shaders/BrightnessContrastShader";
|
||||
export { HueSaturationShader } from "./examples/jsm/shaders/HueSaturationShader";
|
||||
export { ExposureShader } from "./examples/jsm/shaders/ExposureShader";
|
||||
|
2
newIDE/app/public/JsPlatform/Extensions/physics3d.svg
Normal file
2
newIDE/app/public/JsPlatform/Extensions/physics3d.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14" height="14" version="1.1" viewBox="0 0 14 14" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m7 0-6 3.5v7l6 3.5 6-3.5v-7l-6-3.5z" fill="#2e388a"/><g transform="translate(-.0405 .026)" fill="none" stroke="#53a6da" stroke-width=".7"><path d="m8.294 9.112c-1.291 0.7556-2.601 1.165-3.664 1.229-1.085 0.0656-1.803-0.2277-2.095-0.7272-0.2924-0.4996-0.1966-1.269 0.3921-2.183 0.5768-0.8956 1.575-1.837 2.866-2.593 1.291-0.7556 2.601-1.165 3.664-1.229 1.085-0.06552 1.803 0.2277 2.095 0.7272s0.1964 1.269-0.3862 2.174-1.581 1.846-2.872 2.602z"/><path d="m5.787 9.112c1.291 0.7556 2.601 1.165 3.664 1.229 1.085 0.0656 1.803-0.2277 2.095-0.7272 0.2924-0.4996 0.1966-1.269-0.3921-2.183-0.5768-0.8956-1.575-1.837-2.866-2.593-1.291-0.7556-2.601-1.165-3.664-1.229-1.085-0.06552-1.803 0.2277-2.095 0.7272s-0.1964 1.269 0.3862 2.174 1.581 1.846 2.872 2.602z"/><path d="m7.038 1.75c-0.5788 0-1.194 0.4711-1.686 1.441-0.4817 0.9502-0.7898 2.287-0.7898 3.783s0.3081 2.833 0.7898 3.783c0.4916 0.9697 1.107 1.441 1.686 1.441s1.194-0.4712 1.686-1.441c0.4817-0.9502 0.7898-2.287 0.7898-3.783s-0.3081-2.833-0.7898-3.783c-0.4916-0.9698-1.107-1.441-1.686-1.441z"/></g><path d="m7 8c0.5524 0 1-0.4478 1-1 0-0.5524-0.4478-1-1-1s-1 0.4478-1 1c0 0.5524 0.4478 1 1 1z" fill="#fff" stroke-width=".6439"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14" height="14" version="1.1" viewBox="0 0 14 14" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m7 0-6 3.5v7l6 3.5 6-3.5v-7l-6-3.5z" fill="#2e388a"/><g fill="#27aae1" stroke-width=".7"><path d="m9.1 3.801v1.398h-0.6992-2.801-2.1v1.4 1.4h1.4v-1.4h0.6992v2.801h-2.1v1.4h2.1 1.4v-1.4h1.4 0.6992v1.4h1.4v-1.4-1.4h-2.1v-1.4h2.1v-1.4-1.398h-1.4z"/><path d="m7 5.2c0.7732 0 1.4-0.6268 1.4-1.4s-0.6268-1.4-1.4-1.4-1.4 0.6268-1.4 1.4 0.6268 1.4 1.4 1.4z"/></g></svg>
|
After Width: | Height: | Size: 528 B |
@@ -54,7 +54,11 @@ export const AssetCard = ({
|
||||
<div id={id} style={{ ...styles.cardContainer, width: size, height: size }}>
|
||||
<div style={{ ...styles.previewContainer, width: size, height: size }}>
|
||||
<CheckeredBackground />
|
||||
<AssetPreviewImage assetShortHeader={assetShortHeader} maxSize={128} />
|
||||
<AssetPreviewImage
|
||||
assetShortHeader={assetShortHeader}
|
||||
maxSize={size}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
@@ -28,9 +28,14 @@ const styles = {
|
||||
type Props = {|
|
||||
assetShortHeader: AssetShortHeader,
|
||||
maxSize?: number,
|
||||
loading?: 'lazy',
|
||||
|};
|
||||
|
||||
export const AssetPreviewImage = ({ assetShortHeader, maxSize }: Props) => {
|
||||
export const AssetPreviewImage = ({
|
||||
assetShortHeader,
|
||||
maxSize,
|
||||
loading,
|
||||
}: Props) => {
|
||||
const previewImageUrl = assetShortHeader.previewImageUrls[0];
|
||||
const isPrivate = isPrivateAsset(assetShortHeader);
|
||||
const [isLoaded, setIsLoaded] = React.useState(false);
|
||||
@@ -55,6 +60,7 @@ export const AssetPreviewImage = ({ assetShortHeader, maxSize }: Props) => {
|
||||
url={previewImageUrl}
|
||||
alt={assetShortHeader.name}
|
||||
onLoad={onLoad}
|
||||
loading={loading}
|
||||
/>
|
||||
) : (
|
||||
<CorsAwareImage
|
||||
@@ -63,6 +69,7 @@ export const AssetPreviewImage = ({ assetShortHeader, maxSize }: Props) => {
|
||||
src={previewImageUrl}
|
||||
alt={assetShortHeader.name}
|
||||
onLoad={onLoad}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@@ -53,7 +53,7 @@ import HelpIcon from '../UI/HelpIcon';
|
||||
import { OwnedProductLicense } from './ProductLicense/ProductLicenseOptions';
|
||||
import { getUserProductPurchaseUsageType } from './ProductPageHelper';
|
||||
|
||||
const ASSETS_DISPLAY_LIMIT = 250;
|
||||
const ASSETS_DISPLAY_LIMIT = 60;
|
||||
|
||||
const getAssetSize = (windowSize: WindowSizeType) => {
|
||||
switch (windowSize) {
|
||||
@@ -338,7 +338,7 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
scrollViewElement.scrollToPosition(y);
|
||||
},
|
||||
setPageBreakIndex: (index: number) => {
|
||||
setPageBreakIndex(0);
|
||||
setPageBreakIndex(index);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -471,6 +471,8 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
if (!assetShortHeaders) return null;
|
||||
// Don't show assets if filtering on asset packs.)
|
||||
if (hasAssetPackFiltersApplied && !openedAssetPack) return [];
|
||||
const assetSize = getAssetSize(windowSize);
|
||||
const margin = cellSpacing / 2;
|
||||
|
||||
return getAssetShortHeadersToDisplay(
|
||||
assetShortHeaders,
|
||||
@@ -480,9 +482,9 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
<AssetCardTile
|
||||
assetShortHeader={assetShortHeader}
|
||||
onOpenDetails={() => onOpenDetails(assetShortHeader)}
|
||||
size={getAssetSize(windowSize)}
|
||||
size={assetSize}
|
||||
key={assetShortHeader.id}
|
||||
margin={cellSpacing / 2}
|
||||
margin={margin}
|
||||
hideShortDescription={!!hideDetails}
|
||||
/>
|
||||
));
|
||||
|
@@ -11,6 +11,7 @@ type Props = {|
|
||||
onError?: () => void,
|
||||
onLoad?: (e: any) => void,
|
||||
hideLoader?: boolean,
|
||||
loading?: 'lazy',
|
||||
|};
|
||||
|
||||
const AuthorizedAssetImage = ({
|
||||
@@ -20,6 +21,7 @@ const AuthorizedAssetImage = ({
|
||||
onError,
|
||||
onLoad,
|
||||
hideLoader,
|
||||
loading,
|
||||
}: Props) => {
|
||||
const [authorizedUrl, setAuthorizedUrl] = React.useState(null);
|
||||
const [isImageLoaded, setIsImageLoaded] = React.useState(false);
|
||||
@@ -78,6 +80,7 @@ const AuthorizedAssetImage = ({
|
||||
<img
|
||||
alt={alt}
|
||||
src={authorizedUrl}
|
||||
loading={loading}
|
||||
style={{
|
||||
...style,
|
||||
// Use display none to load the image in the background, but not
|
||||
|
@@ -244,6 +244,7 @@ export const PublicAssetPackTile = ({
|
||||
style={styles.previewImage}
|
||||
src={assetPack.thumbnailUrl}
|
||||
alt={`Preview image of asset pack ${assetPack.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
@@ -299,6 +300,7 @@ export const PrivateAssetPackTile = ({
|
||||
style={styles.previewImage}
|
||||
src={assetPackListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of asset pack ${assetPackListingData.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
{assetPackListingData.redeemConditions && !owned && (
|
||||
<div style={styles.redeemableContainer}>
|
||||
@@ -367,6 +369,7 @@ export const PromoBundleCard = ({
|
||||
}}
|
||||
src={productListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of bundle ${productListingData.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<Column expand alignItems="flex-start" justifyContent="center">
|
||||
@@ -476,6 +479,7 @@ export const CategoryTile = ({
|
||||
}}
|
||||
src={imageSource}
|
||||
alt={imageAlt}
|
||||
// No lazy loading because categories are the first seen tiles in the shop.
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="center" noMargin>
|
||||
@@ -527,6 +531,7 @@ export const PrivateGameTemplateTile = ({
|
||||
alt={`Preview image of game template ${
|
||||
privateGameTemplateListingData.name
|
||||
}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div style={styles.priceTagContainer}>
|
||||
<ProductPriceTag
|
||||
@@ -603,6 +608,7 @@ export const ExampleTile = ({
|
||||
style={styles.previewImage}
|
||||
src={thumbnailImgUrl}
|
||||
alt={`Preview image of example ${exampleShortHeader.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<EmptyMessage
|
||||
|
@@ -645,11 +645,11 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
<Column expand useFullHeight noMargin>
|
||||
<SearchBar
|
||||
placeholder={
|
||||
hideGameTemplates ? t`Search assets` : `Search the shop`
|
||||
hideGameTemplates ? t`Search assets` : t`Search the shop`
|
||||
}
|
||||
value={searchText}
|
||||
onChange={(newValue: string) => {
|
||||
if (searchText === newValue) {
|
||||
if (searchText === newValue || newValue.length === 1) {
|
||||
return;
|
||||
}
|
||||
setSearchText(newValue);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import BehaviorPropertiesEditor from './Editors/BehaviorPropertiesEditor';
|
||||
import Physics2Editor from './Editors/Physics2Editor';
|
||||
import Physics3DEditor from './Editors/Physics3DEditor';
|
||||
|
||||
/**
|
||||
* A service returning editor components for each behavior type.
|
||||
@@ -16,6 +17,9 @@ const BehaviorsEditorService = {
|
||||
'Physics2::Physics2Behavior': {
|
||||
component: Physics2Editor,
|
||||
},
|
||||
'Physics3D::Physics3DBehavior': {
|
||||
component: Physics3DEditor,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
402
newIDE/app/src/BehaviorsEditor/Editors/Physics3DEditor/index.js
Normal file
402
newIDE/app/src/BehaviorsEditor/Editors/Physics3DEditor/index.js
Normal file
@@ -0,0 +1,402 @@
|
||||
// @flow
|
||||
import { t } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Line, Column, Spacer } from '../../../UI/Grid';
|
||||
import Checkbox from '../../../UI/Checkbox';
|
||||
import SelectField from '../../../UI/SelectField';
|
||||
import SelectOption from '../../../UI/SelectOption';
|
||||
import SemiControlledTextField from '../../../UI/SemiControlledTextField';
|
||||
import { getMeasurementUnitShortLabel } from '../../../PropertiesEditor/PropertiesMapToSchema';
|
||||
import MeasurementUnitDocumentation from '../../../PropertiesEditor/MeasurementUnitDocumentation';
|
||||
import { type BehaviorEditorProps } from '../BehaviorEditorProps.flow';
|
||||
import Text from '../../../UI/Text';
|
||||
import DismissableAlertMessage from '../../../UI/DismissableAlertMessage';
|
||||
import { ResponsiveLineStackLayout } from '../../../UI/Layout';
|
||||
import useForceUpdate from '../../../Utils/UseForceUpdate';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
type Props = BehaviorEditorProps;
|
||||
|
||||
const NumericProperty = (props: {|
|
||||
id?: string,
|
||||
properties: gdMapStringPropertyDescriptor,
|
||||
propertyName: string,
|
||||
step: number,
|
||||
onUpdate: (newValue: string) => void,
|
||||
|}) => {
|
||||
const { properties, propertyName, step, onUpdate, id } = props;
|
||||
const property = properties.get(propertyName);
|
||||
|
||||
return (
|
||||
<SemiControlledTextField
|
||||
id={id}
|
||||
fullWidth
|
||||
value={property.getValue()}
|
||||
key={propertyName}
|
||||
floatingLabelText={property.getLabel()}
|
||||
step={step}
|
||||
onChange={onUpdate}
|
||||
type="number"
|
||||
endAdornment={<UnitAdornment property={property} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const UnitAdornment = (props: {| property: gdPropertyDescriptor |}) => {
|
||||
const { property } = props;
|
||||
const measurementUnit = property.getMeasurementUnit();
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<MeasurementUnitDocumentation
|
||||
label={measurementUnit.getLabel()}
|
||||
description={measurementUnit.getDescription()}
|
||||
elementsWithWords={measurementUnit.getElementsWithWords()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InputAdornment position="end">
|
||||
{getMeasurementUnitShortLabel(measurementUnit)}
|
||||
</InputAdornment>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BitGroupEditor = (props: {|
|
||||
bits: Array<boolean>,
|
||||
onChange: (index: number, value: boolean) => void,
|
||||
firstIndex: number,
|
||||
disabled: boolean,
|
||||
|}) => {
|
||||
return (
|
||||
<div style={{ overflowX: 'auto', flex: 1 }}>
|
||||
<ButtonGroup disableElevation fullWidth disabled={props.disabled}>
|
||||
{props.bits.map((bit, index) => (
|
||||
<Button
|
||||
key={props.firstIndex + index}
|
||||
variant={bit ? 'contained' : 'outlined'}
|
||||
color={bit ? 'primary' : 'default'}
|
||||
onClick={() => props.onChange(props.firstIndex + index, !bit)}
|
||||
>
|
||||
{props.firstIndex + index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isBitEnabled = (bitsValue: number, pos: number) => {
|
||||
return !!(bitsValue & (1 << pos));
|
||||
};
|
||||
|
||||
const enableBit = (bitsValue: number, pos: number, enable: boolean) => {
|
||||
if (enable) bitsValue |= 1 << pos;
|
||||
else bitsValue &= ~(1 << pos);
|
||||
return bitsValue;
|
||||
};
|
||||
|
||||
const Physics3DEditor = (props: Props) => {
|
||||
const { behavior, onBehaviorUpdated } = props;
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const updateBehaviorProperty = React.useCallback(
|
||||
(property, value) => {
|
||||
behavior.updateProperty(property, value);
|
||||
forceUpdate();
|
||||
onBehaviorUpdated();
|
||||
},
|
||||
[behavior, forceUpdate, onBehaviorUpdated]
|
||||
);
|
||||
|
||||
const properties = behavior.getProperties();
|
||||
const staticBits = Array(4).fill(null);
|
||||
const dynamicBits = Array(4).fill(null);
|
||||
const shape = properties.get('shape').getValue();
|
||||
const layersValues = parseInt(properties.get('layers').getValue(), 10);
|
||||
const masksValues = parseInt(properties.get('masks').getValue(), 10);
|
||||
|
||||
const isStatic = properties.get('bodyType').getValue() === 'Static';
|
||||
|
||||
return (
|
||||
<Column
|
||||
expand
|
||||
// Avoid overflow on small screens
|
||||
noOverflowParent
|
||||
>
|
||||
<Line>
|
||||
<SelectField
|
||||
id="physics3d-parameter-body-type"
|
||||
key={'bodyType'}
|
||||
fullWidth
|
||||
floatingLabelText={properties.get('bodyType').getLabel()}
|
||||
value={properties.get('bodyType').getValue()}
|
||||
onChange={(e, i, newValue: string) =>
|
||||
updateBehaviorProperty('bodyType', newValue)
|
||||
}
|
||||
>
|
||||
{[
|
||||
<SelectOption
|
||||
key={'dynamic'}
|
||||
value={'Dynamic'}
|
||||
label={t`Dynamic`}
|
||||
/>,
|
||||
<SelectOption key={'static'} value={'Static'} label={t`Static`} />,
|
||||
<SelectOption
|
||||
key={'kinematic'}
|
||||
value={'Kinematic'}
|
||||
label={t`Kinematic`}
|
||||
/>,
|
||||
]}
|
||||
</SelectField>
|
||||
</Line>
|
||||
<ResponsiveLineStackLayout>
|
||||
<Checkbox
|
||||
label={properties.get('bullet').getLabel()}
|
||||
checked={properties.get('bullet').getValue() === 'true'}
|
||||
onCheck={(e, checked) =>
|
||||
updateBehaviorProperty('bullet', checked ? '1' : '0')
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label={properties.get('fixedRotation').getLabel()}
|
||||
checked={properties.get('fixedRotation').getValue() === 'true'}
|
||||
onCheck={(e, checked) =>
|
||||
updateBehaviorProperty('fixedRotation', checked ? '1' : '0')
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<Line>
|
||||
<DismissableAlertMessage
|
||||
identifier="physics2-shape-collisions"
|
||||
kind="info"
|
||||
>
|
||||
<Trans>
|
||||
The shape used in the Physics behavior is independent from the
|
||||
collision mask of the object. Be sure to use the "Collision"
|
||||
condition provided by the Physics behavior in the events. The usual
|
||||
"Collision" condition won't take into account the shape that you've
|
||||
set up here.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
</Line>
|
||||
<ResponsiveLineStackLayout>
|
||||
<SelectField
|
||||
id="physics3d-parameter-shape"
|
||||
fullWidth
|
||||
floatingLabelText={properties.get('shape').getLabel()}
|
||||
value={properties.get('shape').getValue()}
|
||||
onChange={(e, i, newValue: string) =>
|
||||
updateBehaviorProperty('shape', newValue)
|
||||
}
|
||||
>
|
||||
<SelectOption key={'sphere'} value={'Sphere'} label={t`Sphere`} />
|
||||
<SelectOption key={'box'} value={'Box'} label={t`Box`} />
|
||||
<SelectOption key={'capsule'} value={'Capsule'} label={t`Capsule`} />
|
||||
<SelectOption
|
||||
key={'cylinder'}
|
||||
value={'Cylinder'}
|
||||
label={t`Cylinder`}
|
||||
/>
|
||||
</SelectField>
|
||||
<SelectField
|
||||
id="physics3d-parameter-shape-orientation"
|
||||
fullWidth
|
||||
floatingLabelText={properties.get('shapeOrientation').getLabel()}
|
||||
value={properties.get('shapeOrientation').getValue()}
|
||||
onChange={(e, i, newValue: string) =>
|
||||
updateBehaviorProperty('shapeOrientation', newValue)
|
||||
}
|
||||
disabled={
|
||||
properties.get('shape').getValue() === 'Sphere' ||
|
||||
properties.get('shape').getValue() === 'Box'
|
||||
}
|
||||
>
|
||||
<SelectOption key={'shape-orientation-z'} value={'Z'} label={t`Z`} />
|
||||
<SelectOption key={'shape-orientation-y'} value={'Y'} label={t`Y`} />
|
||||
<SelectOption key={'shape-orientation-x'} value={'X'} label={t`X`} />
|
||||
</SelectField>
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout>
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionA').getValue()}
|
||||
key={'shapeDimensionA'}
|
||||
floatingLabelText={shape === 'Box' ? 'Width' : 'Radius'}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionA', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionA')} />
|
||||
}
|
||||
/>
|
||||
{shape !== 'Sphere' && (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionB').getValue()}
|
||||
key={'shapeDimensionB'}
|
||||
floatingLabelText={shape === 'Box' ? 'Width' : 'Depth'}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionB', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionB')} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{shape === 'Box' && (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionC').getValue()}
|
||||
key={'shapeDimensionC'}
|
||||
floatingLabelText={'Depth'}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionC', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionC')} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout>
|
||||
<NumericProperty
|
||||
id="physics3d-parameter-density"
|
||||
properties={properties}
|
||||
propertyName={'density'}
|
||||
step={0.1}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty(
|
||||
'density',
|
||||
parseFloat(newValue) > 0 ? newValue : '0'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<NumericProperty
|
||||
properties={properties}
|
||||
propertyName={'gravityScale'}
|
||||
step={0.1}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty('gravityScale', newValue)
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout>
|
||||
<NumericProperty
|
||||
properties={properties}
|
||||
propertyName={'friction'}
|
||||
step={0.1}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty(
|
||||
'friction',
|
||||
parseFloat(newValue) > 0 ? newValue : '0'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<NumericProperty
|
||||
properties={properties}
|
||||
propertyName={'restitution'}
|
||||
step={0.1}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty(
|
||||
'restitution',
|
||||
parseFloat(newValue) > 0 ? newValue : '0'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout>
|
||||
<NumericProperty
|
||||
properties={properties}
|
||||
propertyName={'linearDamping'}
|
||||
step={0.05}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty('linearDamping', newValue)
|
||||
}
|
||||
/>
|
||||
<NumericProperty
|
||||
id="physics3d-parameter-angular-damping"
|
||||
properties={properties}
|
||||
propertyName={'angularDamping'}
|
||||
step={0.05}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty('angularDamping', newValue)
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<Line>
|
||||
<Text style={{ marginRight: 10 }}>
|
||||
{properties.get('layers').getLabel()}
|
||||
</Text>
|
||||
<BitGroupEditor
|
||||
key={'static-layers'}
|
||||
firstIndex={0}
|
||||
bits={staticBits.map(
|
||||
(_, index) => isBitEnabled(layersValues, index) && isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(layersValues, index, value);
|
||||
updateBehaviorProperty('layers', newValue.toString(10));
|
||||
}}
|
||||
disabled={!isStatic}
|
||||
/>
|
||||
<Spacer />
|
||||
<BitGroupEditor
|
||||
key={'dynamic-layers'}
|
||||
firstIndex={4}
|
||||
bits={dynamicBits.map(
|
||||
(_, index) => isBitEnabled(layersValues, index + 4) && !isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(layersValues, index, value);
|
||||
updateBehaviorProperty('layers', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Text style={{ marginRight: 10 }}>
|
||||
{properties.get('masks').getLabel()}
|
||||
</Text>
|
||||
<BitGroupEditor
|
||||
key={'static-mask'}
|
||||
firstIndex={0}
|
||||
bits={staticBits.map(
|
||||
(_, index) => isBitEnabled(masksValues, index) || isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(masksValues, index, value);
|
||||
updateBehaviorProperty('masks', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
<Spacer />
|
||||
<BitGroupEditor
|
||||
key={'dynamic-mask'}
|
||||
firstIndex={4}
|
||||
bits={dynamicBits.map(
|
||||
(_, index) => isBitEnabled(masksValues, index + 4) || isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(masksValues, index, value);
|
||||
updateBehaviorProperty('masks', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Physics3DEditor;
|
@@ -51,18 +51,19 @@ export default class CommandManager implements CommandManagerInterface {
|
||||
}
|
||||
|
||||
registerCommand = (commandName: CommandName, command: Command) => {
|
||||
if (this._commands[commandName])
|
||||
return console.warn(
|
||||
`Tried to register command ${commandName}, but it is already registered.`
|
||||
);
|
||||
if (this._commands[commandName]) return;
|
||||
// if (this._commands[commandName])
|
||||
// return console.warn(
|
||||
// `Tried to register command ${commandName}, but it is already registered.`
|
||||
// );
|
||||
this._commands[commandName] = command;
|
||||
};
|
||||
|
||||
deregisterCommand = (commandName: CommandName) => {
|
||||
if (!this._commands[commandName])
|
||||
return console.warn(
|
||||
`Tried to deregister command ${commandName}, but it is not registered.`
|
||||
);
|
||||
// if (!this._commands[commandName])
|
||||
// return console.warn(
|
||||
// `Tried to deregister command ${commandName}, but it is not registered.`
|
||||
// );
|
||||
delete this._commands[commandName];
|
||||
};
|
||||
|
||||
|
@@ -1,38 +1,62 @@
|
||||
// @flow
|
||||
import { t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import { type DebuggerId } from '../ExportAndShare/PreviewLauncher.flow';
|
||||
import {
|
||||
type DebuggerId,
|
||||
type DebuggerStatus,
|
||||
} from '../ExportAndShare/PreviewLauncher.flow';
|
||||
|
||||
type Props = {|
|
||||
selectedId: DebuggerId,
|
||||
debuggerIds: Array<DebuggerId>,
|
||||
debuggerStatus: { [DebuggerId]: DebuggerStatus },
|
||||
onChooseDebugger: DebuggerId => void,
|
||||
|};
|
||||
|
||||
export default class DebuggerSelector extends React.Component<Props, void> {
|
||||
render() {
|
||||
const hasDebuggers = !!this.props.debuggerIds.length;
|
||||
const debuggerIds = Object.keys(this.props.debuggerStatus);
|
||||
const hasDebuggers = !!debuggerIds.length;
|
||||
return (
|
||||
<SelectField
|
||||
fullWidth
|
||||
value={hasDebuggers ? this.props.selectedId : 0}
|
||||
onChange={(e, i, value) =>
|
||||
this.props.onChooseDebugger(parseInt(value, 10) || 0)
|
||||
}
|
||||
disabled={!hasDebuggers}
|
||||
>
|
||||
{this.props.debuggerIds.map(id => (
|
||||
<SelectOption value={id} key={id} label={t`Game preview #${id}`} />
|
||||
))}
|
||||
{!hasDebuggers && (
|
||||
<SelectOption
|
||||
value={0}
|
||||
label={t`No preview running. Run a preview and you will be able to inspect it with the debugger`}
|
||||
/>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<SelectField
|
||||
fullWidth
|
||||
value={hasDebuggers ? this.props.selectedId : 0}
|
||||
onChange={(e, i, value) =>
|
||||
this.props.onChooseDebugger(parseInt(value, 10) || 0)
|
||||
}
|
||||
disabled={!hasDebuggers}
|
||||
>
|
||||
{debuggerIds.map(id => {
|
||||
const status = this.props.debuggerStatus[+id];
|
||||
const statusText = status.isPaused
|
||||
? status.isInGameEdition
|
||||
? t`Editing`
|
||||
: t`Paused`
|
||||
: status.isInGameEdition
|
||||
? t`Playing (in-game edition)`
|
||||
: t`Playing`;
|
||||
|
||||
return (
|
||||
<SelectOption
|
||||
value={+id}
|
||||
key={id}
|
||||
label={t`Game preview #${id} (${i18n._(statusText)})`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!hasDebuggers && (
|
||||
<SelectOption
|
||||
value={0}
|
||||
label={t`No preview running. Run a preview and you will be able to inspect it with the debugger`}
|
||||
/>
|
||||
)}
|
||||
</SelectField>
|
||||
)}
|
||||
</SelectField>
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import EmptyMessage from '../UI/EmptyMessage';
|
||||
import {
|
||||
type PreviewDebuggerServer,
|
||||
type DebuggerId,
|
||||
type DebuggerStatus,
|
||||
} from '../ExportAndShare/PreviewLauncher.flow';
|
||||
import { type Log, LogsManager } from './DebuggerConsole';
|
||||
|
||||
@@ -53,7 +54,7 @@ type State = {|
|
||||
debuggerGameData: { [DebuggerId]: any },
|
||||
profilerOutputs: { [DebuggerId]: ProfilerOutput },
|
||||
profilingInProgress: { [DebuggerId]: boolean },
|
||||
gameIsPaused: { [DebuggerId]: boolean },
|
||||
debuggerStatus: { [DebuggerId]: DebuggerStatus },
|
||||
selectedId: DebuggerId,
|
||||
logs: { [DebuggerId]: Array<Log> },
|
||||
|};
|
||||
@@ -70,7 +71,7 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
debuggerGameData: {},
|
||||
profilerOutputs: {},
|
||||
profilingInProgress: {},
|
||||
gameIsPaused: {},
|
||||
debuggerStatus: {},
|
||||
selectedId: 0,
|
||||
logs: {},
|
||||
};
|
||||
@@ -79,18 +80,22 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
_debuggerLogs: Map<number, LogsManager> = new Map();
|
||||
|
||||
updateToolbar = () => {
|
||||
const { selectedId, gameIsPaused } = this.state;
|
||||
const { selectedId, debuggerStatus } = this.state;
|
||||
|
||||
const selectedDebuggerContents = this._debuggerContents[
|
||||
this.state.selectedId
|
||||
];
|
||||
|
||||
const isSelectedDebuggerPaused = debuggerStatus[selectedId]
|
||||
? debuggerStatus[selectedId].isPaused
|
||||
: false;
|
||||
|
||||
this.props.setToolbar(
|
||||
<Toolbar
|
||||
onPlay={() => this._play(this.state.selectedId)}
|
||||
onPause={() => this._pause(this.state.selectedId)}
|
||||
canPlay={this._hasSelectedDebugger() && gameIsPaused[selectedId]}
|
||||
canPause={this._hasSelectedDebugger() && !gameIsPaused[selectedId]}
|
||||
canPlay={this._hasSelectedDebugger() && isSelectedDebuggerPaused}
|
||||
canPause={this._hasSelectedDebugger() && !isSelectedDebuggerPaused}
|
||||
canOpenProfiler={this._hasSelectedDebugger()}
|
||||
isProfilerShown={
|
||||
!!selectedDebuggerContents &&
|
||||
@@ -161,14 +166,14 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
debuggerGameData,
|
||||
profilerOutputs,
|
||||
profilingInProgress,
|
||||
gameIsPaused,
|
||||
debuggerStatus,
|
||||
}) => {
|
||||
// Remove any data bound to the instance that might have been stored.
|
||||
// Otherwise this would be a memory leak.
|
||||
if (debuggerGameData[id]) delete debuggerGameData[id];
|
||||
if (profilerOutputs[id]) delete profilerOutputs[id];
|
||||
if (profilingInProgress[id]) delete profilingInProgress[id];
|
||||
if (gameIsPaused[id]) delete gameIsPaused[id];
|
||||
if (debuggerStatus[id]) delete debuggerStatus[id];
|
||||
|
||||
return {
|
||||
debuggerIds,
|
||||
@@ -181,7 +186,7 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
debuggerGameData,
|
||||
profilerOutputs,
|
||||
profilingInProgress,
|
||||
gameIsPaused,
|
||||
debuggerStatus,
|
||||
};
|
||||
},
|
||||
() => this.updateToolbar()
|
||||
@@ -219,6 +224,11 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
this.setState({
|
||||
unregisterDebuggerServerCallbacks: unregisterCallbacks,
|
||||
});
|
||||
|
||||
// Fetch the status of each debugger client.
|
||||
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
|
||||
previewDebuggerServer.sendMessage(debuggerId, { command: 'getStatus' });
|
||||
});
|
||||
};
|
||||
|
||||
_handleMessage = (id: DebuggerId, data: any) => {
|
||||
@@ -229,6 +239,16 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
[id]: data.payload,
|
||||
},
|
||||
});
|
||||
} else if (data.command === 'status') {
|
||||
this.setState(
|
||||
state => ({
|
||||
debuggerStatus: {
|
||||
...state.debuggerStatus,
|
||||
[id]: data.payload,
|
||||
},
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
} else if (data.command === 'profiler.output') {
|
||||
this.setState({
|
||||
profilerOutputs: {
|
||||
@@ -244,22 +264,10 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
this.setState(state => ({
|
||||
profilingInProgress: { ...state.profilingInProgress, [id]: false },
|
||||
}));
|
||||
} else if (data.command === 'game.resumed') {
|
||||
this.setState(
|
||||
state => ({
|
||||
gameIsPaused: { ...state.gameIsPaused, [id]: false },
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
} else if (data.command === 'game.paused') {
|
||||
this.setState(
|
||||
state => ({
|
||||
gameIsPaused: { ...state.gameIsPaused, [id]: true },
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
} else if (data.command === 'hotReloader.logs') {
|
||||
// Nothing to do.
|
||||
} else if (data.command === 'instances.updated') {
|
||||
// Nothing to do.
|
||||
} else if (data.command === 'console.log') {
|
||||
// Filter out unavoidable warnings that do not concern non-engine devs.
|
||||
if (isUnavoidableLibraryWarning(data.payload)) return;
|
||||
@@ -276,24 +284,14 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
const { previewDebuggerServer } = this.props;
|
||||
previewDebuggerServer.sendMessage(id, { command: 'play' });
|
||||
|
||||
this.setState(
|
||||
state => ({
|
||||
gameIsPaused: { ...state.gameIsPaused, [id]: false },
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
// Pause status is transmitted by the game (using `status`).
|
||||
};
|
||||
|
||||
_pause = (id: DebuggerId) => {
|
||||
const { previewDebuggerServer } = this.props;
|
||||
previewDebuggerServer.sendMessage(id, { command: 'pause' });
|
||||
|
||||
this.setState(
|
||||
state => ({
|
||||
gameIsPaused: { ...state.gameIsPaused, [id]: true },
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
// Pause status is transmitted by the game (using `status`).
|
||||
};
|
||||
|
||||
_refresh = (id: DebuggerId) => {
|
||||
@@ -345,7 +343,7 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
debuggerServerError,
|
||||
debuggerServerState,
|
||||
selectedId,
|
||||
debuggerIds,
|
||||
debuggerStatus,
|
||||
debuggerGameData,
|
||||
profilerOutputs,
|
||||
profilingInProgress,
|
||||
@@ -375,7 +373,7 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
<Column expand noMargin>
|
||||
<DebuggerSelector
|
||||
selectedId={selectedId}
|
||||
debuggerIds={debuggerIds}
|
||||
debuggerStatus={debuggerStatus}
|
||||
onChooseDebugger={id =>
|
||||
this.setState(
|
||||
{
|
||||
|
115
newIDE/app/src/EmbeddedGame/EmbeddedGameFrame.js
Normal file
115
newIDE/app/src/EmbeddedGame/EmbeddedGameFrame.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type PreviewDebuggerServer } from '../ExportAndShare/PreviewLauncher.flow';
|
||||
|
||||
type AttachToPreviewOptions = {|
|
||||
previewIndexHtmlLocation: string,
|
||||
|};
|
||||
|
||||
type SwitchToSceneEditionOptions = {|
|
||||
sceneName: string,
|
||||
externalLayoutName?: string,
|
||||
|};
|
||||
|
||||
let onAttachToPreview: null | (AttachToPreviewOptions => void) = null;
|
||||
let onSwitchToSceneEdition: null | (SwitchToSceneEditionOptions => void) = null;
|
||||
|
||||
export const attachToPreview = ({
|
||||
previewIndexHtmlLocation,
|
||||
}: AttachToPreviewOptions) => {
|
||||
if (!onAttachToPreview) throw new Error('No EmbeddedGameFrame registered.');
|
||||
onAttachToPreview({ previewIndexHtmlLocation });
|
||||
};
|
||||
|
||||
export const switchToSceneEdition = ({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
}: SwitchToSceneEditionOptions) => {
|
||||
if (!onSwitchToSceneEdition)
|
||||
throw new Error('No EmbeddedGameFrame registered.');
|
||||
onSwitchToSceneEdition({ sceneName, externalLayoutName });
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
previewDebuggerServer: PreviewDebuggerServer | null,
|
||||
onLaunchPreviewForInGameEdition: ({|
|
||||
sceneName: string,
|
||||
externalLayoutName: ?string,
|
||||
|}) => void,
|
||||
|};
|
||||
|
||||
export const EmbeddedGameFrame = ({
|
||||
previewDebuggerServer,
|
||||
onLaunchPreviewForInGameEdition,
|
||||
}: Props) => {
|
||||
const [
|
||||
previewIndexHtmlLocation,
|
||||
setPreviewIndexHtmlLocation,
|
||||
] = React.useState<string>('');
|
||||
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
// TODO: use a real context for this?
|
||||
onAttachToPreview = (options: AttachToPreviewOptions) => {
|
||||
setPreviewIndexHtmlLocation(options.previewIndexHtmlLocation);
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.contentWindow.focus();
|
||||
}
|
||||
};
|
||||
onSwitchToSceneEdition = (options: SwitchToSceneEditionOptions) => {
|
||||
if (!previewDebuggerServer) return;
|
||||
|
||||
const { sceneName, externalLayoutName } = options;
|
||||
|
||||
if (!previewIndexHtmlLocation) {
|
||||
console.info(
|
||||
externalLayoutName
|
||||
? `Launching in-game edition preview for external layout "${externalLayoutName}" (scene: "${sceneName}").`
|
||||
: `Launching in-game edition preview for scene "${sceneName}".`
|
||||
);
|
||||
onLaunchPreviewForInGameEdition({ sceneName, externalLayoutName });
|
||||
} else {
|
||||
console.info(
|
||||
externalLayoutName
|
||||
? `Switching in-game edition previews to external layout "${externalLayoutName}" (scene: "${sceneName}").`
|
||||
: `Switching in-game edition previews to scene "${sceneName}".`
|
||||
);
|
||||
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
|
||||
previewDebuggerServer.sendMessage(debuggerId, {
|
||||
command: 'switchForInGameEdition',
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
previewDebuggerServer,
|
||||
previewIndexHtmlLocation,
|
||||
onLaunchPreviewForInGameEdition,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Game Preview"
|
||||
src={previewIndexHtmlLocation}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -129,6 +129,7 @@ type Props = {|
|
||||
oldName: string,
|
||||
newName: string
|
||||
) => void,
|
||||
children?: React.Node,
|
||||
|};
|
||||
|
||||
export const EventsFunctionParametersEditor = ({
|
||||
@@ -146,6 +147,7 @@ export const EventsFunctionParametersEditor = ({
|
||||
onMoveBehaviorEventsParameter,
|
||||
onMoveObjectEventsParameter,
|
||||
onFunctionParameterWillBeRenamed,
|
||||
children,
|
||||
}: Props) => {
|
||||
const scrollView = React.useRef<?ScrollViewInterface>(null);
|
||||
const [
|
||||
@@ -177,6 +179,22 @@ export const EventsFunctionParametersEditor = ({
|
||||
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const isABehaviorLifecycleEventsFunction =
|
||||
!!eventsBasedBehavior &&
|
||||
gd.MetadataDeclarationHelper.isBehaviorLifecycleEventsFunction(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
const isAnObjectLifecycleEventsFunction =
|
||||
!!eventsBasedObject &&
|
||||
!eventsBasedBehavior &&
|
||||
gd.MetadataDeclarationHelper.isObjectLifecycleEventsFunction(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
freezeParameters =
|
||||
freezeParameters ||
|
||||
isABehaviorLifecycleEventsFunction ||
|
||||
isAnObjectLifecycleEventsFunction;
|
||||
|
||||
const [
|
||||
longDescriptionShownIndexes,
|
||||
setLongDescriptionShownIndexes,
|
||||
@@ -558,22 +576,6 @@ export const EventsFunctionParametersEditor = ({
|
||||
? ParametersIndexOffsets.ObjectFunction
|
||||
: ParametersIndexOffsets.FreeFunction;
|
||||
|
||||
const isABehaviorLifecycleEventsFunction =
|
||||
!!eventsBasedBehavior &&
|
||||
gd.MetadataDeclarationHelper.isBehaviorLifecycleEventsFunction(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
if (isABehaviorLifecycleEventsFunction) {
|
||||
return (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This is a "lifecycle method". It will be called automatically by the
|
||||
game engine and has two parameters: "Object" (the object the behavior
|
||||
is acting on) and "Behavior" (the behavior itself).
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
);
|
||||
}
|
||||
const isAnExtensionLifecycleEventsFunction =
|
||||
!eventsBasedBehavior &&
|
||||
gd.MetadataDeclarationHelper.isExtensionLifecycleEventsFunction(
|
||||
@@ -616,6 +618,18 @@ export const EventsFunctionParametersEditor = ({
|
||||
{parameters.getParametersCount() > 0 || freezeParameters ? (
|
||||
<React.Fragment>
|
||||
<ScrollView ref={scrollView}>
|
||||
{children
|
||||
? React.Children.map(children, (child, index) => {
|
||||
if (!child) return null;
|
||||
return (
|
||||
<Line>
|
||||
<Column noMargin expand>
|
||||
{child}
|
||||
</Column>
|
||||
</Line>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
<Line>
|
||||
<Column noMargin expand>
|
||||
{mapFor(0, parameters.getParametersCount(), i => {
|
||||
@@ -878,25 +892,45 @@ export const EventsFunctionParametersEditor = ({
|
||||
)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Column noMargin expand justifyContent="center">
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first parameter</Trans>}
|
||||
description={
|
||||
<Trans>Parameters allow function users to give data.</Trans>
|
||||
}
|
||||
actionLabel={<Trans>Add a parameter</Trans>}
|
||||
helpPagePath={helpPagePath}
|
||||
helpPageAnchor={'add-and-use-parameters'}
|
||||
onAction={addParameter}
|
||||
secondaryActionIcon={<PasteIcon />}
|
||||
secondaryActionLabel={
|
||||
isClipboardContainingParameters ? <Trans>Paste</Trans> : null
|
||||
}
|
||||
onSecondaryAction={() => {
|
||||
pasteParametersAtTheEnd();
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
<ScrollView>
|
||||
{children
|
||||
? React.Children.map(children, (child, index) => {
|
||||
if (!child) return null;
|
||||
return (
|
||||
<Line>
|
||||
<Column noMargin expand>
|
||||
{child}
|
||||
</Column>
|
||||
</Line>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
<Line>
|
||||
<Column noMargin expand justifyContent="center">
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first parameter</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
Parameters allow function users to give data.
|
||||
</Trans>
|
||||
}
|
||||
actionLabel={<Trans>Add a parameter</Trans>}
|
||||
helpPagePath={helpPagePath}
|
||||
helpPageAnchor={'add-and-use-parameters'}
|
||||
onAction={addParameter}
|
||||
secondaryActionIcon={<PasteIcon />}
|
||||
secondaryActionLabel={
|
||||
isClipboardContainingParameters ? (
|
||||
<Trans>Paste</Trans>
|
||||
) : null
|
||||
}
|
||||
onSecondaryAction={() => {
|
||||
pasteParametersAtTheEnd();
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
</ScrollView>
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
|
@@ -6,7 +6,6 @@ import ObjectGroupsListWithObjectGroupEditor from '../../ObjectGroupsList/Object
|
||||
import { Tabs } from '../../UI/Tabs';
|
||||
import { EventsFunctionParametersEditor } from './EventsFunctionParametersEditor';
|
||||
import { EventsFunctionPropertiesEditor } from './EventsFunctionPropertiesEditor';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { type GroupWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
|
||||
@@ -163,50 +162,27 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
|
||||
|
||||
return (
|
||||
<Column expand useFullHeight noOverflowParent>
|
||||
<Line>
|
||||
<Column noMargin expand noOverflowParent>
|
||||
<Tabs
|
||||
value={this.state.currentTab}
|
||||
onChange={this._chooseTab}
|
||||
options={[
|
||||
{
|
||||
value: ('config': TabNames),
|
||||
label: <Trans>Configuration</Trans>,
|
||||
},
|
||||
{
|
||||
value: ('parameters': TabNames),
|
||||
label: <Trans>Parameters</Trans>,
|
||||
},
|
||||
hasLegacyFunctionObjectGroups
|
||||
? {
|
||||
value: ('groups': TabNames),
|
||||
label: <Trans>Object groups</Trans>,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean)}
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
{this.state.currentTab === 'config' ? (
|
||||
<ScrollView>
|
||||
<Line>
|
||||
<EventsFunctionPropertiesEditor
|
||||
project={project}
|
||||
eventsFunction={eventsFunction}
|
||||
eventsBasedBehavior={eventsBasedBehavior}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
eventsFunctionsContainer={eventsFunctionsContainer}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
helpPagePath={helpPagePath}
|
||||
onConfigurationUpdated={onConfigurationUpdated}
|
||||
renderConfigurationHeader={renderConfigurationHeader}
|
||||
freezeEventsFunctionType={freezeEventsFunctionType}
|
||||
getFunctionGroupNames={getFunctionGroupNames}
|
||||
{hasLegacyFunctionObjectGroups ? (
|
||||
<Line>
|
||||
<Column noMargin expand noOverflowParent>
|
||||
<Tabs
|
||||
value={this.state.currentTab}
|
||||
onChange={this._chooseTab}
|
||||
options={[
|
||||
{
|
||||
value: ('config': TabNames),
|
||||
label: <Trans>Configuration</Trans>,
|
||||
},
|
||||
{
|
||||
value: ('groups': TabNames),
|
||||
label: <Trans>Object groups</Trans>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Line>
|
||||
</ScrollView>
|
||||
</Column>
|
||||
</Line>
|
||||
) : null}
|
||||
{this.state.currentTab === 'parameters' ? (
|
||||
{this.state.currentTab === 'config' ? (
|
||||
<EventsFunctionParametersEditor
|
||||
project={project}
|
||||
projectScopedContainersAccessor={projectScopedContainersAccessor}
|
||||
@@ -223,7 +199,21 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
|
||||
onMoveObjectEventsParameter={onMoveObjectEventsParameter}
|
||||
onFunctionParameterWillBeRenamed={onFunctionParameterWillBeRenamed}
|
||||
key={eventsFunction ? eventsFunction.ptr : null}
|
||||
/>
|
||||
>
|
||||
<EventsFunctionPropertiesEditor
|
||||
project={project}
|
||||
eventsFunction={eventsFunction}
|
||||
eventsBasedBehavior={eventsBasedBehavior}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
eventsFunctionsContainer={eventsFunctionsContainer}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
helpPagePath={helpPagePath}
|
||||
onConfigurationUpdated={onConfigurationUpdated}
|
||||
renderConfigurationHeader={renderConfigurationHeader}
|
||||
freezeEventsFunctionType={freezeEventsFunctionType}
|
||||
getFunctionGroupNames={getFunctionGroupNames}
|
||||
/>
|
||||
</EventsFunctionParametersEditor>
|
||||
) : null}
|
||||
{this.state.currentTab === 'groups' ? (
|
||||
<ObjectGroupsListWithObjectGroupEditor
|
||||
|
@@ -20,6 +20,7 @@ import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternal
|
||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
import { attachToPreview } from '../../../EmbeddedGame/EmbeddedGameFrame';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type State = {|
|
||||
@@ -158,6 +159,9 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
);
|
||||
previewExportOptions.setLayoutName(layout.getName());
|
||||
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
|
||||
previewExportOptions.setIsInGameEdition(
|
||||
previewOptions.isForInGameEdition
|
||||
);
|
||||
if (externalLayout) {
|
||||
previewExportOptions.setExternalLayoutName(externalLayout.getName());
|
||||
}
|
||||
@@ -220,6 +224,12 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
// Upload any file that must be exported for the preview.
|
||||
await browserS3FileSystem.uploadPendingObjects();
|
||||
|
||||
if (previewOptions.isForInGameEdition) {
|
||||
attachToPreview({
|
||||
previewIndexHtmlLocation: outputDir + '/index.html',
|
||||
});
|
||||
}
|
||||
|
||||
// Change the HTML file displayed by the preview window so that it starts loading
|
||||
// the game.
|
||||
previewWindows.forEach(
|
||||
|
@@ -4,7 +4,6 @@ import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import LocalFileSystem from '../LocalFileSystem';
|
||||
import optionalRequire from '../../../Utils/OptionalRequire';
|
||||
import { timeFunction } from '../../../Utils/TimeFunction';
|
||||
import { findGDJS } from '../../../GameEngineFinder/LocalGDJSFinder';
|
||||
import LocalNetworkPreviewDialog from './LocalNetworkPreviewDialog';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
} from './LocalPreviewDebuggerServer';
|
||||
import Window from '../../../Utils/Window';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
import { attachToPreview } from '../../../EmbeddedGame/EmbeddedGameFrame';
|
||||
const electron = optionalRequire('electron');
|
||||
const path = optionalRequire('path');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
@@ -47,6 +47,28 @@ type State = {|
|
||||
captureOptions: ?CaptureOptions,
|
||||
|};
|
||||
|
||||
const prepareExporter = async (): Promise<{|
|
||||
outputDir: string,
|
||||
exporter: gdjsExporter,
|
||||
gdjsRoot: string,
|
||||
|}> => {
|
||||
const { gdjsRoot } = await findGDJS();
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const localFileSystem = new LocalFileSystem({
|
||||
downloadUrlsToLocalFiles: false,
|
||||
});
|
||||
const fileSystem = assignIn(new gd.AbstractFileSystemJS(), localFileSystem);
|
||||
const outputDir = path.join(fileSystem.getTempDir(), 'preview');
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
outputDir,
|
||||
exporter,
|
||||
gdjsRoot,
|
||||
};
|
||||
};
|
||||
|
||||
export default class LocalPreviewLauncher extends React.Component<
|
||||
PreviewLauncherProps,
|
||||
State
|
||||
@@ -171,168 +193,138 @@ export default class LocalPreviewLauncher extends React.Component<
|
||||
);
|
||||
};
|
||||
|
||||
_prepareExporter = (): Promise<{|
|
||||
outputDir: string,
|
||||
exporter: gdjsExporter,
|
||||
gdjsRoot: string,
|
||||
|}> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const localFileSystem = new LocalFileSystem({
|
||||
downloadUrlsToLocalFiles: false,
|
||||
});
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const outputDir = path.join(fileSystem.getTempDir(), 'preview');
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
outputDir,
|
||||
exporter,
|
||||
gdjsRoot,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
launchPreview = (previewOptions: PreviewOptions): Promise<any> => {
|
||||
launchPreview = async (previewOptions: PreviewOptions): Promise<any> => {
|
||||
const { project, layout, externalLayout } = previewOptions;
|
||||
|
||||
// Start the debugger server for previews. Even if not used,
|
||||
// useful if the user opens the Debugger editor later, or want to
|
||||
// hot reload.
|
||||
return this.getPreviewDebuggerServer()
|
||||
.startServer()
|
||||
.catch(err => {
|
||||
// Ignore any error when running the debugger server - the preview
|
||||
// can still work without it.
|
||||
console.error(
|
||||
'Unable to start the Debugger Server for the preview:',
|
||||
err
|
||||
);
|
||||
})
|
||||
.then(() => this._prepareExporter())
|
||||
.then(({ outputDir, exporter, gdjsRoot }) => {
|
||||
timeFunction(
|
||||
() => {
|
||||
const previewExportOptions = new gd.PreviewExportOptions(
|
||||
project,
|
||||
outputDir
|
||||
);
|
||||
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
|
||||
previewExportOptions.setLayoutName(layout.getName());
|
||||
if (externalLayout) {
|
||||
previewExportOptions.setExternalLayoutName(
|
||||
externalLayout.getName()
|
||||
);
|
||||
}
|
||||
try {
|
||||
await this.getPreviewDebuggerServer().startServer();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Unable to start the Debugger Server for the preview:',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
const previewDebuggerServerAddress = getDebuggerServerAddress();
|
||||
if (previewDebuggerServerAddress) {
|
||||
previewExportOptions.useWebsocketDebuggerClientWithServerAddress(
|
||||
previewDebuggerServerAddress.address,
|
||||
'' + previewDebuggerServerAddress.port
|
||||
);
|
||||
}
|
||||
const { outputDir, exporter, gdjsRoot } = await prepareExporter();
|
||||
|
||||
const includeFileHashs = this.props.getIncludeFileHashs();
|
||||
for (const includeFile in includeFileHashs) {
|
||||
const hash = includeFileHashs[includeFile];
|
||||
previewExportOptions.setIncludeFileHash(includeFile, hash);
|
||||
}
|
||||
var previewStartTime = performance.now();
|
||||
|
||||
// Give the preview the path to the "@electron/remote" module of the editor,
|
||||
// as this is required by some features and we've not removed dependency
|
||||
// on "@electron/remote" yet.
|
||||
previewExportOptions.setElectronRemoteRequirePath(
|
||||
path.join(
|
||||
gdjsRoot,
|
||||
'../preview_node_modules',
|
||||
'@electron/remote',
|
||||
'renderer/index.js'
|
||||
)
|
||||
);
|
||||
const previewExportOptions = new gd.PreviewExportOptions(
|
||||
project,
|
||||
outputDir
|
||||
);
|
||||
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
|
||||
previewExportOptions.setLayoutName(layout.getName());
|
||||
previewExportOptions.setIsInGameEdition(previewOptions.isForInGameEdition);
|
||||
if (externalLayout) {
|
||||
previewExportOptions.setExternalLayoutName(externalLayout.getName());
|
||||
}
|
||||
|
||||
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
|
||||
const shouldHotReload =
|
||||
previewOptions.hotReload && !!debuggerIds.length;
|
||||
const previewDebuggerServerAddress = getDebuggerServerAddress();
|
||||
if (previewDebuggerServerAddress) {
|
||||
previewExportOptions.useWebsocketDebuggerClientWithServerAddress(
|
||||
previewDebuggerServerAddress.address,
|
||||
'' + previewDebuggerServerAddress.port
|
||||
);
|
||||
}
|
||||
|
||||
previewExportOptions.setProjectDataOnlyExport(
|
||||
// Only export project data if asked and if a hot-reloading is being done.
|
||||
shouldHotReload && previewOptions.projectDataOnlyExport
|
||||
);
|
||||
const includeFileHashs = this.props.getIncludeFileHashs();
|
||||
for (const includeFile in includeFileHashs) {
|
||||
const hash = includeFileHashs[includeFile];
|
||||
previewExportOptions.setIncludeFileHash(includeFile, hash);
|
||||
}
|
||||
|
||||
previewExportOptions.setFullLoadingScreen(
|
||||
previewOptions.fullLoadingScreen
|
||||
);
|
||||
previewExportOptions.setGDevelopVersionWithHash(
|
||||
getIDEVersionWithHash()
|
||||
);
|
||||
previewExportOptions.setCrashReportUploadLevel(
|
||||
this.props.crashReportUploadLevel
|
||||
);
|
||||
previewExportOptions.setPreviewContext(this.props.previewContext);
|
||||
previewExportOptions.setProjectTemplateSlug(
|
||||
project.getTemplateSlug()
|
||||
);
|
||||
previewExportOptions.setSourceGameId(this.props.sourceGameId);
|
||||
// Give the preview the path to the "@electron/remote" module of the editor,
|
||||
// as this is required by some features and we've not removed dependency
|
||||
// on "@electron/remote" yet.
|
||||
previewExportOptions.setElectronRemoteRequirePath(
|
||||
path.join(
|
||||
gdjsRoot,
|
||||
'../preview_node_modules',
|
||||
'@electron/remote',
|
||||
'renderer/index.js'
|
||||
)
|
||||
);
|
||||
|
||||
if (previewOptions.fallbackAuthor) {
|
||||
previewExportOptions.setFallbackAuthor(
|
||||
previewOptions.fallbackAuthor.id,
|
||||
previewOptions.fallbackAuthor.username
|
||||
);
|
||||
}
|
||||
if (previewOptions.authenticatedPlayer) {
|
||||
previewExportOptions.setAuthenticatedPlayer(
|
||||
previewOptions.authenticatedPlayer.playerId,
|
||||
previewOptions.authenticatedPlayer.playerUsername,
|
||||
previewOptions.authenticatedPlayer.playerToken
|
||||
);
|
||||
}
|
||||
if (previewOptions.captureOptions) {
|
||||
if (previewOptions.captureOptions.screenshots) {
|
||||
previewOptions.captureOptions.screenshots.forEach(
|
||||
screenshot => {
|
||||
previewExportOptions.addScreenshotCapture(
|
||||
screenshot.delayTimeInSeconds,
|
||||
screenshot.signedUrl,
|
||||
screenshot.publicUrl
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
|
||||
const shouldHotReload = previewOptions.hotReload && !!debuggerIds.length;
|
||||
|
||||
exporter.exportProjectForPixiPreview(previewExportOptions);
|
||||
previewExportOptions.delete();
|
||||
exporter.delete();
|
||||
previewExportOptions.setProjectDataOnlyExport(
|
||||
// Only export project data if asked and if a hot-reloading is being done.
|
||||
shouldHotReload && previewOptions.projectDataOnlyExport
|
||||
);
|
||||
|
||||
if (shouldHotReload) {
|
||||
debuggerIds.forEach(debuggerId => {
|
||||
this.getPreviewDebuggerServer().sendMessage(debuggerId, {
|
||||
command: 'hotReload',
|
||||
});
|
||||
});
|
||||
previewExportOptions.setFullLoadingScreen(previewOptions.fullLoadingScreen);
|
||||
previewExportOptions.setGDevelopVersionWithHash(getIDEVersionWithHash());
|
||||
previewExportOptions.setCrashReportUploadLevel(
|
||||
this.props.crashReportUploadLevel
|
||||
);
|
||||
previewExportOptions.setPreviewContext(this.props.previewContext);
|
||||
previewExportOptions.setProjectTemplateSlug(project.getTemplateSlug());
|
||||
previewExportOptions.setSourceGameId(this.props.sourceGameId);
|
||||
|
||||
if (
|
||||
this.state.hotReloadsCount % 16 === 0 &&
|
||||
this._hotReloadSubscriptionChecker
|
||||
) {
|
||||
this._hotReloadSubscriptionChecker.checkUserHasSubscription();
|
||||
}
|
||||
this.setState(state => ({
|
||||
hotReloadsCount: state.hotReloadsCount + 1,
|
||||
}));
|
||||
} else {
|
||||
this._openPreviewWindow(project, outputDir, previewOptions);
|
||||
}
|
||||
},
|
||||
time => console.info(`Preview took ${time}ms`)
|
||||
);
|
||||
if (previewOptions.fallbackAuthor) {
|
||||
previewExportOptions.setFallbackAuthor(
|
||||
previewOptions.fallbackAuthor.id,
|
||||
previewOptions.fallbackAuthor.username
|
||||
);
|
||||
}
|
||||
if (previewOptions.authenticatedPlayer) {
|
||||
previewExportOptions.setAuthenticatedPlayer(
|
||||
previewOptions.authenticatedPlayer.playerId,
|
||||
previewOptions.authenticatedPlayer.playerUsername,
|
||||
previewOptions.authenticatedPlayer.playerToken
|
||||
);
|
||||
}
|
||||
if (previewOptions.captureOptions) {
|
||||
if (previewOptions.captureOptions.screenshots) {
|
||||
previewOptions.captureOptions.screenshots.forEach(screenshot => {
|
||||
previewExportOptions.addScreenshotCapture(
|
||||
screenshot.delayTimeInSeconds,
|
||||
screenshot.signedUrl,
|
||||
screenshot.publicUrl
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exporter.exportProjectForPixiPreview(previewExportOptions);
|
||||
previewExportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
if (shouldHotReload) {
|
||||
debuggerIds.forEach(debuggerId => {
|
||||
this.getPreviewDebuggerServer().sendMessage(debuggerId, {
|
||||
command: 'hotReload',
|
||||
});
|
||||
});
|
||||
|
||||
if (
|
||||
this.state.hotReloadsCount % 16 === 0 &&
|
||||
this._hotReloadSubscriptionChecker
|
||||
) {
|
||||
this._hotReloadSubscriptionChecker.checkUserHasSubscription();
|
||||
}
|
||||
this.setState(state => ({
|
||||
hotReloadsCount: state.hotReloadsCount + 1,
|
||||
}));
|
||||
} else {
|
||||
if (previewOptions.isForInGameEdition) {
|
||||
attachToPreview({
|
||||
previewIndexHtmlLocation: `file://${outputDir}/index.html`,
|
||||
});
|
||||
}
|
||||
|
||||
if (previewOptions.numberOfWindows >= 1) {
|
||||
this._openPreviewWindow(project, outputDir, previewOptions);
|
||||
}
|
||||
}
|
||||
|
||||
const previewStopTime = performance.now();
|
||||
console.info(`Preview took ${previewStopTime - previewStartTime}ms`);
|
||||
};
|
||||
|
||||
getPreviewDebuggerServer() {
|
||||
|
@@ -15,6 +15,10 @@ export type LaunchPreviewOptions = {
|
||||
fullLoadingScreen?: boolean,
|
||||
forceDiagnosticReport?: boolean,
|
||||
numberOfWindows?: number,
|
||||
isForInGameEdition?: {|
|
||||
forcedSceneName: string,
|
||||
forcedExternalLayoutName: ?string,
|
||||
|},
|
||||
launchCaptureOptions?: LaunchCaptureOptions,
|
||||
};
|
||||
export type CaptureOptions = {|
|
||||
@@ -40,6 +44,7 @@ export type PreviewOptions = {|
|
||||
playerToken: string,
|
||||
},
|
||||
numberOfWindows: number,
|
||||
isForInGameEdition: boolean,
|
||||
getIsMenuBarHiddenInPreview: () => boolean,
|
||||
getIsAlwaysOnTopInPreview: () => boolean,
|
||||
captureOptions: CaptureOptions,
|
||||
@@ -59,6 +64,13 @@ export type PreviewLauncherProps = {|
|
||||
/** Each game connected to the debugger server is identified by a unique number. */
|
||||
export type DebuggerId = number;
|
||||
|
||||
/** Each game connected to the debugger server can communicate its status. */
|
||||
export type DebuggerStatus = {|
|
||||
isPaused: boolean,
|
||||
isInGameEdition: boolean,
|
||||
sceneName: string | null,
|
||||
|};
|
||||
|
||||
/** The callbacks for a debugger server used for previews. */
|
||||
export type PreviewDebuggerServerCallbacks = {|
|
||||
onErrorReceived: (err: Error) => void | Promise<void>,
|
||||
|
@@ -38,6 +38,8 @@ import { type MenuItemTemplate } from '../../UI/Menu/Menu.flow';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import CircularProgress from '../../UI/CircularProgress';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight';
|
||||
import ChevronArrowLeft from '../../UI/CustomSvgIcons/ChevronArrowLeft';
|
||||
|
||||
const styles = {
|
||||
// Make select field width not dependent on build names (name is truncated).
|
||||
@@ -67,27 +69,31 @@ const pushOrCreateKey = (
|
||||
}
|
||||
return object;
|
||||
};
|
||||
const pageSize = 20;
|
||||
|
||||
const groupFeedbacks = (
|
||||
i18n: I18nType,
|
||||
feedbacks: Array<Comment>,
|
||||
currentPage: number,
|
||||
{ build, date }: {| build: boolean, date: boolean |}
|
||||
): { [buildIdOrDate: string]: Array<Comment> } => {
|
||||
const feedbacksByBuild = feedbacks.reduce((acc, feedback) => {
|
||||
if (build) {
|
||||
if (!feedback.buildId) {
|
||||
return pushOrCreateKey('game-only', feedback, acc);
|
||||
const feedbacksByBuild = feedbacks
|
||||
.slice(pageSize * (currentPage - 1), pageSize * currentPage)
|
||||
.reduce((acc, feedback) => {
|
||||
if (build) {
|
||||
if (!feedback.buildId) {
|
||||
return pushOrCreateKey('game-only', feedback, acc);
|
||||
} else {
|
||||
return pushOrCreateKey(feedback.buildId, feedback, acc);
|
||||
}
|
||||
} else {
|
||||
return pushOrCreateKey(feedback.buildId, feedback, acc);
|
||||
const dateKey = i18n.date(feedback.createdAt, {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
return pushOrCreateKey(dateKey, feedback, acc);
|
||||
}
|
||||
} else {
|
||||
const dateKey = i18n.date(feedback.createdAt, {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
return pushOrCreateKey(dateKey, feedback, acc);
|
||||
}
|
||||
}, {});
|
||||
}, {});
|
||||
return feedbacksByBuild;
|
||||
};
|
||||
|
||||
@@ -96,8 +102,9 @@ const getDisplayedFeedbacks = (
|
||||
feedbacks: ?Array<Comment>,
|
||||
showUnprocessed: boolean,
|
||||
sortByDate: boolean,
|
||||
filter: string
|
||||
): ?{ [buildIdOrDate: string]: Array<Comment> } => {
|
||||
filter: string,
|
||||
currentPage: number
|
||||
): ?Array<Comment> => {
|
||||
if (!feedbacks) return null;
|
||||
let filteredFeedbacksByBuild = feedbacks;
|
||||
if (filter === 'game-only') {
|
||||
@@ -112,15 +119,7 @@ const getDisplayedFeedbacks = (
|
||||
? filterUnprocessedComments(filteredFeedbacksByBuild)
|
||||
: filteredFeedbacksByBuild;
|
||||
|
||||
return sortByDate
|
||||
? groupFeedbacks(i18n, filteredFeedbacksByBuildAndUnprocessed, {
|
||||
build: false,
|
||||
date: true,
|
||||
})
|
||||
: groupFeedbacks(i18n, filteredFeedbacksByBuildAndUnprocessed, {
|
||||
build: true,
|
||||
date: false,
|
||||
});
|
||||
return filteredFeedbacksByBuildAndUnprocessed;
|
||||
};
|
||||
|
||||
const GameFeedback = ({ i18n, authenticatedUser, game }: Props) => {
|
||||
@@ -137,21 +136,57 @@ const GameFeedback = ({ i18n, authenticatedUser, game }: Props) => {
|
||||
const [isMarkingAllAsProcessed, setIsMarkingAllAsProcessed] = React.useState(
|
||||
false
|
||||
);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
||||
|
||||
const displayedFeedbacks = getDisplayedFeedbacks(
|
||||
i18n,
|
||||
feedbacks,
|
||||
showProcessed,
|
||||
sortByDate,
|
||||
filter
|
||||
filter,
|
||||
currentPage
|
||||
);
|
||||
|
||||
const displayedFeedbacksArray: Comment[] = displayedFeedbacks
|
||||
? // $FlowFixMe - Flow doesn't understand that we're flattening the array.
|
||||
Object.values(displayedFeedbacks)
|
||||
.filter(Boolean)
|
||||
.flat()
|
||||
: [];
|
||||
const isShowingCommentsForASpecificBuild =
|
||||
filter !== '' && filter !== 'game-only';
|
||||
|
||||
const displayedFeedbacksByDateOrBuild: ?{
|
||||
[buildIdOrDate: string]: Array<Comment>,
|
||||
} = !displayedFeedbacks
|
||||
? null
|
||||
: groupFeedbacks(
|
||||
i18n,
|
||||
displayedFeedbacks,
|
||||
currentPage,
|
||||
sortByDate
|
||||
? {
|
||||
build: false,
|
||||
date: true,
|
||||
}
|
||||
: {
|
||||
build: true,
|
||||
date: false,
|
||||
}
|
||||
);
|
||||
|
||||
const totalNumberOfPages = Math.ceil(
|
||||
!displayedFeedbacks ? 1 : displayedFeedbacks.length / pageSize
|
||||
);
|
||||
|
||||
const onCurrentPageChange = React.useCallback(
|
||||
newPage => {
|
||||
const minPage = 1;
|
||||
const maxPage = totalNumberOfPages;
|
||||
if (newPage < minPage) {
|
||||
setCurrentPage(minPage);
|
||||
} else if (newPage > maxPage) {
|
||||
setCurrentPage(maxPage);
|
||||
} else {
|
||||
setCurrentPage(newPage);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, totalNumberOfPages]
|
||||
);
|
||||
|
||||
const getBuildNameOption = (buildId: string) => {
|
||||
const shortenedUuid = shortenUuidForDisplay(buildId);
|
||||
@@ -252,11 +287,11 @@ const GameFeedback = ({ i18n, authenticatedUser, game }: Props) => {
|
||||
};
|
||||
|
||||
const markAllCommentsAsProcessed = async (i18n: I18nType) => {
|
||||
if (!profile || isMarkingAllAsProcessed) return;
|
||||
if (!profile || isMarkingAllAsProcessed || !displayedFeedbacks) return;
|
||||
try {
|
||||
setIsMarkingAllAsProcessed(true);
|
||||
await Promise.all(
|
||||
displayedFeedbacksArray
|
||||
displayedFeedbacks
|
||||
.filter(comment => !comment.processedAt)
|
||||
.map(comment =>
|
||||
updateComment(getAuthorizationHeader, profile.id, {
|
||||
@@ -303,6 +338,13 @@ const GameFeedback = ({ i18n, authenticatedUser, game }: Props) => {
|
||||
},
|
||||
];
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
setCurrentPage(1);
|
||||
},
|
||||
[sortByDate, showProcessed, filter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column noMargin expand>
|
||||
@@ -324,7 +366,10 @@ const GameFeedback = ({ i18n, authenticatedUser, game }: Props) => {
|
||||
)}
|
||||
{authenticatedUser.authenticated && displayedFeedbacks && (
|
||||
<Column expand noMargin>
|
||||
<ResponsiveLineStackLayout justifyContent="space-between">
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
noColumnMargin
|
||||
>
|
||||
<Column justifyContent="center">
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<BackgroundText>
|
||||
@@ -350,7 +395,7 @@ const GameFeedback = ({ i18n, authenticatedUser, game }: Props) => {
|
||||
<SelectOption
|
||||
key={'all'}
|
||||
value={''}
|
||||
label={t`All builds`}
|
||||
label={t`All exports`}
|
||||
/>
|
||||
<SelectOption
|
||||
key={'game-only'}
|
||||
@@ -374,89 +419,154 @@ const GameFeedback = ({ i18n, authenticatedUser, game }: Props) => {
|
||||
</ResponsiveLineStackLayout>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{!!feedbacks && feedbacks.length > 0 && (
|
||||
// TODO: Should it display the data for the filtered pieces of feedback only?
|
||||
<FeedbackAverageCard feedbacks={feedbacks} />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
{displayedFeedbacksArray.length === 0 && (
|
||||
{displayedFeedbacks.length === 0 && (
|
||||
<>
|
||||
{showProcessed ? (
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Text>
|
||||
<Trans>
|
||||
You don't have any unread feedback for this game.
|
||||
</Trans>
|
||||
</Text>
|
||||
<EmptyMessage>
|
||||
{isShowingCommentsForASpecificBuild ? (
|
||||
<Trans>
|
||||
You don't have any unread feedback for this export.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
You don't have any unread feedback for this game.
|
||||
</Trans>
|
||||
)}
|
||||
</EmptyMessage>
|
||||
<FlatButton
|
||||
onClick={() => setShowProcessed(false)}
|
||||
label={<Trans>Show all feedbacks</Trans>}
|
||||
/>
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<Text>
|
||||
<Trans>You don't have any feedback for this game.</Trans>
|
||||
</Text>
|
||||
<EmptyMessage>
|
||||
{isShowingCommentsForASpecificBuild ? (
|
||||
<Trans>
|
||||
You don't have any feedback for this export.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
You don't have any feedback for this game.
|
||||
</Trans>
|
||||
)}
|
||||
</EmptyMessage>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{displayedFeedbacksArray.length !== 0 && (
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{Object.keys(displayedFeedbacks).map((key, index) => {
|
||||
const title = sortByDate ? key : getBuildNameTitle(key);
|
||||
return (
|
||||
<ColumnStackLayout key={key} noMargin>
|
||||
<Line
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Column>
|
||||
<Text size="block-title">
|
||||
{title}
|
||||
{title ? ' - ' : ' '}
|
||||
{displayedFeedbacks[key].length === 1 ? (
|
||||
<Trans>1 review</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{displayedFeedbacks[key].length} reviews
|
||||
</Trans>
|
||||
{displayedFeedbacks.length !== 0 &&
|
||||
!!displayedFeedbacksByDateOrBuild && (
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{Object.keys(displayedFeedbacksByDateOrBuild).map(
|
||||
(key, index) => {
|
||||
const title = sortByDate ? key : getBuildNameTitle(key);
|
||||
return (
|
||||
<ColumnStackLayout key={key} noMargin>
|
||||
<Line
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Column>
|
||||
<Text size="block-title">
|
||||
{title}
|
||||
{title ? ' - ' : ' '}
|
||||
{displayedFeedbacksByDateOrBuild[key]
|
||||
.length === 1 ? (
|
||||
<Trans>1 review</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{
|
||||
displayedFeedbacksByDateOrBuild[key]
|
||||
.length
|
||||
}{' '}
|
||||
reviews
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Column>
|
||||
{index === 0 && (
|
||||
<Line alignItems="center">
|
||||
<Line
|
||||
noMargin
|
||||
expand
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<IconButton
|
||||
tooltip={t`Previous page`}
|
||||
onClick={() =>
|
||||
onCurrentPageChange(currentPage - 1)
|
||||
}
|
||||
disabled={currentPage === 1}
|
||||
size="small"
|
||||
>
|
||||
<ChevronArrowLeft />
|
||||
</IconButton>
|
||||
<Text
|
||||
noMargin
|
||||
style={{
|
||||
opacity: 1,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{totalNumberOfPages === 1
|
||||
? 1
|
||||
: `${currentPage}/${totalNumberOfPages}`}
|
||||
</Text>
|
||||
<IconButton
|
||||
tooltip={t`Next page`}
|
||||
onClick={() =>
|
||||
onCurrentPageChange(currentPage + 1)
|
||||
}
|
||||
disabled={
|
||||
currentPage >= totalNumberOfPages
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
<ChevronArrowRight />
|
||||
</IconButton>
|
||||
</Line>
|
||||
<IconButton
|
||||
disabled={isMarkingAllAsProcessed}
|
||||
onClick={event =>
|
||||
openOptionsContextMenu(event)
|
||||
}
|
||||
>
|
||||
{!isMarkingAllAsProcessed ? (
|
||||
<Options fontSize="small" />
|
||||
) : (
|
||||
<CircularProgress size={20} />
|
||||
)}
|
||||
</IconButton>
|
||||
</Line>
|
||||
)}
|
||||
</Text>
|
||||
</Column>
|
||||
{index === 0 && (
|
||||
<Column justifyContent="center">
|
||||
<IconButton
|
||||
disabled={isMarkingAllAsProcessed}
|
||||
onClick={event => openOptionsContextMenu(event)}
|
||||
>
|
||||
{!isMarkingAllAsProcessed ? (
|
||||
<Options fontSize="small" />
|
||||
) : (
|
||||
<CircularProgress size={20} />
|
||||
)}
|
||||
</IconButton>
|
||||
</Column>
|
||||
)}
|
||||
</Line>
|
||||
{displayedFeedbacks[key].map(
|
||||
(comment: Comment, index: number) => (
|
||||
<FeedbackCard
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
buildProperties={getBuildPropertiesForComment(
|
||||
comment
|
||||
)}
|
||||
authenticatedUser={authenticatedUser}
|
||||
onCommentUpdated={onCommentUpdated}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
);
|
||||
})}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</Line>
|
||||
{displayedFeedbacksByDateOrBuild[key].map(
|
||||
(comment: Comment, index: number) => (
|
||||
<FeedbackCard
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
buildProperties={getBuildPropertiesForComment(
|
||||
comment
|
||||
)}
|
||||
authenticatedUser={authenticatedUser}
|
||||
onCommentUpdated={onCommentUpdated}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
</Line>
|
||||
|
@@ -1,8 +1,9 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import * as React from 'react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
LineStackLayout,
|
||||
@@ -50,13 +51,37 @@ import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
import FileWithLines from '../UI/CustomSvgIcons/FileWithLines';
|
||||
import TextButton from '../UI/TextButton';
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
import { getRelativeOrAbsoluteDisplayDate } from '../Utils/DateDisplay';
|
||||
const electron = optionalRequire('electron');
|
||||
const path = optionalRequire('path');
|
||||
|
||||
export const getThumbnailWidth = ({ isMobile }: {| isMobile: boolean |}) =>
|
||||
isMobile ? undefined : Math.min(245, Math.max(130, window.innerWidth / 4));
|
||||
|
||||
export const getProjectDisplayDate = (i18n: I18nType, date: number) =>
|
||||
getRelativeOrAbsoluteDisplayDate({
|
||||
i18n,
|
||||
dateAsNumber: date,
|
||||
sameDayFormat: 'todayAndHour',
|
||||
dayBeforeFormat: 'yesterdayAndHour',
|
||||
relativeLimit: 'currentWeek',
|
||||
sameWeekFormat: 'thisWeek',
|
||||
});
|
||||
export const getDetailedProjectDisplayDate = (i18n: I18nType, date: number) =>
|
||||
i18n.date(date, {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
|
||||
const getNoProjectAlertMessage = () => {
|
||||
if (!electron) {
|
||||
// Trying to open a local project from the web app of the mobile app.
|
||||
return t`Looks like your project isn't there!${'\n\n'}Your project must be stored on your computer.`;
|
||||
} else {
|
||||
return t`We couldn't find your project.${'\n\n'}If your project is stored on a different computer, launch GDevelop on that computer.${'\n'}Otherwise, use the "Open project" button and find it in your filesystem.`;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = {
|
||||
tooltipButtonContainer: {
|
||||
display: 'flex',
|
||||
@@ -67,7 +92,7 @@ const styles = {
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconAndText: { display: 'flex', gap: 2, alignItems: 'flex-start' },
|
||||
title: {
|
||||
@@ -281,18 +306,10 @@ const GameDashboardCard = ({
|
||||
<Text size="block-title" noMargin style={styles.title}>
|
||||
{gameName || <Trans>Unknown game</Trans>}
|
||||
</Text>
|
||||
{projectsList.length > 0 && game && (
|
||||
{projectsList.length >= 2 && game && (
|
||||
<>
|
||||
<Spacer />
|
||||
<Tooltip
|
||||
title={
|
||||
projectsList.length === 1 ? (
|
||||
<Trans>{projectsList.length} project</Trans>
|
||||
) : (
|
||||
<Trans>{projectsList.length} projects</Trans>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Tooltip title={<Trans>{projectsList.length} projects</Trans>}>
|
||||
{/* Button must be wrapped in a container so that the parent tooltip
|
||||
can display even if the button is disabled. */}
|
||||
<div style={styles.tooltipButtonContainer}>
|
||||
@@ -325,16 +342,29 @@ const GameDashboardCard = ({
|
||||
authenticatedUser={authenticatedUser}
|
||||
currentFileMetadata={currentFileMetadata}
|
||||
textColor="secondary"
|
||||
textSize="body-small"
|
||||
textPrefix={isCurrentProjectOpened ? null : <Trans>Last edited:</Trans>}
|
||||
/>
|
||||
) : game ? (
|
||||
<LineStackLayout noMargin expand>
|
||||
<Text color="secondary" noMargin size="body-small">
|
||||
<Trans>Last edited:</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin size="body-small">
|
||||
{i18n.date((game.updatedAt || 0) * 1000)}
|
||||
{!itemStorageProvider && isCurrentProjectOpened ? (
|
||||
<Trans>Draft created:</Trans>
|
||||
) : (
|
||||
<Trans>Last edited:</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={getDetailedProjectDisplayDate(
|
||||
i18n,
|
||||
(game.updatedAt || 0) * 1000
|
||||
)}
|
||||
>
|
||||
<Text color="secondary" noMargin size="body-small">
|
||||
{getProjectDisplayDate(i18n, (game.updatedAt || 0) * 1000)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</LineStackLayout>
|
||||
) : null;
|
||||
|
||||
@@ -387,18 +417,16 @@ const GameDashboardCard = ({
|
||||
const actions = [];
|
||||
if (projectsList.length > 1) {
|
||||
actions.push(
|
||||
...[
|
||||
...projectsList.map(fileMetadataAndStorageProviderName => {
|
||||
return {
|
||||
label: getProjectItemLabel(
|
||||
fileMetadataAndStorageProviderName,
|
||||
storageProviders,
|
||||
i18n
|
||||
),
|
||||
click: () => onOpenProject(fileMetadataAndStorageProviderName),
|
||||
};
|
||||
}),
|
||||
]
|
||||
...projectsList.slice(0, 3).map(fileMetadataAndStorageProviderName => {
|
||||
return {
|
||||
label: getProjectItemLabel(
|
||||
fileMetadataAndStorageProviderName,
|
||||
storageProviders,
|
||||
i18n
|
||||
),
|
||||
click: () => onOpenProject(fileMetadataAndStorageProviderName),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (game) {
|
||||
@@ -457,7 +485,10 @@ const GameDashboardCard = ({
|
||||
// If there are multiple projects, suggest opening the game dashboard.
|
||||
actions.push({
|
||||
label: i18n._(t`See all projects`),
|
||||
click: game ? () => onOpenGameManager({ game }) : undefined,
|
||||
click: game
|
||||
? () =>
|
||||
onOpenGameManager({ game, widgetToScrollTo: 'projects' })
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -571,7 +602,7 @@ const GameDashboardCard = ({
|
||||
: () => {
|
||||
showAlert({
|
||||
title: t`No project to open`,
|
||||
message: t`Looks like your project isn't there!${'\n\n'}You may be using a different computer or opening GDevelop on the web and your project is saved locally.`,
|
||||
message: getNoProjectAlertMessage(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { getGameMainImageUrl, type Game } from '../Utils/GDevelopServices/Game';
|
||||
import { GameThumbnail } from './GameThumbnail';
|
||||
import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
@@ -23,9 +24,10 @@ import RaisedButton from '../UI/RaisedButton';
|
||||
import Edit from '../UI/CustomSvgIcons/Edit';
|
||||
import GameLinkAndShareIcons from './GameLinkAndShareIcons';
|
||||
import { CompactToggleField } from '../UI/CompactToggleField';
|
||||
import { FixedHeightFlexContainer } from '../UI/Grid';
|
||||
import { FixedHeightFlexContainer, Line } from '../UI/Grid';
|
||||
import useOnResize from '../Utils/UseOnResize';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import { getDetailedProjectDisplayDate } from './GameDashboardCard';
|
||||
|
||||
const styles = {
|
||||
iconAndText: { display: 'flex', gap: 2, alignItems: 'flex-start' },
|
||||
@@ -106,9 +108,21 @@ const GameHeader = ({
|
||||
};
|
||||
const renderTitle = (i18n: I18nType) => (
|
||||
<ColumnStackLayout noMargin>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>Created on {i18n.date(game.createdAt * 1000)}</Trans>
|
||||
</Text>
|
||||
<Line noMargin>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={getDetailedProjectDisplayDate(i18n, game.createdAt * 1000)}
|
||||
>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>
|
||||
Created on
|
||||
{i18n.date(game.createdAt * 1000, {
|
||||
dateStyle: 'short',
|
||||
})}
|
||||
</Trans>
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Line>
|
||||
<Text size="block-title" noMargin>
|
||||
{game.gameName}
|
||||
</Text>
|
||||
|
@@ -232,14 +232,14 @@ export const GameRegistration = ({
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>
|
||||
Get ready-made packs to make your game visible to the GDevelop
|
||||
community.
|
||||
Make your game visible to the GDevelop community and to the
|
||||
world with Marketing Boosts.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Line>
|
||||
<RightLoader isLoading={!game}>
|
||||
<RaisedButton
|
||||
label={<Trans>See marketing packs</Trans>}
|
||||
label={<Trans>See Marketing Boosts</Trans>}
|
||||
primary
|
||||
onClick={() => setMarketingPlansDialogOpen(true)}
|
||||
disabled={!game}
|
||||
|
@@ -395,17 +395,20 @@ const GamesList = ({
|
||||
searchText ? 250 : 150
|
||||
);
|
||||
|
||||
// Refresh games to display, depending on a few parameters.
|
||||
React.useEffect(getDashboardItemsToDisplayDebounced, [
|
||||
React.useEffect(
|
||||
getDashboardItemsToDisplayDebounced,
|
||||
searchText, // search text changes (user input)
|
||||
games, // games change (when updating a game for instance)
|
||||
currentPage, // user changes page
|
||||
orderBy, // user changes order
|
||||
currentFileMetadata, // opened project changes (when opening or closing a project from here)
|
||||
allRecentProjectFiles.length, // list of recent projects changes (when a project is removed from list)
|
||||
project, // opened project changes (when closing a project from here)
|
||||
]);
|
||||
// Refresh games to display, depending on a few parameters.
|
||||
[
|
||||
getDashboardItemsToDisplayDebounced,
|
||||
searchText, // search text changes (user input)
|
||||
games, // games change (when updating a game for instance)
|
||||
currentPage, // user changes page
|
||||
orderBy, // user changes order
|
||||
currentFileMetadata, // opened project changes (when opening or closing a project from here)
|
||||
allRecentProjectFiles.length, // list of recent projects changes (when a project is removed from list)
|
||||
project, // opened project changes (when closing a project from here)
|
||||
]
|
||||
);
|
||||
|
||||
const projectUuid = project ? project.getProjectUuid() : null;
|
||||
|
||||
@@ -428,10 +431,11 @@ const GamesList = ({
|
||||
lastModifiedInfoByProjectId,
|
||||
setLastModifiedInfoByProjectId,
|
||||
] = React.useState({});
|
||||
// Look at projects where lastCommittedBy is not the current user (cloud projects only), and fetch
|
||||
// public profiles of the users that have modified them.
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
// Look at projects where lastCommittedBy is not the current user (cloud projects only), and fetch
|
||||
// public profiles of the users that have modified them.
|
||||
const updateModificationInfoByProjectId = async () => {
|
||||
if (!cloudProjects || !profile) return;
|
||||
|
||||
@@ -449,6 +453,15 @@ const GamesList = ({
|
||||
[cloudProjects, profile]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
// Reset pagination when modifying the sorting order.
|
||||
setCurrentPage(1);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[orderBy]
|
||||
);
|
||||
|
||||
const shouldShowOpenProject =
|
||||
canOpen &&
|
||||
// Only show on large screens.
|
||||
@@ -616,8 +629,11 @@ const GamesList = ({
|
||||
const isCurrentProjectOpened =
|
||||
(!!projectUuid && (!!game && game.id === projectUuid)) ||
|
||||
(!!projectFileMetadataAndStorageProviderName &&
|
||||
!!currentFileMetadata &&
|
||||
projectFileMetadataAndStorageProviderName.fileMetadata
|
||||
.gameId === projectUuid);
|
||||
.gameId === projectUuid &&
|
||||
projectFileMetadataAndStorageProviderName.fileMetadata
|
||||
.fileIdentifier === currentFileMetadata.fileIdentifier);
|
||||
|
||||
return (
|
||||
<GameDashboardCard
|
||||
|
@@ -23,6 +23,7 @@ const useGamesList = (): GamesList => {
|
||||
authenticated,
|
||||
firebaseUser,
|
||||
getAuthorizationHeader,
|
||||
loginState,
|
||||
} = authenticatedUser;
|
||||
|
||||
const [games, setGames] = React.useState<?Array<Game>>(null);
|
||||
@@ -32,6 +33,8 @@ const useGamesList = (): GamesList => {
|
||||
|
||||
const fetchGames = React.useCallback(
|
||||
async (): Promise<void> => {
|
||||
if (loginState !== 'done') return;
|
||||
|
||||
if (!authenticated || !firebaseUser) {
|
||||
setGames([]);
|
||||
return;
|
||||
@@ -53,7 +56,7 @@ const useGamesList = (): GamesList => {
|
||||
gamesFetchingPromise.current = null;
|
||||
}
|
||||
},
|
||||
[authenticated, firebaseUser, getAuthorizationHeader]
|
||||
[authenticated, firebaseUser, getAuthorizationHeader, loginState]
|
||||
);
|
||||
|
||||
const onGameUpdated = React.useCallback(
|
||||
|
@@ -5,11 +5,15 @@ import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
const electron = optionalRequire('electron');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
|
||||
type Props = {
|
||||
onGDJSUpdated: () => Promise<void> | void,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up some watchers for GDJS and Extensions sources.
|
||||
* Stop the watchers when the component is unmounted or `shouldWatch` prop is false.
|
||||
*/
|
||||
export const LocalGDJSDevelopmentWatcher = () => {
|
||||
export const LocalGDJSDevelopmentWatcher = ({ onGDJSUpdated }: Props) => {
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const shouldWatch = preferences.values.useGDJSDevelopmentWatcher;
|
||||
|
||||
@@ -35,5 +39,35 @@ export const LocalGDJSDevelopmentWatcher = () => {
|
||||
[shouldWatch]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!shouldWatch) {
|
||||
// Nothing to set up in the effect if watch is deactivated.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ipcRenderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.removeAllListeners(
|
||||
'local-gdjs-development-watcher-runtime-updated'
|
||||
);
|
||||
ipcRenderer.on(
|
||||
'local-gdjs-development-watcher-runtime-updated',
|
||||
(event, err) => {
|
||||
onGDJSUpdated();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeAllListeners(
|
||||
'local-gdjs-development-watcher-runtime-updated'
|
||||
);
|
||||
};
|
||||
},
|
||||
[shouldWatch, onGDJSUpdated]
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@@ -53,6 +53,12 @@ const jsExtensions = [
|
||||
extensionModule: require('GDJS-for-web-app-only/Runtime/Extensions/Physics2Behavior/JsExtension.js'),
|
||||
objectsRenderingServiceModules: {},
|
||||
},
|
||||
{
|
||||
name: 'Physics3D',
|
||||
// $FlowExpectedError - this path is ignored for Flow.
|
||||
extensionModule: require('GDJS-for-web-app-only/Runtime/Extensions/Physics3DBehavior/JsExtension.js'),
|
||||
objectsRenderingServiceModules: {},
|
||||
},
|
||||
{
|
||||
name: 'ExampleJsExtension',
|
||||
// $FlowExpectedError - this path is ignored for Flow.
|
||||
|
@@ -97,7 +97,7 @@ export const create = (authentication: Authentication) => {
|
||||
)}
|
||||
quickPublishOnlineWebExporter={localOnlineWebExporter}
|
||||
renderGDJSDevelopmentWatcher={
|
||||
isDev ? () => <LocalGDJSDevelopmentWatcher /> : null
|
||||
isDev ? ({ onGDJSUpdated }) => <LocalGDJSDevelopmentWatcher onGDJSUpdated={onGDJSUpdated} /> : null
|
||||
}
|
||||
storageProviders={storageProviders}
|
||||
resourceMover={LocalResourceMover}
|
||||
|
@@ -27,6 +27,9 @@ import {
|
||||
} from '../ResourcesWatcher';
|
||||
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
import { switchToSceneEdition } from '../../EmbeddedGame/EmbeddedGameFrame';
|
||||
|
||||
const gameEditorMode = 'embedded-game'; // TODO: move to a preference.
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
@@ -69,6 +72,13 @@ export class ExternalLayoutEditorContainer extends React.Component<
|
||||
layout ? layout.getName() : null,
|
||||
projectItemName
|
||||
);
|
||||
|
||||
if (gameEditorMode === 'embedded-game' && layout && projectItemName) {
|
||||
switchToSceneEdition({
|
||||
sceneName: layout.getName(),
|
||||
externalLayoutName: projectItemName,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.resourceExternallyChangedCallbackId = registerOnResourceExternallyChangedCallback(
|
||||
this.onResourceExternallyChanged.bind(this)
|
||||
@@ -88,6 +98,13 @@ export class ExternalLayoutEditorContainer extends React.Component<
|
||||
layout ? layout.getName() : null,
|
||||
projectItemName
|
||||
);
|
||||
|
||||
if (gameEditorMode === 'embedded-game' && layout && projectItemName) {
|
||||
switchToSceneEdition({
|
||||
sceneName: layout.getName(),
|
||||
externalLayoutName: projectItemName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Text from '../../../../UI/Text';
|
||||
import { LineStackLayout } from '../../../../UI/Layout';
|
||||
import {
|
||||
@@ -9,11 +10,14 @@ import {
|
||||
type StorageProvider,
|
||||
} from '../../../../ProjectsStorage';
|
||||
import { type AuthenticatedUser } from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { getRelativeOrAbsoluteDisplayDate } from '../../../../Utils/DateDisplay';
|
||||
import { getGravatarUrl } from '../../../../UI/GravatarUrl';
|
||||
import { type LastModifiedInfo } from './utils';
|
||||
import { type FileMetadata } from '../../../../ProjectsStorage';
|
||||
import AvatarWithStatusAndTooltip from './AvatarWithStatusAndTooltip';
|
||||
import {
|
||||
getDetailedProjectDisplayDate,
|
||||
getProjectDisplayDate,
|
||||
} from '../../../../GameDashboard/GameDashboardCard';
|
||||
|
||||
type LastModificationInfoProps = {|
|
||||
file: FileMetadataAndStorageProviderName,
|
||||
@@ -22,6 +26,7 @@ type LastModificationInfoProps = {|
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
currentFileMetadata: ?FileMetadata,
|
||||
textColor?: 'primary' | 'secondary',
|
||||
textSize?: 'body-small',
|
||||
textPrefix?: React.Node,
|
||||
|};
|
||||
|
||||
@@ -32,6 +37,7 @@ const LastModificationInfo = ({
|
||||
authenticatedUser,
|
||||
currentFileMetadata,
|
||||
textColor = 'primary',
|
||||
textSize = 'body',
|
||||
textPrefix,
|
||||
}: LastModificationInfoProps) => {
|
||||
const isProjectSavedOnCloud =
|
||||
@@ -77,7 +83,7 @@ const LastModificationInfo = ({
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
{textPrefix && (
|
||||
<Text color="secondary" noMargin size="body-small">
|
||||
<Text color="secondary" noMargin size={textSize}>
|
||||
{textPrefix}
|
||||
</Text>
|
||||
)}
|
||||
@@ -97,20 +103,20 @@ const LastModificationInfo = ({
|
||||
hideStatus={!isProjectOpenedNotTheLatestVersion}
|
||||
/>
|
||||
)}
|
||||
<Text noMargin color={textColor} size="body-small">
|
||||
{isCurrentProjectOpened ? (
|
||||
{isCurrentProjectOpened ? (
|
||||
<Text noMargin color={textColor} size={textSize}>
|
||||
<Trans>Modifying</Trans>
|
||||
) : (
|
||||
getRelativeOrAbsoluteDisplayDate({
|
||||
i18n,
|
||||
dateAsNumber: lastModifiedAt,
|
||||
sameDayFormat: 'todayAndHour',
|
||||
dayBeforeFormat: 'yesterdayAndHour',
|
||||
relativeLimit: 'currentWeek',
|
||||
sameWeekFormat: 'thisWeek',
|
||||
})
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={getDetailedProjectDisplayDate(i18n, lastModifiedAt)}
|
||||
>
|
||||
<Text noMargin color={textColor} size={textSize}>
|
||||
{getProjectDisplayDate(i18n, lastModifiedAt)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -90,6 +90,7 @@ const ProjectFileList = ({
|
||||
}: Props) => {
|
||||
const projectFiles = useProjectsListFor(game);
|
||||
const contextMenu = React.useRef<?ContextMenuInterface>(null);
|
||||
const [loadingProjectId, setLoadingProjectId] = React.useState<?string>(null);
|
||||
const { removeRecentProjectFile } = React.useContext(PreferencesContext);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const {
|
||||
@@ -141,6 +142,18 @@ const ProjectFileList = ({
|
||||
[removeRecentProjectFile, showConfirmation]
|
||||
);
|
||||
|
||||
const onWillDeleteCloudProject = React.useCallback(
|
||||
async (i18n, file: FileMetadataAndStorageProviderName) => {
|
||||
setLoadingProjectId(file.fileMetadata.fileIdentifier);
|
||||
try {
|
||||
await onDeleteCloudProject(i18n, file);
|
||||
} finally {
|
||||
setLoadingProjectId(null);
|
||||
}
|
||||
},
|
||||
[onDeleteCloudProject]
|
||||
);
|
||||
|
||||
const buildContextMenu = (
|
||||
i18n: I18nType,
|
||||
file: ?FileMetadataAndStorageProviderName
|
||||
@@ -155,7 +168,7 @@ const ProjectFileList = ({
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
click: () => onDeleteCloudProject(i18n, file),
|
||||
click: () => onWillDeleteCloudProject(i18n, file),
|
||||
}
|
||||
);
|
||||
} else if (file.storageProviderName === 'LocalFile') {
|
||||
@@ -226,7 +239,7 @@ const ProjectFileList = ({
|
||||
</ListItem>
|
||||
))
|
||||
) : projectFiles.length > 0 ? (
|
||||
<Line>
|
||||
<Line noMargin>
|
||||
<Column noMargin expand>
|
||||
{!isMobile && (
|
||||
<Line justifyContent="space-between">
|
||||
@@ -253,7 +266,10 @@ const ProjectFileList = ({
|
||||
key={file.fileMetadata.fileIdentifier}
|
||||
file={file}
|
||||
onOpenContextMenu={openContextMenu}
|
||||
isLoading={disabled}
|
||||
disabled={disabled}
|
||||
isLoading={
|
||||
file.fileMetadata.fileIdentifier === loadingProjectId
|
||||
}
|
||||
currentFileMetadata={currentFileMetadata}
|
||||
storageProviders={storageProviders}
|
||||
isWindowSizeMediumOrLarger={!isMobile}
|
||||
|
@@ -62,6 +62,7 @@ type ProjectFileListItemProps = {|
|
||||
storageProviders: Array<StorageProvider>,
|
||||
onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|
||||
isWindowSizeMediumOrLarger: boolean,
|
||||
disabled: boolean,
|
||||
isLoading: boolean,
|
||||
onOpenContextMenu: (
|
||||
event: ClientCoordinates,
|
||||
@@ -76,6 +77,7 @@ export const ProjectFileListItem = ({
|
||||
storageProviders,
|
||||
onOpenProject,
|
||||
isWindowSizeMediumOrLarger,
|
||||
disabled,
|
||||
isLoading,
|
||||
onOpenContextMenu,
|
||||
}: ProjectFileListItemProps) => {
|
||||
@@ -86,12 +88,20 @@ export const ProjectFileListItem = ({
|
||||
file.storageProviderName
|
||||
);
|
||||
|
||||
const onWillOpenContextMenu = React.useCallback(
|
||||
(event: ClientCoordinates, file: FileMetadataAndStorageProviderName) => {
|
||||
if (disabled) return;
|
||||
onOpenContextMenu(event, file);
|
||||
},
|
||||
[disabled, onOpenContextMenu]
|
||||
);
|
||||
|
||||
const longTouchForContextMenuProps = useLongTouch(
|
||||
React.useCallback(
|
||||
event => {
|
||||
onOpenContextMenu(event, file);
|
||||
onWillOpenContextMenu(event, file);
|
||||
},
|
||||
[onOpenContextMenu, file]
|
||||
[onWillOpenContextMenu, file]
|
||||
)
|
||||
);
|
||||
return (
|
||||
@@ -102,10 +112,11 @@ export const ProjectFileListItem = ({
|
||||
button
|
||||
key={file.fileMetadata.fileIdentifier}
|
||||
onClick={() => {
|
||||
if (disabled) return;
|
||||
onOpenProject(file);
|
||||
}}
|
||||
style={styles.listItem}
|
||||
onContextMenu={event => onOpenContextMenu(event, file)}
|
||||
onContextMenu={event => onWillOpenContextMenu(event, file)}
|
||||
{...longTouchForContextMenuProps}
|
||||
>
|
||||
{isWindowSizeMediumOrLarger ? (
|
||||
@@ -158,7 +169,7 @@ export const ProjectFileListItem = ({
|
||||
onClick={event => {
|
||||
// prevent triggering the click on the list item.
|
||||
event.stopPropagation();
|
||||
onOpenContextMenu(event, file);
|
||||
onWillOpenContextMenu(event, file);
|
||||
}}
|
||||
>
|
||||
<ThreeDotsMenu />
|
||||
|
@@ -146,7 +146,6 @@ const CreateSection = ({
|
||||
const [initialWidgetToScrollTo, setInitialWidgetToScrollTo] = React.useState(
|
||||
null
|
||||
);
|
||||
const [showAllGameTemplates, setShowAllGameTemplates] = React.useState(false);
|
||||
const { routeArguments, removeRouteArguments } = React.useContext(
|
||||
RouterContext
|
||||
);
|
||||
@@ -315,7 +314,7 @@ const CreateSection = ({
|
||||
const isCurrentProjectOpened =
|
||||
!!project &&
|
||||
!!currentFileMetadata &&
|
||||
fileMetadata.gameId === currentFileMetadata.gameId;
|
||||
fileMetadata.fileIdentifier === currentFileMetadata.fileIdentifier;
|
||||
|
||||
if (isCurrentProjectOpened) {
|
||||
const result = await showConfirmation({
|
||||
@@ -445,26 +444,6 @@ const CreateSection = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (showAllGameTemplates) {
|
||||
return (
|
||||
<SectionContainer
|
||||
backAction={() => setShowAllGameTemplates(false)}
|
||||
flexBody
|
||||
>
|
||||
<SectionRow expand>
|
||||
<ExampleStore
|
||||
onSelectExampleShortHeader={onSelectExampleShortHeader}
|
||||
onSelectPrivateGameTemplateListingData={
|
||||
onSelectPrivateGameTemplateListingData
|
||||
}
|
||||
i18n={i18n}
|
||||
columnsCount={getExampleItemsColumns(windowSize, isLandscape)}
|
||||
/>
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -610,7 +589,7 @@ const CreateSection = ({
|
||||
<Trans>Start from a template</Trans>
|
||||
</Text>
|
||||
<FlatButton
|
||||
onClick={() => setShowAllGameTemplates(true)}
|
||||
onClick={onOpenNewProjectSetupDialog}
|
||||
label={
|
||||
isMobile ? (
|
||||
<Trans>Browse</Trans>
|
||||
|
@@ -22,19 +22,18 @@ import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext'
|
||||
import { SECTION_DESKTOP_SPACING } from './SectionContainer';
|
||||
|
||||
const iconSize = 20;
|
||||
const iconButtonPaddingTop = 8;
|
||||
const iconButtonPadding = 8;
|
||||
/**
|
||||
* Padding bottom is bigger than padding top to leave space for the Android/iOS
|
||||
* bottom navigation bar.
|
||||
*/
|
||||
const iconButtonPaddingBottom = 20;
|
||||
const iconButtonPaddingHorizontal = 8;
|
||||
const iconButtonMarginBottom = 12;
|
||||
const iconButtonLabelPadding = 4;
|
||||
const toolbarHeight =
|
||||
iconSize +
|
||||
2 * iconButtonLabelPadding +
|
||||
iconButtonPaddingTop +
|
||||
iconButtonPaddingBottom;
|
||||
2 * iconButtonPadding +
|
||||
iconButtonMarginBottom;
|
||||
export const homepageDesktopMenuBarWidth = 230;
|
||||
export const homepageMediumMenuBarWidth =
|
||||
verticalTabButtonSize + 2 * marginsSize;
|
||||
@@ -62,7 +61,8 @@ export const styles = {
|
||||
marginBottom: 'env(safe-area-inset-bottom)',
|
||||
},
|
||||
mobileButton: {
|
||||
padding: `${iconButtonPaddingTop}px ${iconButtonPaddingHorizontal}px ${iconButtonPaddingBottom}px ${iconButtonPaddingHorizontal}px`,
|
||||
padding: iconButtonPadding,
|
||||
marginBottom: iconButtonMarginBottom,
|
||||
fontSize: 'inherit',
|
||||
},
|
||||
buttonLabel: { padding: iconButtonLabelPadding, display: 'flex' },
|
||||
|
@@ -37,6 +37,7 @@ const styles = {
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
scrollContainer: {
|
||||
overflowY: 'scroll', // Force a scrollbar to prevent layout shifts.
|
||||
|
@@ -167,6 +167,7 @@ const TeamMemberProjectsView = ({
|
||||
file={file}
|
||||
currentFileMetadata={currentFileMetadata}
|
||||
key={file.fileMetadata.fileIdentifier}
|
||||
disabled={false}
|
||||
isLoading={false}
|
||||
onOpenContextMenu={openContextMenu}
|
||||
onOpenProject={onOpenRecentFile}
|
||||
|
@@ -12,6 +12,9 @@ import {
|
||||
} from './BaseEditor';
|
||||
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
import { switchToSceneEdition } from '../../EmbeddedGame/EmbeddedGameFrame';
|
||||
|
||||
const gameEditorMode = 'embedded-game'; // TODO: move to a preference.
|
||||
|
||||
export class SceneEditorContainer extends React.Component<RenderEditorContainerProps> {
|
||||
editor: ?SceneEditor;
|
||||
@@ -32,6 +35,10 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
|
||||
if (this.props.isActive) {
|
||||
const { projectItemName } = this.props;
|
||||
this.props.setPreviewedLayout(projectItemName);
|
||||
|
||||
if (gameEditorMode === 'embedded-game' && projectItemName) {
|
||||
switchToSceneEdition({ sceneName: projectItemName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +46,10 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
|
||||
if (!prevProps.isActive && this.props.isActive) {
|
||||
const { projectItemName } = this.props;
|
||||
this.props.setPreviewedLayout(projectItemName);
|
||||
|
||||
if (gameEditorMode === 'embedded-game' && projectItemName) {
|
||||
switchToSceneEdition({ sceneName: projectItemName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +155,7 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
|
||||
}
|
||||
onOpenEvents={this.props.onOpenEvents}
|
||||
isActive={isActive}
|
||||
previewDebuggerServer={this.props.previewDebuggerServer}
|
||||
hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps}
|
||||
openBehaviorEvents={this.props.openBehaviorEvents}
|
||||
onExtractAsExternalLayout={this.props.onExtractAsExternalLayout}
|
||||
|
@@ -16,6 +16,7 @@ import {
|
||||
ClosableTab,
|
||||
type ClosableTabProps,
|
||||
} from '../../UI/ClosableTabs';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
|
||||
const DragSourceAndDropTarget = makeDragSourceAndDropTarget<EditorTab>(
|
||||
'draggable-closable-tab'
|
||||
@@ -46,6 +47,7 @@ export function DraggableEditorTabs({
|
||||
onDropTab,
|
||||
}: DraggableEditorTabsProps) {
|
||||
let draggedTabIndex: ?number = null;
|
||||
const { windowSize } = useResponsiveWindowSize();
|
||||
|
||||
const currentTab = getCurrentTab(editorTabs);
|
||||
|
||||
@@ -54,10 +56,15 @@ export function DraggableEditorTabs({
|
||||
if (!currentTab) return;
|
||||
const tabElement = document.getElementById(getTabId(currentTab));
|
||||
if (tabElement) {
|
||||
tabElement.scrollIntoView();
|
||||
tabElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
// Use 'end' to keep "Home" tab visible on small screens
|
||||
// when opening a new project.
|
||||
inline: windowSize === 'small' ? 'end' : 'nearest',
|
||||
});
|
||||
}
|
||||
},
|
||||
[currentTab]
|
||||
[currentTab, windowSize]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -48,6 +48,8 @@ export type EditorTab = {|
|
||||
extraEditorProps: ?EditorContainerExtraProps,
|
||||
/** If set to false, the tab can't be closed. */
|
||||
closable: boolean,
|
||||
/** If set to true, `pointer-events: none` is applied to the tab content. */
|
||||
removePointerEvents: boolean,
|
||||
|};
|
||||
|
||||
export type EditorTabsState = {|
|
||||
@@ -91,6 +93,7 @@ export type EditorOpeningOptions = {|
|
||||
extraEditorProps?: EditorContainerExtraProps,
|
||||
dontFocusTab?: boolean,
|
||||
closable?: boolean,
|
||||
removePointerEvents?: boolean,
|
||||
|};
|
||||
|
||||
export const getEditorTabMetadata = (
|
||||
@@ -138,6 +141,7 @@ export const openEditorTab = (
|
||||
key,
|
||||
extraEditorProps,
|
||||
dontFocusTab,
|
||||
removePointerEvents,
|
||||
closable,
|
||||
}: EditorOpeningOptions
|
||||
): EditorTabsState => {
|
||||
@@ -163,6 +167,7 @@ export const openEditorTab = (
|
||||
extraEditorProps,
|
||||
editorRef: null,
|
||||
closable: typeof closable === 'undefined' ? true : !!closable,
|
||||
removePointerEvents: !!removePointerEvents,
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
type PreviewDebuggerServer,
|
||||
type DebuggerId,
|
||||
type HotReloaderLog,
|
||||
type DebuggerStatus,
|
||||
} from '../ExportAndShare/PreviewLauncher.flow';
|
||||
|
||||
/** Represents what should be run when a preview is launched */
|
||||
@@ -22,26 +23,31 @@ export type PreviewState = {|
|
||||
|};
|
||||
|
||||
type PreviewDebuggerServerWatcherResults = {|
|
||||
previewDebuggerIds: Array<DebuggerId>,
|
||||
hasNonEditionPreviewsRunning: boolean,
|
||||
|
||||
hotReloadLogs: Array<HotReloaderLog>,
|
||||
clearHotReloadLogs: () => void,
|
||||
|
||||
hardReloadAllPreviews: () => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Return the ids of the debuggers being run, watching for changes (new
|
||||
* Return the status of the debuggers being run, watching for changes (new
|
||||
* debugger launched or existing one closed).
|
||||
*/
|
||||
export const usePreviewDebuggerServerWatcher = (
|
||||
previewDebuggerServer: ?PreviewDebuggerServer
|
||||
): PreviewDebuggerServerWatcherResults => {
|
||||
const [debuggerIds, setDebuggerIds] = React.useState<Array<DebuggerId>>([]);
|
||||
const [debuggerStatus, setDebuggerStatus] = React.useState<{
|
||||
[DebuggerId]: DebuggerStatus,
|
||||
}>({});
|
||||
const [hotReloadLogs, setHotReloadLogs] = React.useState<
|
||||
Array<HotReloaderLog>
|
||||
>([]);
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!previewDebuggerServer) {
|
||||
setDebuggerIds([]);
|
||||
setDebuggerStatus({});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,10 +56,24 @@ export const usePreviewDebuggerServerWatcher = (
|
||||
// Nothing to do.
|
||||
},
|
||||
onConnectionClosed: ({ id, debuggerIds }) => {
|
||||
setDebuggerIds([...debuggerIds]);
|
||||
// Remove the debugger status.
|
||||
setDebuggerStatus(debuggerStatus => {
|
||||
const {
|
||||
[id]: closedDebuggerStatus,
|
||||
...otherDebuggerStatus
|
||||
} = debuggerStatus;
|
||||
console.info(
|
||||
`Connection closed with preview #${id}. Last status was:`,
|
||||
closedDebuggerStatus
|
||||
);
|
||||
|
||||
return otherDebuggerStatus;
|
||||
});
|
||||
},
|
||||
onConnectionOpened: ({ id, debuggerIds }) => {
|
||||
setDebuggerIds([...debuggerIds]);
|
||||
// Ask the new debugger client for its status (but don't assume anything
|
||||
// at this stage).
|
||||
previewDebuggerServer.sendMessage(id, { command: 'getStatus' });
|
||||
},
|
||||
onConnectionErrored: ({ id }) => {
|
||||
// Nothing to do (onConnectionClosed is called if necessary).
|
||||
@@ -64,6 +84,15 @@ export const usePreviewDebuggerServerWatcher = (
|
||||
onHandleParsedMessage: ({ id, parsedMessage }) => {
|
||||
if (parsedMessage.command === 'hotReloader.logs') {
|
||||
setHotReloadLogs(parsedMessage.payload);
|
||||
} else if (parsedMessage.command === 'status') {
|
||||
setDebuggerStatus(debuggerStatus => ({
|
||||
...debuggerStatus,
|
||||
[id]: {
|
||||
isPaused: !!parsedMessage.payload.isPaused,
|
||||
isInGameEdition: !!parsedMessage.payload.isInGameEdition,
|
||||
sceneName: parsedMessage.payload.sceneName,
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -77,5 +106,28 @@ export const usePreviewDebuggerServerWatcher = (
|
||||
setHotReloadLogs,
|
||||
]);
|
||||
|
||||
return { previewDebuggerIds: debuggerIds, hotReloadLogs, clearHotReloadLogs };
|
||||
const hardReloadAllPreviews = React.useCallback(
|
||||
() => {
|
||||
if (!previewDebuggerServer) return;
|
||||
|
||||
console.info('Hard reloading all previews...');
|
||||
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
|
||||
previewDebuggerServer.sendMessage(debuggerId, {
|
||||
command: 'hardReload',
|
||||
});
|
||||
});
|
||||
},
|
||||
[previewDebuggerServer]
|
||||
);
|
||||
|
||||
const hasNonEditionPreviewsRunning = Object.keys(debuggerStatus).some(
|
||||
key => !debuggerStatus[+key].isInGameEdition
|
||||
);
|
||||
|
||||
return {
|
||||
hasNonEditionPreviewsRunning,
|
||||
hotReloadLogs,
|
||||
clearHotReloadLogs,
|
||||
hardReloadAllPreviews,
|
||||
};
|
||||
};
|
||||
|
@@ -19,7 +19,12 @@ type Props = {|
|
||||
const DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
|
||||
|
||||
const styles = {
|
||||
container: { display: 'flex', flexShrink: 0, alignItems: 'flex-end' },
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
alignItems: 'flex-end',
|
||||
position: 'relative',
|
||||
},
|
||||
leftSideArea: { alignSelf: 'stretch', flexShrink: 0 },
|
||||
rightSideArea: { alignSelf: 'stretch', flex: 1 },
|
||||
menuIcon: { marginLeft: 4, marginRight: 4 },
|
||||
|
@@ -87,21 +87,21 @@ const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
|
||||
click: async () => {
|
||||
await onPreviewWithoutHotReload({ numberOfWindows: 2 });
|
||||
},
|
||||
enabled: isPreviewEnabled && !hasPreviewsRunning,
|
||||
enabled: isPreviewEnabled,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`3 previews in 3 windows`),
|
||||
click: async () => {
|
||||
onPreviewWithoutHotReload({ numberOfWindows: 3 });
|
||||
},
|
||||
enabled: isPreviewEnabled && !hasPreviewsRunning,
|
||||
enabled: isPreviewEnabled,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`4 previews in 4 windows`),
|
||||
click: async () => {
|
||||
onPreviewWithoutHotReload({ numberOfWindows: 4 });
|
||||
},
|
||||
enabled: isPreviewEnabled && !hasPreviewsRunning,
|
||||
enabled: isPreviewEnabled,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -185,7 +185,9 @@ const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
|
||||
<LineStackLayout noMargin>
|
||||
<FlatButtonWithSplitMenu
|
||||
primary
|
||||
onClick={onHotReloadPreview}
|
||||
onClick={
|
||||
hasPreviewsRunning ? onHotReloadPreview : onPreviewWithoutHotReload
|
||||
}
|
||||
disabled={!isPreviewEnabled}
|
||||
icon={hasPreviewsRunning ? <UpdateIcon /> : <PreviewIcon />}
|
||||
label={
|
||||
|
@@ -103,6 +103,7 @@ import {
|
||||
type StorageProviderOperations,
|
||||
type FileMetadata,
|
||||
type SaveAsLocation,
|
||||
type SaveAsOptions,
|
||||
type FileMetadataAndStorageProviderName,
|
||||
type ResourcesActionsMenuBuilder,
|
||||
} from '../ProjectsStorage';
|
||||
@@ -199,6 +200,7 @@ import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import useGamesList from '../GameDashboard/UseGamesList';
|
||||
import useCapturesManager from './UseCapturesManager';
|
||||
import useHomepageWitchForRouting from './UseHomepageWitchForRouting';
|
||||
import { EmbeddedGameFrame } from '../EmbeddedGame/EmbeddedGameFrame';
|
||||
|
||||
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
|
||||
|
||||
@@ -304,7 +306,9 @@ export type Props = {|
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
requestUpdate?: () => void,
|
||||
renderShareDialog: ShareDialogWithoutExportsProps => React.Node,
|
||||
renderGDJSDevelopmentWatcher?: ?() => React.Node,
|
||||
renderGDJSDevelopmentWatcher?: ?({|
|
||||
onGDJSUpdated: () => Promise<void> | void,
|
||||
|}) => React.Node,
|
||||
extensionsLoader?: JsExtensionsLoader,
|
||||
initialFileMetadataToOpen: ?FileMetadata,
|
||||
initialExampleSlugToOpen: ?string,
|
||||
@@ -408,11 +412,11 @@ const MainFrame = (props: Props) => {
|
||||
_previewLauncher.current &&
|
||||
_previewLauncher.current.getPreviewDebuggerServer();
|
||||
const {
|
||||
previewDebuggerIds,
|
||||
hasNonEditionPreviewsRunning,
|
||||
hotReloadLogs,
|
||||
clearHotReloadLogs,
|
||||
hardReloadAllPreviews,
|
||||
} = usePreviewDebuggerServerWatcher(previewDebuggerServer);
|
||||
const hasPreviewsRunning = !!previewDebuggerIds.length;
|
||||
const {
|
||||
ensureInteractionHappened,
|
||||
renderOpenConfirmDialog,
|
||||
@@ -619,6 +623,10 @@ const MainFrame = (props: Props) => {
|
||||
<ExtensionIcon />
|
||||
) : null;
|
||||
|
||||
// Scene editors can have an embedded game, so they redefine manually
|
||||
// which components can have clicks/touches.
|
||||
const removePointerEvents = kind === 'layout';
|
||||
|
||||
const closable = kind !== 'start page';
|
||||
const extraEditorProps =
|
||||
kind === 'start page'
|
||||
@@ -636,6 +644,7 @@ const MainFrame = (props: Props) => {
|
||||
)
|
||||
: null,
|
||||
closable,
|
||||
removePointerEvents,
|
||||
label,
|
||||
projectItemName: name,
|
||||
tabOptions,
|
||||
@@ -1606,6 +1615,7 @@ const MainFrame = (props: Props) => {
|
||||
fullLoadingScreen,
|
||||
forceDiagnosticReport,
|
||||
launchCaptureOptions,
|
||||
isForInGameEdition,
|
||||
}: LaunchPreviewOptions) => {
|
||||
if (!currentProject) return;
|
||||
if (currentProject.getLayoutsCount() === 0) return;
|
||||
@@ -1616,10 +1626,14 @@ const MainFrame = (props: Props) => {
|
||||
setPreviewLoading(true);
|
||||
notifyPreviewOrExportWillStart(state.editorTabs);
|
||||
|
||||
const layoutName = previewState.isPreviewOverriden
|
||||
const layoutName = isForInGameEdition
|
||||
? isForInGameEdition.forcedSceneName
|
||||
: previewState.isPreviewOverriden
|
||||
? previewState.overridenPreviewLayoutName
|
||||
: previewState.previewLayoutName;
|
||||
const externalLayoutName = previewState.isPreviewOverriden
|
||||
const externalLayoutName = isForInGameEdition
|
||||
? isForInGameEdition.forcedExternalLayoutName
|
||||
: previewState.isPreviewOverriden
|
||||
? previewState.overridenPreviewExternalLayoutName
|
||||
: previewState.previewExternalLayoutName;
|
||||
|
||||
@@ -1661,6 +1675,7 @@ const MainFrame = (props: Props) => {
|
||||
const startTime = Date.now();
|
||||
await previewLauncher.launchPreview({
|
||||
project: currentProject,
|
||||
// TODO: replace by scene name and external layout name
|
||||
layout,
|
||||
externalLayout,
|
||||
networkPreview: !!networkPreview,
|
||||
@@ -1671,23 +1686,25 @@ const MainFrame = (props: Props) => {
|
||||
authenticatedPlayer,
|
||||
getIsMenuBarHiddenInPreview: preferences.getIsMenuBarHiddenInPreview,
|
||||
getIsAlwaysOnTopInPreview: preferences.getIsAlwaysOnTopInPreview,
|
||||
numberOfWindows: numberOfWindows || 1,
|
||||
numberOfWindows: numberOfWindows === undefined ? 1 : numberOfWindows,
|
||||
isForInGameEdition: !!isForInGameEdition,
|
||||
captureOptions,
|
||||
onCaptureFinished,
|
||||
});
|
||||
setPreviewLoading(false);
|
||||
|
||||
sendPreviewStarted({
|
||||
quickCustomizationGameId:
|
||||
quickCustomizationDialogOpenedFromGameId || null,
|
||||
networkPreview: !!networkPreview,
|
||||
hotReload: !!hotReload,
|
||||
projectDataOnlyExport: !!projectDataOnlyExport,
|
||||
fullLoadingScreen: !!fullLoadingScreen,
|
||||
numberOfWindows: numberOfWindows || 1,
|
||||
forceDiagnosticReport: !!forceDiagnosticReport,
|
||||
previewLaunchDuration: Date.now() - startTime,
|
||||
});
|
||||
if (!isForInGameEdition)
|
||||
sendPreviewStarted({
|
||||
quickCustomizationGameId:
|
||||
quickCustomizationDialogOpenedFromGameId || null,
|
||||
networkPreview: !!networkPreview,
|
||||
hotReload: !!hotReload,
|
||||
projectDataOnlyExport: !!projectDataOnlyExport,
|
||||
fullLoadingScreen: !!fullLoadingScreen,
|
||||
numberOfWindows: numberOfWindows || 1,
|
||||
forceDiagnosticReport: !!forceDiagnosticReport,
|
||||
previewLaunchDuration: Date.now() - startTime,
|
||||
});
|
||||
|
||||
if (inAppTutorialOrchestratorRef.current) {
|
||||
inAppTutorialOrchestratorRef.current.onPreviewLaunch();
|
||||
@@ -1771,6 +1788,47 @@ const MainFrame = (props: Props) => {
|
||||
[launchPreview]
|
||||
);
|
||||
|
||||
const onLaunchPreviewForInGameEdition = React.useCallback(
|
||||
({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
}: {|
|
||||
sceneName: string,
|
||||
externalLayoutName: ?string,
|
||||
|}) => {
|
||||
launchPreview({
|
||||
networkPreview: false,
|
||||
hotReload: false,
|
||||
forceDiagnosticReport: false,
|
||||
isForInGameEdition: {
|
||||
forcedSceneName: sceneName,
|
||||
forcedExternalLayoutName: externalLayoutName,
|
||||
},
|
||||
numberOfWindows: 0,
|
||||
});
|
||||
},
|
||||
[launchPreview]
|
||||
);
|
||||
|
||||
const relaunchAndThenHardReloadAllPreviews = React.useCallback(
|
||||
async () => {
|
||||
// Build a new preview (so that any changes in runtime files are picked up)
|
||||
// and then ask all previews to "hard reload" themselves (i.e: refresh their page).
|
||||
await launchPreview({
|
||||
networkPreview: false,
|
||||
hotReload: false,
|
||||
forceDiagnosticReport: false,
|
||||
numberOfWindows: 0,
|
||||
});
|
||||
|
||||
hardReloadAllPreviews();
|
||||
},
|
||||
[
|
||||
hardReloadAllPreviews,
|
||||
launchPreview,
|
||||
]
|
||||
);
|
||||
|
||||
const launchQuickCustomizationPreview = React.useCallback(
|
||||
() =>
|
||||
launchPreview({
|
||||
@@ -1789,7 +1847,7 @@ const MainFrame = (props: Props) => {
|
||||
|
||||
const hotReloadPreviewButtonProps: HotReloadPreviewButtonProps = React.useMemo(
|
||||
() => ({
|
||||
hasPreviewsRunning,
|
||||
hasPreviewsRunning: hasNonEditionPreviewsRunning,
|
||||
launchProjectWithLoadingScreenPreview: () =>
|
||||
launchPreview({ fullLoadingScreen: true }),
|
||||
launchProjectDataOnlyPreview: () =>
|
||||
@@ -1797,7 +1855,7 @@ const MainFrame = (props: Props) => {
|
||||
launchProjectCodeAndDataPreview: () =>
|
||||
launchPreview({ hotReload: true, projectDataOnlyExport: false }),
|
||||
}),
|
||||
[hasPreviewsRunning, launchPreview]
|
||||
[hasNonEditionPreviewsRunning, launchPreview]
|
||||
);
|
||||
|
||||
const getEditorsTabStateWithScene = React.useCallback(
|
||||
@@ -2604,15 +2662,27 @@ const MainFrame = (props: Props) => {
|
||||
try {
|
||||
let newSaveAsLocation: ?SaveAsLocation =
|
||||
options && options.forcedSavedAsLocation;
|
||||
let newSaveAsOptions: ?SaveAsOptions = null;
|
||||
if (onChooseSaveProjectAsLocation && !newSaveAsLocation) {
|
||||
const { saveAsLocation } = await onChooseSaveProjectAsLocation({
|
||||
const {
|
||||
saveAsLocation,
|
||||
saveAsOptions,
|
||||
} = await onChooseSaveProjectAsLocation({
|
||||
project: currentProject,
|
||||
fileMetadata: currentFileMetadata,
|
||||
displayOptionToGenerateNewProjectUuid:
|
||||
// No need to display the option if current file metadata doesn't have
|
||||
// a gameId...
|
||||
!!currentFileMetadata &&
|
||||
!!currentFileMetadata.gameId &&
|
||||
// ... or if the project is opened from a URL.
|
||||
oldStorageProvider.internalName !== 'UrlStorageProvider',
|
||||
});
|
||||
if (!saveAsLocation) {
|
||||
return; // Save as was cancelled.
|
||||
}
|
||||
newSaveAsLocation = saveAsLocation;
|
||||
newSaveAsOptions = saveAsOptions;
|
||||
}
|
||||
|
||||
if (canFileMetadataBeSafelySavedAs && currentFileMetadata) {
|
||||
@@ -2627,6 +2697,21 @@ const MainFrame = (props: Props) => {
|
||||
if (!canProjectBeSafelySavedAs) return;
|
||||
}
|
||||
|
||||
let originalProjectUuid = null;
|
||||
if (newSaveAsOptions && newSaveAsOptions.generateNewProjectUuid) {
|
||||
originalProjectUuid = currentProject.getProjectUuid();
|
||||
currentProject.resetProjectUuid();
|
||||
}
|
||||
let originalProjectName = null;
|
||||
const newProjectName =
|
||||
newSaveAsLocation && newSaveAsLocation.name
|
||||
? newSaveAsLocation.name
|
||||
: null;
|
||||
if (newProjectName) {
|
||||
originalProjectName = currentProject.getName();
|
||||
currentProject.setName(newProjectName);
|
||||
}
|
||||
|
||||
const { wasSaved, fileMetadata } = await onSaveProjectAs(
|
||||
currentProject,
|
||||
newSaveAsLocation,
|
||||
@@ -2649,7 +2734,13 @@ const MainFrame = (props: Props) => {
|
||||
}
|
||||
);
|
||||
|
||||
if (!wasSaved) return; // Save was cancelled, don't do anything.
|
||||
if (!wasSaved) {
|
||||
_replaceSnackMessage(i18n._(t`An error occurred. Please try again.`));
|
||||
if (originalProjectName) currentProject.setName(originalProjectName);
|
||||
if (originalProjectUuid)
|
||||
currentProject.setProjectUuid(originalProjectUuid);
|
||||
return;
|
||||
}
|
||||
|
||||
sealUnsavedChanges({ setCheckpointTime: true });
|
||||
_replaceSnackMessage(i18n._(t`Project properly saved`));
|
||||
@@ -3441,7 +3532,7 @@ const MainFrame = (props: Props) => {
|
||||
previewEnabled:
|
||||
!!state.currentProject && state.currentProject.getLayoutsCount() > 0,
|
||||
onOpenProjectManager: toggleProjectManager,
|
||||
hasPreviewsRunning,
|
||||
hasPreviewsRunning: hasNonEditionPreviewsRunning,
|
||||
allowNetworkPreview:
|
||||
!!_previewLauncher.current &&
|
||||
_previewLauncher.current.canDoNetworkPreview(),
|
||||
@@ -3536,6 +3627,30 @@ const MainFrame = (props: Props) => {
|
||||
'main-frame' /* The root styling, done in CSS to read some CSS variables. */
|
||||
}
|
||||
>
|
||||
{!!renderPreviewLauncher &&
|
||||
renderPreviewLauncher(
|
||||
{
|
||||
crashReportUploadLevel:
|
||||
preferences.values.previewCrashReportUploadLevel ||
|
||||
'exclude-javascript-code-events',
|
||||
previewContext: quickCustomizationDialogOpenedFromGameId
|
||||
? 'preview-quick-customization'
|
||||
: 'preview',
|
||||
sourceGameId: quickCustomizationDialogOpenedFromGameId || '',
|
||||
getIncludeFileHashs:
|
||||
eventsFunctionsExtensionsContext.getIncludeFileHashs,
|
||||
onExport: () => openShareDialog('publish'),
|
||||
onCaptureFinished,
|
||||
},
|
||||
(previewLauncher: ?PreviewLauncherInterface) => {
|
||||
_previewLauncher.current = previewLauncher;
|
||||
}
|
||||
)}
|
||||
<EmbeddedGameFrame
|
||||
key={currentProject ? currentProject.ptr : 0}
|
||||
previewDebuggerServer={previewDebuggerServer || null}
|
||||
onLaunchPreviewForInGameEdition={onLaunchPreviewForInGameEdition}
|
||||
/>
|
||||
{!!renderMainMenu &&
|
||||
renderMainMenu(
|
||||
{ ...buildMainMenuProps, isApplicationTopLevelMenu: true },
|
||||
@@ -3643,7 +3758,7 @@ const MainFrame = (props: Props) => {
|
||||
!checkedOutVersionStatus && !cloudProjectRecoveryOpenedVersionId
|
||||
}
|
||||
onOpenDebugger={launchDebuggerAndPreview}
|
||||
hasPreviewsRunning={hasPreviewsRunning}
|
||||
hasPreviewsRunning={hasNonEditionPreviewsRunning}
|
||||
onPreviewWithoutHotReload={launchNewPreview}
|
||||
onNetworkPreview={launchNetworkPreview}
|
||||
onHotReloadPreview={launchHotReloadPreview}
|
||||
@@ -3672,7 +3787,11 @@ const MainFrame = (props: Props) => {
|
||||
const errorBoundaryProps = getEditorErrorBoundaryProps(editorTab.key);
|
||||
|
||||
return (
|
||||
<TabContentContainer key={editorTab.key} active={isCurrentTab}>
|
||||
<TabContentContainer
|
||||
key={editorTab.key}
|
||||
active={isCurrentTab}
|
||||
removePointerEvents={editorTab.removePointerEvents}
|
||||
>
|
||||
<CommandsContextScopedProvider active={isCurrentTab}>
|
||||
<ErrorBoundary
|
||||
componentTitle={errorBoundaryProps.componentTitle}
|
||||
@@ -3803,7 +3922,11 @@ const MainFrame = (props: Props) => {
|
||||
<LoaderModal
|
||||
show={showLoader}
|
||||
progress={fileMetadataOpeningProgress}
|
||||
message={loaderModalOpeningMessage || fileMetadataOpeningMessage}
|
||||
message={
|
||||
loaderModalOpeningMessage ||
|
||||
fileMetadataOpeningMessage ||
|
||||
(previewLoading ? t`Loading preview...` : null)
|
||||
}
|
||||
/>
|
||||
<Snackbar
|
||||
open={state.snackMessageOpen}
|
||||
@@ -3826,25 +3949,6 @@ const MainFrame = (props: Props) => {
|
||||
initialTab: shareDialogInitialTab,
|
||||
gamesList,
|
||||
})}
|
||||
{!!renderPreviewLauncher &&
|
||||
renderPreviewLauncher(
|
||||
{
|
||||
crashReportUploadLevel:
|
||||
preferences.values.previewCrashReportUploadLevel ||
|
||||
'exclude-javascript-code-events',
|
||||
previewContext: quickCustomizationDialogOpenedFromGameId
|
||||
? 'preview-quick-customization'
|
||||
: 'preview',
|
||||
sourceGameId: quickCustomizationDialogOpenedFromGameId || '',
|
||||
getIncludeFileHashs:
|
||||
eventsFunctionsExtensionsContext.getIncludeFileHashs,
|
||||
onExport: () => openShareDialog('publish'),
|
||||
onCaptureFinished,
|
||||
},
|
||||
(previewLauncher: ?PreviewLauncherInterface) => {
|
||||
_previewLauncher.current = previewLauncher;
|
||||
}
|
||||
)}
|
||||
{chooseResourceOptions && onResourceChosen && !!currentProject && (
|
||||
<NewResourceDialog
|
||||
project={currentProject}
|
||||
@@ -3977,7 +4081,9 @@ const MainFrame = (props: Props) => {
|
||||
)}
|
||||
{state.gdjsDevelopmentWatcherEnabled &&
|
||||
renderGDJSDevelopmentWatcher &&
|
||||
renderGDJSDevelopmentWatcher()}
|
||||
renderGDJSDevelopmentWatcher({
|
||||
onGDJSUpdated: relaunchAndThenHardReloadAllPreviews,
|
||||
})}
|
||||
{!!hotReloadLogs.length && (
|
||||
<HotReloadLogsDialog
|
||||
logs={hotReloadLogs}
|
||||
|
@@ -17,6 +17,8 @@ import {
|
||||
getRequirementsErrors,
|
||||
isMarketingPlanActive,
|
||||
} from './MarketingPlanUtils';
|
||||
import MarketingPlansDialog from './MarketingPlansDialog';
|
||||
import Link from '../UI/Link';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
@@ -40,6 +42,10 @@ const MarketingPlanSingleDisplay = ({
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
const [
|
||||
marketingPlansDialogOpen,
|
||||
setMarketingPlansDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const activeGameFeaturings: ?(GameFeaturing[]) = React.useMemo(
|
||||
() => {
|
||||
if (!gameFeaturings) return null;
|
||||
@@ -90,11 +96,21 @@ const MarketingPlanSingleDisplay = ({
|
||||
</Paper>
|
||||
<Text size="body-small" color="inherit">
|
||||
<Trans>
|
||||
Get ready-made packs to make your game visible to the GDevelop
|
||||
community.
|
||||
Make your game visible to the GDevelop community and to the world
|
||||
with{' '}
|
||||
<Link onClick={() => setMarketingPlansDialogOpen(true)} href="#">
|
||||
<Trans>Marketing Boosts</Trans>
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
{marketingPlansDialogOpen && game && (
|
||||
<MarketingPlansDialog
|
||||
game={game}
|
||||
onClose={() => setMarketingPlansDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -123,8 +123,8 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
<ColumnStackLayout noMargin>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>
|
||||
Get ready-made packs to make your game visible to the GDevelop
|
||||
community.{' '}
|
||||
Make your game visible to the GDevelop community and to the
|
||||
world with Marketing Boosts.{' '}
|
||||
<Link
|
||||
href="https://wiki.gdevelop.io/gdevelop5/interface/games-dashboard/marketing"
|
||||
onClick={() =>
|
||||
|
@@ -307,7 +307,7 @@ const CollisionMasksEditor = ({
|
||||
if (!animations.getAnimationsCount()) return null;
|
||||
const resourceName = sprite ? sprite.getImageName() : '';
|
||||
|
||||
const editors: { [string]: Editor } = {
|
||||
const editors: { [string]: Editor | null } = {
|
||||
preview: {
|
||||
type: 'primary',
|
||||
noTitleBar: true,
|
||||
|
@@ -205,7 +205,7 @@ const PointsEditor = ({
|
||||
if (!animations.getAnimationsCount()) return null;
|
||||
const resourceName = sprite ? sprite.getImageName() : '';
|
||||
|
||||
const editors: { [string]: Editor } = {
|
||||
const editors: { [string]: Editor | null } = {
|
||||
preview: {
|
||||
type: 'primary',
|
||||
noTitleBar: true,
|
||||
|
@@ -13,6 +13,7 @@ import AchievementList from './AchievementList';
|
||||
import Trophy from '../../UI/CustomSvgIcons/Trophy';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import { Column } from '../../UI/Grid';
|
||||
|
||||
type Props = {|
|
||||
achievements: ?Array<Achievement>,
|
||||
@@ -46,47 +47,52 @@ const UserAchievements = ({
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
|
||||
return (
|
||||
<ResponsiveLineStackLayout>
|
||||
{!!badges && !!achievements ? (
|
||||
<>
|
||||
<div style={styles.leftContainer}>
|
||||
<div
|
||||
style={{
|
||||
...styles.summary,
|
||||
padding: isMobile ? '0 20' : '20',
|
||||
}}
|
||||
>
|
||||
<Trophy color="secondary" fontSize="large" />
|
||||
<Text size="block-title">
|
||||
<Trans>
|
||||
{badges.length}/{achievements.length} achievements
|
||||
</Trans>
|
||||
{badges.length === 0 && (
|
||||
<>
|
||||
{' '}
|
||||
<Trans>(yet!)</Trans>
|
||||
</>
|
||||
<Column expand noMargin>
|
||||
<Text size="block-title">
|
||||
<Trans>Achievements</Trans>
|
||||
</Text>
|
||||
<ResponsiveLineStackLayout>
|
||||
{!!badges && !!achievements ? (
|
||||
<>
|
||||
<div style={styles.leftContainer}>
|
||||
<div
|
||||
style={{
|
||||
...styles.summary,
|
||||
padding: isMobile ? '0 20' : '20',
|
||||
}}
|
||||
>
|
||||
<Trophy color="secondary" fontSize="large" />
|
||||
<Text size="block-title">
|
||||
<Trans>
|
||||
{badges.length}/{achievements.length} achievements
|
||||
</Trans>
|
||||
{badges.length === 0 && (
|
||||
<>
|
||||
{' '}
|
||||
<Trans>(yet!)</Trans>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{badges.length > 0 && (
|
||||
<div style={styles.rightContainer}>
|
||||
{badges && achievements && (
|
||||
<AchievementList
|
||||
badges={badges}
|
||||
achievements={achievements}
|
||||
displayUnclaimedAchievements={displayUnclaimedAchievements}
|
||||
displayNotifications={displayNotifications}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{badges.length > 0 && (
|
||||
<div style={styles.rightContainer}>
|
||||
{badges && achievements && (
|
||||
<AchievementList
|
||||
badges={badges}
|
||||
achievements={achievements}
|
||||
displayUnclaimedAchievements={displayUnclaimedAchievements}
|
||||
displayNotifications={displayNotifications}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -245,7 +245,7 @@ const ContributionDetails = ({ userId }: Props) => {
|
||||
</Column>
|
||||
</>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
<PlaceholderLoader style={{ minHeight: 205 }} />
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
|
@@ -161,7 +161,6 @@ const CurrentUsageDisplayer = ({
|
||||
!isFeatureLocked ? 'Build limit reached' : 'Unlock build type'
|
||||
}
|
||||
label={<Trans>Upgrade your subscription</Trans>}
|
||||
makeButtonRaised
|
||||
payWithCreditsOptions={
|
||||
hidePurchaseWithCredits
|
||||
? undefined
|
||||
@@ -208,7 +207,6 @@ const CurrentUsageDisplayer = ({
|
||||
!isFeatureLocked ? 'Build limit reached' : 'Unlock build type'
|
||||
}
|
||||
label={<Trans>Get a subscription</Trans>}
|
||||
makeButtonRaised={quota.limitReached}
|
||||
payWithCreditsOptions={
|
||||
!quota.limitReached || hidePurchaseWithCredits
|
||||
? undefined
|
||||
|
@@ -181,7 +181,7 @@ const ProfileDialog = ({ open, onClose }: Props) => {
|
||||
authenticatedUser.onOpenChangeEmailDialog
|
||||
}
|
||||
/>
|
||||
{isStudentAccount ? null : subscriptionPlansWithPricingSystems ? (
|
||||
{isStudentAccount ? null : (
|
||||
<SubscriptionDetails
|
||||
subscription={authenticatedUser.subscription}
|
||||
subscriptionPlansWithPricingSystems={
|
||||
@@ -190,8 +190,6 @@ const ProfileDialog = ({ open, onClose }: Props) => {
|
||||
onManageSubscription={onManageSubscription}
|
||||
isManageSubscriptionLoading={isManageSubscriptionLoading}
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
{!isStudentAccount && !isTeacherAccount && (
|
||||
<Column noMargin>
|
||||
|
@@ -1,21 +1,26 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column } from '../../UI/Grid';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import Link from '../../UI/Link';
|
||||
import { type SubscriptionDialogDisplayReason } from '../../Utils/Analytics/EventSender';
|
||||
import { SubscriptionSuggestionContext } from './SubscriptionSuggestionContext';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import { CalloutCard } from '../../UI/CalloutCard';
|
||||
import classes from './GetSubscriptionCard.module.css';
|
||||
import Paper from '../../UI/Paper';
|
||||
import CrownShining from '../../UI/CustomSvgIcons/CrownShining';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
|
||||
const styles = {
|
||||
paper: {
|
||||
zIndex: 2, // Make sure the paper is above the background for the border effect.
|
||||
flex: 1,
|
||||
},
|
||||
diamondIcon: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 70,
|
||||
height: 70,
|
||||
},
|
||||
coinIcon: {
|
||||
width: 12,
|
||||
@@ -29,7 +34,6 @@ type Props = {|
|
||||
children: React.Node,
|
||||
subscriptionDialogOpeningReason: SubscriptionDialogDisplayReason,
|
||||
label?: React.Node,
|
||||
makeButtonRaised?: boolean,
|
||||
hideButton?: boolean,
|
||||
payWithCreditsOptions?: {|
|
||||
label: React.Node,
|
||||
@@ -41,65 +45,54 @@ const GetSubscriptionCard = ({
|
||||
children,
|
||||
subscriptionDialogOpeningReason,
|
||||
label,
|
||||
makeButtonRaised,
|
||||
hideButton,
|
||||
payWithCreditsOptions,
|
||||
}: Props) => {
|
||||
const { openSubscriptionDialog } = React.useContext(
|
||||
SubscriptionSuggestionContext
|
||||
);
|
||||
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
return (
|
||||
<CalloutCard
|
||||
renderImage={style => (
|
||||
<img src="res/diamond.svg" style={styles.diamondIcon} alt="diamond" />
|
||||
)}
|
||||
>
|
||||
<Column expand justifyContent="center">
|
||||
<ResponsiveLineStackLayout alignItems="center" noColumnMargin noMargin>
|
||||
<Column noMargin expand>
|
||||
{children}
|
||||
<div className={classes.premiumContainer}>
|
||||
<Paper style={styles.paper} background="medium">
|
||||
<Line expand alignItems="center" noMargin={!isMobile}>
|
||||
<img src="res/diamond.svg" style={styles.diamondIcon} alt="diamond" />
|
||||
<Column expand justifyContent="center">
|
||||
<ResponsiveLineStackLayout
|
||||
alignItems="center"
|
||||
noColumnMargin
|
||||
noMargin
|
||||
>
|
||||
<Column noMargin expand>
|
||||
{children}
|
||||
</Column>
|
||||
{payWithCreditsOptions && (
|
||||
<FlatButton
|
||||
leftIcon={<Coin style={styles.coinIcon} />}
|
||||
label={payWithCreditsOptions.label}
|
||||
primary
|
||||
onClick={payWithCreditsOptions.onPayWithCredits}
|
||||
/>
|
||||
)}
|
||||
{!hideButton && (
|
||||
<RaisedButton
|
||||
label={label || <Trans>Upgrade</Trans>}
|
||||
primary
|
||||
onClick={() => {
|
||||
openSubscriptionDialog({
|
||||
analyticsMetadata: {
|
||||
reason: subscriptionDialogOpeningReason,
|
||||
},
|
||||
});
|
||||
}}
|
||||
icon={<CrownShining fontSize="small" />}
|
||||
/>
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
{payWithCreditsOptions && (
|
||||
<FlatButton
|
||||
leftIcon={<Coin style={styles.coinIcon} />}
|
||||
label={payWithCreditsOptions.label}
|
||||
primary
|
||||
onClick={payWithCreditsOptions.onPayWithCredits}
|
||||
/>
|
||||
)}
|
||||
{!hideButton &&
|
||||
(!makeButtonRaised ? (
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => {
|
||||
openSubscriptionDialog({
|
||||
analyticsMetadata: {
|
||||
reason: subscriptionDialogOpeningReason,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text noMargin color="inherit">
|
||||
{label || <Trans>Upgrade</Trans>}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<RaisedButton
|
||||
label={label || <Trans>Upgrade</Trans>}
|
||||
primary
|
||||
onClick={() => {
|
||||
openSubscriptionDialog({
|
||||
analyticsMetadata: {
|
||||
reason: subscriptionDialogOpeningReason,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</CalloutCard>
|
||||
</Line>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -0,0 +1,19 @@
|
||||
.premiumContainer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.premiumContainer::before {
|
||||
content: '';
|
||||
display: block;
|
||||
background: linear-gradient(90deg, var(--theme-premium-teal) 0%, var(--theme-premium-orange) 100%);
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
|
||||
import {
|
||||
type SubscriptionPlan,
|
||||
type SubscriptionPlanWithPricingSystems,
|
||||
type SubscriptionPlanPricingSystem,
|
||||
} from '../../Utils/GDevelopServices/Usage';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout';
|
||||
import CheckCircle from '../../UI/CustomSvgIcons/CheckCircle';
|
||||
import Paper from '../../UI/Paper';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
@@ -428,20 +428,23 @@ const PlanCard = (props: Props) => {
|
||||
</Line>
|
||||
{mainPricingSystem && (
|
||||
<Paper background="light" style={styles.planPricesPaper}>
|
||||
{otherPricingSystem && (
|
||||
<span style={styles.discountedPrice}>
|
||||
{getPlanPrices({
|
||||
pricingSystems: [
|
||||
extrapolateMonthlyPricingSystemToYearlyBasis(
|
||||
otherPricingSystem
|
||||
),
|
||||
],
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{getPlanPrices({
|
||||
pricingSystems: [mainPricingSystem],
|
||||
})}
|
||||
<LineStackLayout noMargin>
|
||||
{otherPricingSystem && (
|
||||
<span style={styles.discountedPrice}>
|
||||
{getPlanPrices({
|
||||
pricingSystems: [
|
||||
extrapolateMonthlyPricingSystemToYearlyBasis(
|
||||
otherPricingSystem
|
||||
),
|
||||
],
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{otherPricingSystem && <Divider orientation="vertical" />}
|
||||
{getPlanPrices({
|
||||
pricingSystems: [mainPricingSystem],
|
||||
})}
|
||||
</LineStackLayout>
|
||||
</Paper>
|
||||
)}
|
||||
<Spacer />
|
||||
|
@@ -63,28 +63,32 @@ const subscriptionOptions: {
|
||||
title: React.Node,
|
||||
description: React.Node,
|
||||
icon: React.Node,
|
||||
buttonColor?: 'primary' | 'premium',
|
||||
|},
|
||||
} = {
|
||||
individual: {
|
||||
title: <Trans>For Individuals</Trans>,
|
||||
description: <Trans>Hobbyists and indie devs</Trans>,
|
||||
icon: <IndividualPlans style={{ width: 115, height: 100 }} />,
|
||||
buttonColor: 'premium',
|
||||
},
|
||||
team: {
|
||||
title: <Trans>For Teams</Trans>,
|
||||
description: <Trans>Companies, studios and agencies</Trans>,
|
||||
icon: <TeamPlans style={{ width: 175, height: 100 }} />,
|
||||
buttonColor: 'premium',
|
||||
},
|
||||
education: {
|
||||
title: <Trans>For Education</Trans>,
|
||||
description: <Trans>Teachers, courses and universities</Trans>,
|
||||
icon: <EducationPlans style={{ width: 110, height: 100 }} />,
|
||||
buttonColor: 'primary',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
subscription: ?Subscription,
|
||||
subscriptionPlansWithPricingSystems: SubscriptionPlanWithPricingSystems[],
|
||||
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
|
||||
onManageSubscription: () => void | Promise<void>,
|
||||
isManageSubscriptionLoading: boolean,
|
||||
simulateNativeMobileApp?: boolean,
|
||||
@@ -130,7 +134,7 @@ const SubscriptionDetails = ({
|
||||
setError(null);
|
||||
setIsLoadingUserPrice(true);
|
||||
try {
|
||||
if (!subscription) {
|
||||
if (!subscription || !subscriptionPlansWithPricingSystems) {
|
||||
setUserSubscriptionPlanWithPricingSystems(null);
|
||||
return;
|
||||
}
|
||||
@@ -206,38 +210,56 @@ const SubscriptionDetails = ({
|
||||
const isOnOrSimulateMobileApp =
|
||||
isNativeMobileApp() || simulateNativeMobileApp;
|
||||
|
||||
const header = (
|
||||
<Line alignItems="center">
|
||||
<Column noMargin>
|
||||
<Text size="block-title">
|
||||
<Trans>Subscriptions</Trans>
|
||||
</Text>
|
||||
<Text size="body" noMargin>
|
||||
<Trans>
|
||||
Publish to Android, iOS, unlock more cloud projects, leaderboards,
|
||||
collaboration features and more online services.{' '}
|
||||
<Link
|
||||
href="https://gdevelop.io/pricing#feature-comparison"
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
'https://gdevelop.io/pricing#feature-comparison'
|
||||
)
|
||||
}
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <PlaceholderError>{error}</PlaceholderError>;
|
||||
return (
|
||||
<Column noMargin>
|
||||
{header}
|
||||
<PlaceholderError>{error}</PlaceholderError>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
if (!subscription || isLoadingUserPrice) {
|
||||
return <PlaceholderLoader />;
|
||||
if (
|
||||
!subscription ||
|
||||
!subscriptionPlansWithPricingSystems ||
|
||||
isLoadingUserPrice
|
||||
) {
|
||||
return (
|
||||
<Column noMargin>
|
||||
{header}
|
||||
<PlaceholderLoader style={{ minHeight: 205 }} />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line alignItems="center">
|
||||
<Column noMargin>
|
||||
<Text size="block-title">
|
||||
<Trans>Subscriptions</Trans>
|
||||
</Text>
|
||||
<Text size="body" noMargin>
|
||||
<Trans>
|
||||
Publish to Android, iOS, unlock more cloud projects, leaderboards,
|
||||
collaboration features and more online services.{' '}
|
||||
<Link
|
||||
href="https://gdevelop.io/pricing#feature-comparison"
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
'https://gdevelop.io/pricing#feature-comparison'
|
||||
)
|
||||
}
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
{header}
|
||||
{userSubscriptionPlanWithPricingSystems &&
|
||||
userSubscriptionPlanWithPricingSystems.id &&
|
||||
!isSubscriptionExpired ? (
|
||||
@@ -401,7 +423,12 @@ const SubscriptionDetails = ({
|
||||
) : (
|
||||
<ResponsiveLineStackLayout noColumnMargin noResponsiveLandscape>
|
||||
{Object.keys(subscriptionOptions).map(key => {
|
||||
const { title, description, icon } = subscriptionOptions[key];
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
buttonColor,
|
||||
} = subscriptionOptions[key];
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -433,7 +460,7 @@ const SubscriptionDetails = ({
|
||||
</Column>
|
||||
<Spacer />
|
||||
<RaisedButton
|
||||
primary
|
||||
color={buttonColor}
|
||||
onClick={() =>
|
||||
openSubscriptionDialog({
|
||||
analyticsMetadata: { reason: 'Consult profile' },
|
||||
|
@@ -20,7 +20,6 @@ import {
|
||||
hasSubscriptionBeenManuallyAdded,
|
||||
isSubscriptionComingFromTeam,
|
||||
} from '../../Utils/GDevelopServices/Usage';
|
||||
import EmptyMessage from '../../UI/EmptyMessage';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import {
|
||||
sendSubscriptionDialogShown,
|
||||
@@ -406,9 +405,13 @@ export default function SubscriptionDialog({
|
||||
.filter(Boolean)
|
||||
.filter(plan => {
|
||||
if (filter === 'individual') {
|
||||
return ['free', 'gdevelop_silver', 'gdevelop_gold'].includes(
|
||||
plan.id
|
||||
);
|
||||
if (isPlanValid) {
|
||||
return ['free', 'gdevelop_silver', 'gdevelop_gold'].includes(
|
||||
plan.id
|
||||
);
|
||||
} else {
|
||||
return ['gdevelop_silver', 'gdevelop_gold'].includes(plan.id);
|
||||
}
|
||||
}
|
||||
if (filter === 'team') {
|
||||
return ['gdevelop_startup', 'gdevelop_enterprise'].includes(
|
||||
@@ -419,13 +422,15 @@ export default function SubscriptionDialog({
|
||||
return ['gdevelop_education'].includes(plan.id);
|
||||
}
|
||||
|
||||
return plan.id !== 'gdevelop_education';
|
||||
return !['gdevelop_education', 'free'].includes(plan.id);
|
||||
})
|
||||
: null;
|
||||
|
||||
const dialogMaxWidth =
|
||||
!displayedSubscriptionPlanWithPricingSystems ||
|
||||
displayedSubscriptionPlanWithPricingSystems.length === 1
|
||||
? 'sm'
|
||||
: displayedSubscriptionPlanWithPricingSystems.length < 3
|
||||
? 'md'
|
||||
: displayedSubscriptionPlanWithPricingSystems.length < 4
|
||||
? 'lg'
|
||||
@@ -463,101 +468,92 @@ export default function SubscriptionDialog({
|
||||
/>,
|
||||
]}
|
||||
open={open}
|
||||
fixedContent={
|
||||
<>
|
||||
{hasValidSubscriptionPlan(authenticatedUser.subscription) &&
|
||||
userSubscriptionPlanWithPricingSystems && (
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>Your plan:</Trans>
|
||||
</Text>
|
||||
<Paper
|
||||
background="medium"
|
||||
variant="outlined"
|
||||
style={styles.currentPlanPaper}
|
||||
>
|
||||
<Line
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
<Line alignItems="center" noMargin>
|
||||
{getPlanIcon({
|
||||
subscriptionPlan: userSubscriptionPlanWithPricingSystems,
|
||||
logoSize: 20,
|
||||
})}
|
||||
<Text size="block-title">
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
userSubscriptionPlanWithPricingSystems.nameByLocale
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
{!hasSubscriptionBeenManuallyAdded(
|
||||
authenticatedUser.subscription
|
||||
) &&
|
||||
!isSubscriptionComingFromTeam(
|
||||
authenticatedUser.subscription
|
||||
) &&
|
||||
!willCancelAtPeriodEnd &&
|
||||
userPricingSystemId !== 'REDEMPTION_CODE' && (
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>Cancel subscription</Trans>}
|
||||
onClick={() =>
|
||||
buyUpdateOrCancelPlan(i18n, null)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</Paper>
|
||||
</Column>
|
||||
)}
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<Text size="block-title">
|
||||
<Trans>Subscription plans</Trans>
|
||||
</Text>
|
||||
<TwoStatesButton
|
||||
value={period}
|
||||
leftButton={{
|
||||
label: <Trans>Monthly</Trans>,
|
||||
value: 'month',
|
||||
}}
|
||||
rightButton={{
|
||||
label: <Trans>Yearly</Trans>,
|
||||
value: 'year',
|
||||
}}
|
||||
// $FlowIgnore
|
||||
onChange={setPeriod}
|
||||
/>
|
||||
</Line>
|
||||
{period !== 'year' && maximumDiscount > 0 && (
|
||||
<HotMessage
|
||||
title={
|
||||
<Trans>
|
||||
Up to {maximumDiscount.toFixed(0)}% discount
|
||||
</Trans>
|
||||
}
|
||||
message={
|
||||
<Trans>
|
||||
Get a yearly subscription and enjoy discounts up to
|
||||
{maximumDiscount.toFixed(0)}%!
|
||||
</Trans>
|
||||
}
|
||||
onClickRightButton={() => setPeriod('year')}
|
||||
rightButtonLabel={
|
||||
isMobile ? (
|
||||
<Trans>Check out</Trans>
|
||||
) : (
|
||||
<Trans>See yearly subs</Trans>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{isPlanValid && userSubscriptionPlanWithPricingSystems && (
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>Your plan:</Trans>
|
||||
</Text>
|
||||
<Paper
|
||||
background="medium"
|
||||
variant="outlined"
|
||||
style={styles.currentPlanPaper}
|
||||
>
|
||||
<Line
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
<Line alignItems="center" noMargin>
|
||||
{getPlanIcon({
|
||||
subscriptionPlan: userSubscriptionPlanWithPricingSystems,
|
||||
logoSize: 20,
|
||||
})}
|
||||
<Text size="block-title">
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
userSubscriptionPlanWithPricingSystems.nameByLocale
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
{!hasSubscriptionBeenManuallyAdded(
|
||||
authenticatedUser.subscription
|
||||
) &&
|
||||
!isSubscriptionComingFromTeam(
|
||||
authenticatedUser.subscription
|
||||
) &&
|
||||
!willCancelAtPeriodEnd &&
|
||||
userPricingSystemId !== 'REDEMPTION_CODE' && (
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>Cancel subscription</Trans>}
|
||||
onClick={() => buyUpdateOrCancelPlan(i18n, null)}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</Paper>
|
||||
</Column>
|
||||
)}
|
||||
<ColumnStackLayout noMargin>
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<Text size="block-title">
|
||||
<Trans>Get GDevelop Premium</Trans>
|
||||
</Text>
|
||||
<TwoStatesButton
|
||||
value={period}
|
||||
leftButton={{
|
||||
label: <Trans>Monthly</Trans>,
|
||||
value: 'month',
|
||||
}}
|
||||
rightButton={{
|
||||
label: <Trans>Yearly</Trans>,
|
||||
value: 'year',
|
||||
}}
|
||||
// $FlowIgnore
|
||||
onChange={setPeriod}
|
||||
/>
|
||||
</Line>
|
||||
{period !== 'year' && maximumDiscount > 0 && (
|
||||
<HotMessage
|
||||
title={
|
||||
<Trans>Up to {maximumDiscount.toFixed(0)}% discount</Trans>
|
||||
}
|
||||
message={
|
||||
<Trans>
|
||||
Get a yearly subscription and enjoy discounts up to
|
||||
{maximumDiscount.toFixed(0)}%!
|
||||
</Trans>
|
||||
}
|
||||
onClickRightButton={() => setPeriod('year')}
|
||||
rightButtonLabel={
|
||||
isMobile ? (
|
||||
<Trans>Check out</Trans>
|
||||
) : (
|
||||
<Trans>See yearly subs</Trans>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{willCancelAtPeriodEnd && (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
@@ -733,7 +729,7 @@ export default function SubscriptionDialog({
|
||||
if (pricingSystem) {
|
||||
actions = [
|
||||
<RaisedButton
|
||||
primary
|
||||
color="premium"
|
||||
key="upgrade"
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
@@ -772,23 +768,8 @@ export default function SubscriptionDialog({
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
{getPlanSpecificRequirements(
|
||||
i18n,
|
||||
displayedSubscriptionPlanWithPricingSystems
|
||||
).map(planSpecificRequirements => (
|
||||
<AlertMessage
|
||||
kind="info"
|
||||
key={planSpecificRequirements.substring(0, 25)}
|
||||
>
|
||||
{planSpecificRequirements}
|
||||
</AlertMessage>
|
||||
))}
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
No ties, cancel your subscription anytime. Payments done using
|
||||
Stripe.com and PayPal secure infrastructure.
|
||||
</Trans>
|
||||
<div>
|
||||
<Line noMargin justifyContent="center">
|
||||
<Text size="body" color="secondary">
|
||||
<Trans>
|
||||
Compare all the advantages of the different plans in this{' '}
|
||||
<Link
|
||||
@@ -803,8 +784,31 @@ export default function SubscriptionDialog({
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</div>
|
||||
</EmptyMessage>
|
||||
</Text>
|
||||
</Line>
|
||||
<Column noMargin>
|
||||
<Text size="sub-title">
|
||||
❤️ <Trans>Support What You Love</Trans>
|
||||
</Text>
|
||||
<Text size="body" color="secondary">
|
||||
The GDevelop project is open-source, powered by passion and
|
||||
community. Your membership helps the GDevelop company maintain
|
||||
servers, build new features, develop commercial offerings and
|
||||
keep the open-source project thriving. Our goal: make game
|
||||
development fast, fun and accessible to all.
|
||||
</Text>
|
||||
</Column>
|
||||
{getPlanSpecificRequirements(
|
||||
i18n,
|
||||
displayedSubscriptionPlanWithPricingSystems
|
||||
).map(planSpecificRequirements => (
|
||||
<AlertMessage
|
||||
kind="info"
|
||||
key={planSpecificRequirements.substring(0, 25)}
|
||||
>
|
||||
{planSpecificRequirements}
|
||||
</AlertMessage>
|
||||
))}
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
{hasMobileAppStoreSubscriptionPlan(
|
||||
|
@@ -53,6 +53,7 @@ const EmptyProjectTile = ({
|
||||
onSelectEmptyProject();
|
||||
}
|
||||
}}
|
||||
id="empty-project-tile"
|
||||
>
|
||||
<Column alignItems="center" justifyContent="center" expand>
|
||||
<Add />
|
||||
|
@@ -1,8 +1,9 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
|
||||
import { type FileMetadata, type SaveAsLocation } from '..';
|
||||
import { type FileMetadata, type SaveAsLocation, type SaveAsOptions } from '..';
|
||||
import {
|
||||
CLOUD_PROJECT_NAME_MAX_LENGTH,
|
||||
commitVersion,
|
||||
createCloudProject,
|
||||
getCredentialsForCloudProject,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
import type { $AxiosError } from 'axios';
|
||||
import type { MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
|
||||
import { serializeToJSON } from '../../Utils/Serializer';
|
||||
import CloudSaveAsDialog from './CloudSaveAsDialog';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
createZipWithSingleTextFile,
|
||||
@@ -21,6 +21,7 @@ import ProjectCache from '../../Utils/ProjectCache';
|
||||
import { getProjectCache } from './CloudProjectOpener';
|
||||
import { retryIfFailed } from '../../Utils/RetryIfFailed';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
|
||||
import SaveAsOptionsDialog from '../SaveAsOptionsDialog';
|
||||
|
||||
const zipProject = async (project: gdProject): Promise<[Blob, string]> => {
|
||||
const projectJson = serializeToJSON(project);
|
||||
@@ -184,38 +185,49 @@ export const generateOnChooseSaveProjectAsLocation = ({
|
||||
|}) => async ({
|
||||
project,
|
||||
fileMetadata,
|
||||
displayOptionToGenerateNewProjectUuid,
|
||||
}: {|
|
||||
project: gdProject,
|
||||
fileMetadata: ?FileMetadata,
|
||||
displayOptionToGenerateNewProjectUuid: boolean,
|
||||
|}): Promise<{|
|
||||
saveAsLocation: ?SaveAsLocation,
|
||||
saveAsOptions: ?SaveAsOptions,
|
||||
|}> => {
|
||||
if (!authenticatedUser.authenticated) {
|
||||
return { saveAsLocation: null };
|
||||
return { saveAsLocation: null, saveAsOptions: null };
|
||||
}
|
||||
|
||||
const name = await new Promise(resolve => {
|
||||
const options = await new Promise(resolve => {
|
||||
setDialog(() => (
|
||||
<CloudSaveAsDialog
|
||||
<SaveAsOptionsDialog
|
||||
onCancel={() => {
|
||||
closeDialog();
|
||||
resolve(null);
|
||||
}}
|
||||
nameSuggestion={project.getName()}
|
||||
onSave={(newName: string) => {
|
||||
nameMaxLength={CLOUD_PROJECT_NAME_MAX_LENGTH}
|
||||
nameSuggestion={
|
||||
fileMetadata ? `${project.getName()} - Copy` : project.getName()
|
||||
}
|
||||
displayOptionToGenerateNewProjectUuid={
|
||||
displayOptionToGenerateNewProjectUuid
|
||||
}
|
||||
onSave={options => {
|
||||
closeDialog();
|
||||
resolve(newName);
|
||||
resolve(options);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
});
|
||||
|
||||
if (!name) return { saveAsLocation: null }; // Save was cancelled.
|
||||
if (!options) return { saveAsLocation: null, saveAsOptions: null }; // Save was cancelled.
|
||||
|
||||
return {
|
||||
saveAsLocation: {
|
||||
name,
|
||||
gameId: project.getProjectUuid(),
|
||||
name: options.name,
|
||||
},
|
||||
saveAsOptions: {
|
||||
generateNewProjectUuid: options.generateNewProjectUuid,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -243,7 +255,7 @@ export const generateOnSaveProjectAs = (
|
||||
}
|
||||
options.onStartSaving();
|
||||
|
||||
const gameId = saveAsLocation.gameId || project.getProjectUuid();
|
||||
const gameId = project.getProjectUuid();
|
||||
|
||||
try {
|
||||
// Create a new cloud project.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user