mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
19 Commits
experiment
...
network-sy
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0a182346fa | ||
![]() |
bb6eb01153 | ||
![]() |
9fb086dcdf | ||
![]() |
7e60a0246e | ||
![]() |
3cb2da3de5 | ||
![]() |
ef23470a00 | ||
![]() |
196ea5e480 | ||
![]() |
e732f1952c | ||
![]() |
f5f024cc42 | ||
![]() |
6a3df62598 | ||
![]() |
75f049d911 | ||
![]() |
4d0ac6f355 | ||
![]() |
00a5c93b35 | ||
![]() |
a90cc83967 | ||
![]() |
87a5934df3 | ||
![]() |
d0245b8f1a | ||
![]() |
45d73df6fb | ||
![]() |
7ac600e92d | ||
![]() |
7ba8d0133e |
@@ -1518,7 +1518,7 @@ void WholeProjectRefactorer::DoRenameObject(
|
||||
projectBrowser.ExposeFunctions(project, objectParameterRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::ObjectRemovedInLayout(
|
||||
void WholeProjectRefactorer::ObjectRemovedInScene(
|
||||
gd::Project &project, gd::Layout &layout, const gd::String &objectName) {
|
||||
auto projectScopedContainers = gd::ProjectScopedContainers::
|
||||
MakeNewProjectScopedContainersForProjectAndLayout(project, layout);
|
||||
@@ -1540,7 +1540,7 @@ void WholeProjectRefactorer::ObjectRemovedInLayout(
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::BehaviorsAddedToObjectInLayout(
|
||||
void WholeProjectRefactorer::BehaviorsAddedToObjectInScene(
|
||||
gd::Project &project, gd::Layout &layout, const gd::String &objectName) {
|
||||
auto projectScopedContainers = gd::ProjectScopedContainers::
|
||||
MakeNewProjectScopedContainersForProjectAndLayout(project, layout);
|
||||
@@ -1550,7 +1550,7 @@ void WholeProjectRefactorer::BehaviorsAddedToObjectInLayout(
|
||||
project, layout, behaviorParameterFiller);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
|
||||
void WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
|
||||
gd::Project &project, gd::Layout &layout, const gd::String &oldName,
|
||||
const gd::String &newName, bool isObjectGroup) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
@@ -1641,81 +1641,150 @@ void WholeProjectRefactorer::RenameExternalEvents(gd::Project &project,
|
||||
linkEventTargetRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameLayer(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName) {
|
||||
void WholeProjectRefactorer::RenameLayerInScene(gd::Project &project,
|
||||
gd::Layout &scene,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
|
||||
gd::ProjectElementRenamer projectElementRenamer(project.GetCurrentPlatform(),
|
||||
"layer", oldName, newName);
|
||||
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
|
||||
project, layout, projectElementRenamer);
|
||||
layout.GetInitialInstances().MoveInstancesToLayer(oldName, newName);
|
||||
project, scene, projectElementRenamer);
|
||||
scene.GetInitialInstances().MoveInstancesToLayer(oldName, newName);
|
||||
|
||||
std::vector<gd::String> externalLayoutsNames =
|
||||
GetAssociatedExternalLayouts(project, layout);
|
||||
GetAssociatedExternalLayouts(project, scene);
|
||||
for (gd::String name : externalLayoutsNames) {
|
||||
auto &externalLayout = project.GetExternalLayout(name);
|
||||
externalLayout.GetInitialInstances().MoveInstancesToLayer(oldName, newName);
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameLayerEffect(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
gd::Layer &layer,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName) {
|
||||
void WholeProjectRefactorer::RenameLayerInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, const gd::String &oldName,
|
||||
const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
|
||||
gd::ProjectElementRenamer projectElementRenamer(project.GetCurrentPlatform(),
|
||||
"layer", oldName, newName);
|
||||
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
|
||||
project, eventsFunctionsExtension, eventsBasedObject,
|
||||
projectElementRenamer);
|
||||
eventsBasedObject.GetInitialInstances().MoveInstancesToLayer(oldName,
|
||||
newName);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameLayerEffectInScene(
|
||||
gd::Project &project, gd::Layout &scene, gd::Layer &layer,
|
||||
const gd::String &oldName, const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
gd::ProjectElementRenamer projectElementRenamer(
|
||||
project.GetCurrentPlatform(), "layerEffectName", oldName, newName);
|
||||
projectElementRenamer.SetLayerConstraint(layer.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
|
||||
project, layout, projectElementRenamer);
|
||||
project, scene, projectElementRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameObjectAnimation(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
gd::Object &object,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName) {
|
||||
void WholeProjectRefactorer::RenameLayerEffectInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, gd::Layer &layer,
|
||||
const gd::String &oldName, const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
gd::ProjectElementRenamer projectElementRenamer(
|
||||
project.GetCurrentPlatform(), "layerEffectName", oldName, newName);
|
||||
projectElementRenamer.SetLayerConstraint(layer.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
|
||||
project, eventsFunctionsExtension, eventsBasedObject,
|
||||
projectElementRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameObjectAnimationInScene(
|
||||
gd::Project &project, gd::Layout &scene, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
gd::ProjectElementRenamer projectElementRenamer(
|
||||
project.GetCurrentPlatform(), "objectAnimationName", oldName, newName);
|
||||
projectElementRenamer.SetObjectConstraint(object.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
|
||||
project, layout, projectElementRenamer);
|
||||
project, scene, projectElementRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameObjectPoint(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
gd::Object &object,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName) {
|
||||
void WholeProjectRefactorer::RenameObjectAnimationInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
gd::ProjectElementRenamer projectElementRenamer(
|
||||
project.GetCurrentPlatform(), "objectAnimationName", oldName, newName);
|
||||
projectElementRenamer.SetObjectConstraint(object.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
|
||||
project, eventsFunctionsExtension, eventsBasedObject,
|
||||
projectElementRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameObjectPointInScene(
|
||||
gd::Project &project, gd::Layout &scene, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
gd::ProjectElementRenamer projectElementRenamer(
|
||||
project.GetCurrentPlatform(), "objectPointName", oldName, newName);
|
||||
projectElementRenamer.SetObjectConstraint(object.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
|
||||
project, layout, projectElementRenamer);
|
||||
project, scene, projectElementRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameObjectEffect(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
gd::Object &object,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName) {
|
||||
void WholeProjectRefactorer::RenameObjectPointInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
gd::ProjectElementRenamer projectElementRenamer(
|
||||
project.GetCurrentPlatform(), "objectPointName", oldName, newName);
|
||||
projectElementRenamer.SetObjectConstraint(object.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
|
||||
project, eventsFunctionsExtension, eventsBasedObject,
|
||||
projectElementRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameObjectEffectInScene(
|
||||
gd::Project &project, gd::Layout &scene, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
gd::ProjectElementRenamer projectElementRenamer(
|
||||
project.GetCurrentPlatform(), "objectEffectName", oldName, newName);
|
||||
projectElementRenamer.SetObjectConstraint(object.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
|
||||
project, layout, projectElementRenamer);
|
||||
project, scene, projectElementRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RenameObjectEffectInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName) {
|
||||
if (oldName == newName || newName.empty() || oldName.empty())
|
||||
return;
|
||||
gd::ProjectElementRenamer projectElementRenamer(
|
||||
project.GetCurrentPlatform(), "objectEffectName", oldName, newName);
|
||||
projectElementRenamer.SetObjectConstraint(object.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
|
||||
project, eventsFunctionsExtension, eventsBasedObject,
|
||||
projectElementRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::ObjectRemovedInEventsBasedObject(
|
||||
@@ -1805,7 +1874,7 @@ void WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
|
||||
if (layout.GetObjects().HasObjectNamed(oldName))
|
||||
continue;
|
||||
|
||||
ObjectOrGroupRenamedInLayout(project, layout, oldName, newName,
|
||||
ObjectOrGroupRenamedInScene(project, layout, oldName, newName,
|
||||
isObjectGroup);
|
||||
}
|
||||
}
|
||||
@@ -1822,7 +1891,7 @@ void WholeProjectRefactorer::GlobalObjectRemoved(
|
||||
if (layout.GetObjects().HasObjectNamed(objectName))
|
||||
continue;
|
||||
|
||||
ObjectRemovedInLayout(project, layout, objectName);
|
||||
ObjectRemovedInScene(project, layout, objectName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1833,7 +1902,7 @@ void WholeProjectRefactorer::BehaviorsAddedToGlobalObject(
|
||||
if (layout.GetObjects().HasObjectNamed(objectName))
|
||||
continue;
|
||||
|
||||
BehaviorsAddedToObjectInLayout(project, layout, objectName);
|
||||
BehaviorsAddedToObjectInScene(project, layout, objectName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -68,7 +68,7 @@ struct VariablesChangeset : VariablesRenamingChangesetNode {
|
||||
* \brief Tool functions to do refactoring on the whole project after
|
||||
* changes like deletion or renaming of an object.
|
||||
*
|
||||
* \TODO Ideally ObjectOrGroupRenamedInLayout, ObjectRemovedInLayout,
|
||||
* \TODO Ideally ObjectOrGroupRenamedInScene, ObjectRemovedInScene,
|
||||
* GlobalObjectOrGroupRenamed, GlobalObjectRemoved would be implemented
|
||||
* using ExposeProjectEvents.
|
||||
*/
|
||||
@@ -339,40 +339,91 @@ class GD_CORE_API WholeProjectRefactorer {
|
||||
static void RenameExternalEvents(gd::Project &project,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after a layer is renamed.
|
||||
*/
|
||||
static void RenameLayer(gd::Project &project, gd::Layout &layout,
|
||||
const gd::String &oldName, const gd::String &newName);
|
||||
static void RenameLayerInScene(gd::Project &project, gd::Layout &scene,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after a layer is renamed.
|
||||
*/
|
||||
static void RenameLayerInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after a layer effect is renamed.
|
||||
*/
|
||||
static void RenameLayerEffect(gd::Project &project, gd::Layout &layout,
|
||||
gd::Layer &layer, const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
static void RenameLayerEffectInScene(gd::Project &project, gd::Layout &scene,
|
||||
gd::Layer &layer,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after a layer effect is renamed.
|
||||
*/
|
||||
static void RenameLayerEffectInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, gd::Layer &layer,
|
||||
const gd::String &oldName, const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object animation is renamed.
|
||||
*/
|
||||
static void RenameObjectAnimation(gd::Project &project, gd::Layout &layout,
|
||||
gd::Object &object,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
static void RenameObjectAnimationInScene(gd::Project &project,
|
||||
gd::Layout &scene,
|
||||
gd::Object &object,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object animation is renamed.
|
||||
*/
|
||||
static void RenameObjectAnimationInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object point is renamed.
|
||||
*/
|
||||
static void RenameObjectPoint(gd::Project &project, gd::Layout &layout,
|
||||
gd::Object &object, const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
static void RenameObjectPointInScene(gd::Project &project, gd::Layout &scene,
|
||||
gd::Object &object,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object point is renamed.
|
||||
*/
|
||||
static void RenameObjectPointInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object effect is renamed.
|
||||
*/
|
||||
static void RenameObjectEffect(gd::Project &project, gd::Layout &layout,
|
||||
gd::Object &object, const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
static void RenameObjectEffectInScene(gd::Project &project, gd::Layout &scene,
|
||||
gd::Object &object,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object effect is renamed.
|
||||
*/
|
||||
static void RenameObjectEffectInEventsBasedObject(
|
||||
gd::Project &project,
|
||||
gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
gd::EventsBasedObject &eventsBasedObject, gd::Object &object,
|
||||
const gd::String &oldName, const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object is renamed in a layout
|
||||
@@ -380,11 +431,11 @@ class GD_CORE_API WholeProjectRefactorer {
|
||||
* This will update the layout, all external layouts associated with it
|
||||
* and all external events associated with it.
|
||||
*/
|
||||
static void ObjectOrGroupRenamedInLayout(gd::Project& project,
|
||||
gd::Layout& layout,
|
||||
const gd::String& oldName,
|
||||
const gd::String& newName,
|
||||
bool isObjectGroup);
|
||||
static void ObjectOrGroupRenamedInScene(gd::Project &project,
|
||||
gd::Layout &scene,
|
||||
const gd::String &oldName,
|
||||
const gd::String &newName,
|
||||
bool isObjectGroup);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object is removed in a layout
|
||||
@@ -392,9 +443,8 @@ class GD_CORE_API WholeProjectRefactorer {
|
||||
* This will update the layout, all external layouts associated with it
|
||||
* and all external events associated with it.
|
||||
*/
|
||||
static void ObjectRemovedInLayout(gd::Project& project,
|
||||
gd::Layout& layout,
|
||||
const gd::String& objectName);
|
||||
static void ObjectRemovedInScene(gd::Project &project, gd::Layout &scene,
|
||||
const gd::String &objectName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after behaviors are added to an object in a
|
||||
@@ -404,9 +454,9 @@ class GD_CORE_API WholeProjectRefactorer {
|
||||
* The refactor is actually applied to all objects because it allow to handle
|
||||
* groups.
|
||||
*/
|
||||
static void BehaviorsAddedToObjectInLayout(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
const gd::String &objectName);
|
||||
static void BehaviorsAddedToObjectInScene(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
const gd::String &objectName);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project after an object is removed in an events-based
|
||||
|
@@ -1228,7 +1228,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
layout1.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "Object2", 0);
|
||||
|
||||
gd::WholeProjectRefactorer::ObjectRemovedInLayout(project, layout1,
|
||||
gd::WholeProjectRefactorer::ObjectRemovedInScene(project, layout1,
|
||||
"Object1");
|
||||
gd::WholeProjectRefactorer::GlobalObjectRemoved(project, "GlobalObject1");
|
||||
REQUIRE(layout1.GetObjects().GetObjectGroups()[0].Find(
|
||||
@@ -1262,7 +1262,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
layout1.GetInitialInstances().InsertInitialInstance(instance2);
|
||||
layout1.GetInitialInstances().InsertInitialInstance(instance3);
|
||||
|
||||
gd::WholeProjectRefactorer::ObjectRemovedInLayout(
|
||||
gd::WholeProjectRefactorer::ObjectRemovedInScene(
|
||||
project, layout1, "Object1");
|
||||
gd::WholeProjectRefactorer::GlobalObjectRemoved(
|
||||
project, "GlobalObject1");
|
||||
@@ -1306,7 +1306,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
externalLayout2.GetInitialInstances().InsertInitialInstance(instance2);
|
||||
externalLayout2.GetInitialInstances().InsertInitialInstance(instance3);
|
||||
|
||||
gd::WholeProjectRefactorer::ObjectRemovedInLayout(
|
||||
gd::WholeProjectRefactorer::ObjectRemovedInScene(
|
||||
project, layout1, "Object1");
|
||||
gd::WholeProjectRefactorer::GlobalObjectRemoved(
|
||||
project, "GlobalObject1");
|
||||
@@ -1344,7 +1344,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
layout1.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "Object2", 0);
|
||||
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
|
||||
project, layout1, "Object1", "Object3", /* isObjectGroup =*/false);
|
||||
gd::WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
|
||||
project, "GlobalObject1", "GlobalObject3", /* isObjectGroup =*/false);
|
||||
@@ -1381,7 +1381,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
layout1.GetInitialInstances().InsertInitialInstance(instance2);
|
||||
layout1.GetInitialInstances().InsertInitialInstance(instance3);
|
||||
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
|
||||
project, layout1, "Object1", "Object3", /* isObjectGroup =*/false);
|
||||
gd::WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
|
||||
project, "GlobalObject1", "GlobalObject3", /* isObjectGroup =*/false);
|
||||
@@ -1427,7 +1427,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
externalLayout2.GetInitialInstances().InsertInitialInstance(instance2);
|
||||
externalLayout2.GetInitialInstances().InsertInitialInstance(instance3);
|
||||
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
|
||||
project, layout1, "Object1", "Object3", /* isObjectGroup =*/false);
|
||||
gd::WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
|
||||
project, "GlobalObject1", "GlobalObject3", /* isObjectGroup =*/false);
|
||||
@@ -1462,7 +1462,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
auto &layout = project.GetLayout("Scene");
|
||||
|
||||
// Trigger the refactoring after the renaming of an object
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
|
||||
project, layout, "ObjectWithMyBehavior",
|
||||
"RenamedObjectWithMyBehavior",
|
||||
/* isObjectGroup=*/false);
|
||||
@@ -1490,7 +1490,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
auto &layout = project.GetLayout("Scene");
|
||||
|
||||
// Trigger the refactoring after the renaming of a group
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
|
||||
gd::WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
|
||||
project, layout, "GroupWithMyBehavior", "RenamedGroupWithMyBehavior",
|
||||
/* isObjectGroup=*/true);
|
||||
|
||||
@@ -1534,7 +1534,7 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
|
||||
|
||||
// Attach the behavior to the object.
|
||||
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::WholeProjectRefactorer::BehaviorsAddedToObjectInLayout(project, scene,
|
||||
gd::WholeProjectRefactorer::BehaviorsAddedToObjectInScene(project, scene,
|
||||
"Object");
|
||||
|
||||
// The behavior parameter is now filled.
|
||||
@@ -3584,7 +3584,7 @@ CreateExpressionWithLayerParameter(gd::Project &project,
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("RenameLayer", "[common]") {
|
||||
SECTION("Can update layer names in events") {
|
||||
SECTION("Can update layer names in scene events") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
@@ -3616,7 +3616,7 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
auto &otherExternalExpression = CreateExpressionWithLayerParameter(
|
||||
project, otherExternalEvents.GetEvents(), "My layer");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer",
|
||||
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer",
|
||||
"My renamed layer");
|
||||
|
||||
REQUIRE(layoutAction.GetParameter(3).GetPlainString() ==
|
||||
@@ -3644,6 +3644,38 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
"MyExtension::CameraCenterX(\"My layer\")");
|
||||
}
|
||||
|
||||
SECTION("Can update layer names in event-based object events") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
auto &eventsExtension =
|
||||
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
|
||||
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
|
||||
"MyEventsBasedObject", 0);
|
||||
auto &eventsFunction =
|
||||
eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
|
||||
"MyEventsFunction", 0);
|
||||
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
|
||||
eventsExtension, eventsBasedObject);
|
||||
|
||||
auto &action =
|
||||
CreateActionWithLayerParameter(project, eventsFunction.GetEvents());
|
||||
|
||||
auto &expression = CreateExpressionWithLayerParameter(
|
||||
project, eventsFunction.GetEvents(), "My layer");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayerInEventsBasedObject(
|
||||
project, eventsExtension, eventsBasedObject, "My layer",
|
||||
"My renamed layer");
|
||||
|
||||
REQUIRE(action.GetParameter(3).GetPlainString() == "\"My renamed layer\"");
|
||||
|
||||
REQUIRE(expression.GetParameter(0).GetPlainString() ==
|
||||
"MyExtension::CameraCenterX(\"My renamed layer\") + "
|
||||
"MyExtension::CameraCenterX(\"My renamed layer\")");
|
||||
}
|
||||
|
||||
SECTION("Can update layer names in expressions with a smaller name") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
@@ -3654,7 +3686,7 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
auto &layoutExpression =
|
||||
CreateExpressionWithLayerParameter(project, layout.GetEvents(), "My layer");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer",
|
||||
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer",
|
||||
"layerA");
|
||||
|
||||
REQUIRE(layoutExpression.GetParameter(0).GetPlainString() ==
|
||||
@@ -3662,7 +3694,7 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
"MyExtension::CameraCenterX(\"layerA\")");
|
||||
}
|
||||
|
||||
SECTION("Renaming a layer also moves the instances on this layer and of the associated external layouts") {
|
||||
SECTION("Renaming a layer also moves the instances on this layer in its scene and associated external layouts") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
@@ -3713,7 +3745,7 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
REQUIRE(otherInitialInstance1.GetLayer() == "My layer");
|
||||
REQUIRE(otherExternalInitialInstance1.GetLayer() == "My layer");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer", "My new layer");
|
||||
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer", "My new layer");
|
||||
|
||||
// Instances on the renamed layer are moved to the new layer.
|
||||
REQUIRE(initialInstance1.GetLayer() == "My new layer");
|
||||
@@ -3728,6 +3760,46 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
REQUIRE(otherExternalInitialInstance1.GetLayer() == "My layer");
|
||||
}
|
||||
|
||||
SECTION("Renaming a layer also moves the instances on this layer in its "
|
||||
"event-based object") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
auto &eventsExtension =
|
||||
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
|
||||
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
|
||||
"MyEventsBasedObject", 0);
|
||||
auto &eventsFunction =
|
||||
eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
|
||||
"MyEventsFunction", 0);
|
||||
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
|
||||
eventsExtension, eventsBasedObject);
|
||||
|
||||
eventsBasedObject.GetLayers().InsertNewLayer("My layer", 0);
|
||||
|
||||
auto &initialInstances = eventsBasedObject.GetInitialInstances();
|
||||
auto &initialInstance1 = initialInstances.InsertNewInitialInstance();
|
||||
initialInstance1.SetLayer("My layer");
|
||||
auto &initialInstance2 = initialInstances.InsertNewInitialInstance();
|
||||
initialInstance2.SetLayer("My layer");
|
||||
auto &initialInstance3 = initialInstances.InsertNewInitialInstance();
|
||||
initialInstance3.SetLayer("");
|
||||
|
||||
REQUIRE(initialInstance1.GetLayer() == "My layer");
|
||||
REQUIRE(initialInstance2.GetLayer() == "My layer");
|
||||
REQUIRE(initialInstance3.GetLayer() == "");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayerInEventsBasedObject(
|
||||
project, eventsExtension, eventsBasedObject, "My layer",
|
||||
"My new layer");
|
||||
|
||||
// Instances on the renamed layer are moved to the new layer.
|
||||
REQUIRE(initialInstance1.GetLayer() == "My new layer");
|
||||
REQUIRE(initialInstance2.GetLayer() == "My new layer");
|
||||
REQUIRE(initialInstance3.GetLayer() == "");
|
||||
}
|
||||
|
||||
SECTION("Can rename a layer when a layer parameter is empty") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
@@ -3738,7 +3810,7 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
auto &layoutAction =
|
||||
CreateActionWithEmptyLayerParameter(project, layout.GetEvents());
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer",
|
||||
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer",
|
||||
"layerA");
|
||||
|
||||
REQUIRE(layoutAction.GetParameter(0).GetPlainString() == "");
|
||||
@@ -3754,7 +3826,7 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
auto &layoutExpression =
|
||||
CreateExpressionWithLayerParameter(project, layout.GetEvents(), "My layer");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer",
|
||||
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "My layer",
|
||||
"");
|
||||
|
||||
REQUIRE(layoutExpression.GetParameter(0).GetPlainString() ==
|
||||
@@ -3772,7 +3844,7 @@ TEST_CASE("RenameLayer", "[common]") {
|
||||
auto &layoutExpression =
|
||||
CreateExpressionWithLayerParameter(project, layout.GetEvents(), "");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayer(project, layout, "", "My layer");
|
||||
gd::WholeProjectRefactorer::RenameLayerInScene(project, layout, "", "My layer");
|
||||
|
||||
REQUIRE(layoutExpression.GetParameter(0).GetPlainString() ==
|
||||
"MyExtension::CameraCenterX(\"\") + "
|
||||
@@ -3813,7 +3885,7 @@ CreateExpressionWithAnimationParameter(gd::Project &project,
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("RenameObjectAnimation", "[common]") {
|
||||
SECTION("Can update object animation names in event") {
|
||||
SECTION("Can update object animation names in scene events") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
@@ -3855,7 +3927,7 @@ TEST_CASE("RenameObjectAnimation", "[common]") {
|
||||
auto &wrongObjectExpression =
|
||||
CreateExpressionWithAnimationParameter(project, layout.GetEvents(), "MySprite2");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameObjectAnimation(project, layout, object, "My animation",
|
||||
gd::WholeProjectRefactorer::RenameObjectAnimationInScene(project, layout, object, "My animation",
|
||||
"My renamed animation");
|
||||
|
||||
REQUIRE(layoutAction.GetParameter(1).GetPlainString() ==
|
||||
@@ -3887,6 +3959,53 @@ TEST_CASE("RenameObjectAnimation", "[common]") {
|
||||
"MySprite2.AnimationFrameCount(\"My animation\") + "
|
||||
"MySprite2.AnimationFrameCount(\"My animation\")");
|
||||
}
|
||||
|
||||
SECTION("Can update object animation names in events-based object events") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
auto &eventsExtension =
|
||||
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
|
||||
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
|
||||
"MyEventsBasedObject", 0);
|
||||
auto &eventsFunction =
|
||||
eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
|
||||
"MyEventsFunction", 0);
|
||||
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
|
||||
eventsExtension, eventsBasedObject);
|
||||
|
||||
auto &object = eventsBasedObject.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MySprite", 0);
|
||||
eventsBasedObject.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MySprite2", 1);
|
||||
|
||||
auto &action = CreateActionWithAnimationParameter(
|
||||
project, eventsFunction.GetEvents(), "MySprite");
|
||||
auto &wrongObjectAction = CreateActionWithAnimationParameter(
|
||||
project, eventsFunction.GetEvents(), "MySprite2");
|
||||
|
||||
auto &expression = CreateExpressionWithAnimationParameter(
|
||||
project, eventsFunction.GetEvents(), "MySprite");
|
||||
auto &wrongObjectExpression = CreateExpressionWithAnimationParameter(
|
||||
project, eventsFunction.GetEvents(), "MySprite2");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameObjectAnimationInEventsBasedObject(
|
||||
project, eventsExtension, eventsBasedObject, object, "My animation",
|
||||
"My renamed animation");
|
||||
|
||||
REQUIRE(action.GetParameter(1).GetPlainString() ==
|
||||
"\"My renamed animation\"");
|
||||
REQUIRE(wrongObjectAction.GetParameter(1).GetPlainString() ==
|
||||
"\"My animation\"");
|
||||
|
||||
REQUIRE(expression.GetParameter(0).GetPlainString() ==
|
||||
"MySprite.AnimationFrameCount(\"My renamed animation\") + "
|
||||
"MySprite.AnimationFrameCount(\"My renamed animation\")");
|
||||
REQUIRE(wrongObjectExpression.GetParameter(0).GetPlainString() ==
|
||||
"MySprite2.AnimationFrameCount(\"My animation\") + "
|
||||
"MySprite2.AnimationFrameCount(\"My animation\")");
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -3922,7 +4041,7 @@ CreateExpressionWithLayerEffectParameter(gd::Project &project,
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("RenameLayerEffect", "[common]") {
|
||||
SECTION("Can update layer effect names in event") {
|
||||
SECTION("Can update layer effect names in scene events") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
@@ -3966,7 +4085,7 @@ TEST_CASE("RenameLayerEffect", "[common]") {
|
||||
auto &wrongLayerExpression =
|
||||
CreateExpressionWithLayerEffectParameter(project, layout.GetEvents(), "My layer 2");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayerEffect(project, layout, layer, "My effect",
|
||||
gd::WholeProjectRefactorer::RenameLayerEffectInScene(project, layout, layer, "My effect",
|
||||
"My renamed effect");
|
||||
|
||||
REQUIRE(layoutAction.GetParameter(2).GetPlainString() ==
|
||||
@@ -3998,10 +4117,61 @@ TEST_CASE("RenameLayerEffect", "[common]") {
|
||||
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\") + "
|
||||
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\")");
|
||||
}
|
||||
|
||||
SECTION("Can update layer effect names in events-based object events") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
auto &eventsExtension =
|
||||
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
|
||||
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
|
||||
"MyEventsBasedObject", 0);
|
||||
auto &eventsFunction =
|
||||
eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
|
||||
"MyEventsFunction", 0);
|
||||
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
|
||||
eventsExtension, eventsBasedObject);
|
||||
|
||||
eventsBasedObject.GetLayers().InsertNewLayer("My layer", 0);
|
||||
auto &layer = eventsBasedObject.GetLayers().GetLayer("My layer");
|
||||
auto &object = eventsBasedObject.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MySprite", 0);
|
||||
eventsBasedObject.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MySprite2", 1);
|
||||
|
||||
auto &action = CreateActionWithLayerEffectParameter(
|
||||
project, eventsFunction.GetEvents(), "My layer");
|
||||
auto &wrongLayerAction = CreateActionWithLayerEffectParameter(
|
||||
project, eventsFunction.GetEvents(), "My layer 2");
|
||||
|
||||
auto &expression = CreateExpressionWithLayerEffectParameter(
|
||||
project, eventsFunction.GetEvents(), "My layer");
|
||||
auto &wrongLayerExpression = CreateExpressionWithLayerEffectParameter(
|
||||
project, eventsFunction.GetEvents(), "My layer 2");
|
||||
|
||||
gd::WholeProjectRefactorer::RenameLayerEffectInEventsBasedObject(
|
||||
project, eventsExtension, eventsBasedObject, layer, "My effect",
|
||||
"My renamed effect");
|
||||
|
||||
REQUIRE(action.GetParameter(2).GetPlainString() == "\"My renamed effect\"");
|
||||
REQUIRE(wrongLayerAction.GetParameter(2).GetPlainString() ==
|
||||
"\"My effect\"");
|
||||
|
||||
REQUIRE(expression.GetParameter(0).GetPlainString() ==
|
||||
"MyExtension::LayerEffectParameter(\"My layer\", \"My renamed "
|
||||
"effect\") + "
|
||||
"MyExtension::LayerEffectParameter(\"My layer\", \"My renamed "
|
||||
"effect\")");
|
||||
REQUIRE(
|
||||
wrongLayerExpression.GetParameter(0).GetPlainString() ==
|
||||
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\") + "
|
||||
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\")");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RemoveLayer", "[common]") {
|
||||
SECTION("Can remove instances from a layer (in a scene and their associated external layouts)") {
|
||||
SECTION("Can remove instances from a layer (in a scene and its associated external layouts)") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
@@ -4086,7 +4256,7 @@ TEST_CASE("RemoveLayer", "[common]") {
|
||||
}
|
||||
|
||||
TEST_CASE("MergeLayers", "[common]") {
|
||||
SECTION("Can merge instances from a layer into another layer (in a scene and their associated external layouts)") {
|
||||
SECTION("Can merge instances from a layer into another layer (in a scene and its associated external layouts)") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
@@ -38,7 +38,7 @@ module.exports = {
|
||||
.setName('Consent Cordova plugin')
|
||||
.setDependencyType('cordova')
|
||||
.setExportName('cordova-plugin-consent')
|
||||
.setVersion('2.4.0')
|
||||
.setVersion('3.0.0-alpha.8')
|
||||
.onlyIfOtherDependencyIsExported('AdMob Cordova plugin');
|
||||
|
||||
extension
|
||||
@@ -58,7 +58,7 @@ module.exports = {
|
||||
.setName('AdMob Cordova plugin')
|
||||
.setDependencyType('cordova')
|
||||
.setExportName('admob-plus-cordova')
|
||||
.setVersion('1.28.0')
|
||||
.setVersion('2.0.0-alpha.18')
|
||||
.setExtraSetting(
|
||||
'APP_ID_ANDROID',
|
||||
new gd.PropertyDescriptor('AdMobAppIdAndroid').setType(
|
||||
@@ -166,11 +166,11 @@ module.exports = {
|
||||
)
|
||||
.addParameter('string', _('Android app open ID'), '', false)
|
||||
.setParameterLongDescription(
|
||||
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/3419835294"` for loading a test app open.'
|
||||
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/9257395921"` for loading a test app open.'
|
||||
)
|
||||
.addParameter('string', _('iOS app open ID'), '', false)
|
||||
.setParameterLongDescription(
|
||||
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/5662855259"` for loading a test app open.'
|
||||
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/5575463023"` for loading a test app open.'
|
||||
)
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
|
@@ -7,8 +7,8 @@ namespace gdjs {
|
||||
|
||||
const testAdIds = {
|
||||
appOpen: {
|
||||
android: 'ca-app-pub-3940256099942544/3419835294',
|
||||
ios: 'ca-app-pub-3940256099942544/5662855259',
|
||||
android: 'ca-app-pub-3940256099942544/9257395921',
|
||||
ios: 'ca-app-pub-3940256099942544/5575463023',
|
||||
},
|
||||
banner: {
|
||||
android: 'ca-app-pub-3940256099942544/6300978111',
|
||||
@@ -67,6 +67,7 @@ namespace gdjs {
|
||||
|
||||
// Admob does not initialize automatically, so we store a flag to know if it's initialized.
|
||||
let admobStarted = false;
|
||||
let isStarting = false;
|
||||
let isUsingTestAds = false;
|
||||
|
||||
// Banner
|
||||
@@ -115,9 +116,13 @@ namespace gdjs {
|
||||
async () => {
|
||||
// Obtain user consent ?
|
||||
|
||||
logger.info('Starting AdMob.');
|
||||
isStarting = true;
|
||||
|
||||
await admob.start();
|
||||
|
||||
logger.info('AdMob successfully started.');
|
||||
isStarting = false;
|
||||
admobStarted = true;
|
||||
},
|
||||
false
|
||||
@@ -126,15 +131,32 @@ namespace gdjs {
|
||||
/**
|
||||
* Helper to know if we are on mobile and admob is correctly initialized.
|
||||
*/
|
||||
const checkIfAdMobIsAvailable = () => {
|
||||
const checkIfAdMobIsAvailable = async () => {
|
||||
if (typeof cordova === 'undefined') {
|
||||
logger.warn('We are not on mobile, AdMob will not be available.');
|
||||
return false;
|
||||
}
|
||||
if (typeof admob === 'undefined' || !admobStarted) {
|
||||
logger.warn('AdMob has not been configured or started properly.');
|
||||
if (typeof admob === 'undefined') {
|
||||
logger.warn('AdMob has not been configured properly.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!admobStarted) {
|
||||
if (isStarting) {
|
||||
// Delay the call until AdMob is started, up to 5 seconds.
|
||||
let time = 0;
|
||||
while (!admobStarted && time < 5000) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
time += 100;
|
||||
}
|
||||
}
|
||||
|
||||
if (!admobStarted) {
|
||||
logger.warn('AdMob is not started.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -164,8 +186,10 @@ namespace gdjs {
|
||||
* charging advertisers. If you click on too many ads without being in test mode, you risk your
|
||||
* account being flagged for invalid activity.
|
||||
*/
|
||||
export const setTestMode = (enable: boolean) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
export const setTestMode = async (enable: boolean) => {
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
logger.info('Setting AdMob test mode to:', enable);
|
||||
|
||||
isUsingTestAds = enable;
|
||||
};
|
||||
@@ -185,7 +209,7 @@ namespace gdjs {
|
||||
displayLandscape,
|
||||
displayWhenLoaded
|
||||
) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
// If an appOpen is already loading or showing, we don't stop it.
|
||||
if (appOpenLoading || appOpenShowing) {
|
||||
return;
|
||||
@@ -242,7 +266,7 @@ namespace gdjs {
|
||||
|
||||
/** Show the loaded appOpen. */
|
||||
export const showAppOpen = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!appOpen) {
|
||||
logger.warn('App Open has not been set up, call loadAppOpen first.');
|
||||
@@ -293,7 +317,7 @@ namespace gdjs {
|
||||
* If a banner is already set up, it will be hidden and replaced by the new one.
|
||||
*/
|
||||
export const setupBanner = async (androidAdUnitId, iosAdUnitId, atTop) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
const adUnitId = getAdUnitId(androidAdUnitId, iosAdUnitId, 'banner');
|
||||
if (!adUnitId) return;
|
||||
|
||||
@@ -354,7 +378,7 @@ namespace gdjs {
|
||||
|
||||
/** Hide the banner shown on screen. */
|
||||
export const hideBanner = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!banner || !bannerShowing) {
|
||||
logger.warn('No banner is being shown.');
|
||||
@@ -381,7 +405,7 @@ namespace gdjs {
|
||||
iosAdUnitId,
|
||||
displayWhenLoaded
|
||||
) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
// If an interstitial is already loading or showing, we don't stop it.
|
||||
if (interstitialLoading || interstitialShowing) {
|
||||
return;
|
||||
@@ -440,7 +464,7 @@ namespace gdjs {
|
||||
|
||||
/** Show the loaded interstitial. */
|
||||
export const showInterstitial = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!interstitial) {
|
||||
logger.warn(
|
||||
@@ -495,7 +519,7 @@ namespace gdjs {
|
||||
iosAdUnitID,
|
||||
displayWhenLoaded
|
||||
) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
if (rewardedInterstitialLoading || rewardedInterstitialShowing) {
|
||||
return;
|
||||
}
|
||||
@@ -557,7 +581,7 @@ namespace gdjs {
|
||||
|
||||
/** Show the loaded reward interstitial. */
|
||||
export const showRewardedInterstitial = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!rewardedInterstitial) {
|
||||
logger.warn(
|
||||
@@ -614,7 +638,7 @@ namespace gdjs {
|
||||
iosAdUnitID,
|
||||
displayWhenLoaded
|
||||
) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
if (rewardedVideoLoading || rewardedVideoShowing) {
|
||||
return;
|
||||
}
|
||||
@@ -672,7 +696,7 @@ namespace gdjs {
|
||||
|
||||
/** Show the loaded reward video. */
|
||||
export const showRewardedVideo = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!rewardedVideo) {
|
||||
logger.warn('Video has not been set up, call loadRewardedVideo first.');
|
||||
|
@@ -417,7 +417,7 @@ module.exports = {
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerLeft');
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerJustLeft');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
@@ -443,7 +443,108 @@ module.exports = {
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerLeft');
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerJustLeft');
|
||||
|
||||
extension
|
||||
.addExpression(
|
||||
'LastLeftPlayerNumber',
|
||||
_('Last left player number'),
|
||||
_('Returns the number of the player that has just left the lobby.'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName(
|
||||
'gdjs.multiplayerMessageManager.getLatestPlayerWhoJustLeft'
|
||||
);
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'HasAnyPlayerJoined',
|
||||
_('Any player has joined'),
|
||||
_('Check if any player has joined the lobby.'),
|
||||
_('Any player has joined'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerJustJoined');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'HasPlayerJoined',
|
||||
_('Player has joined'),
|
||||
_('Check if the player has joined the lobby.'),
|
||||
_('Player _PARAM0_ has joined'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.addParameter('number', _('Player number'), '', false)
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerJustJoined');
|
||||
|
||||
extension
|
||||
.addExpression(
|
||||
'LastJoinedPlayerNumber',
|
||||
_('Last joined player number'),
|
||||
_('Returns the number of the player that has just joined the lobby.'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName(
|
||||
'gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined'
|
||||
);
|
||||
|
||||
extension
|
||||
.addStrExpression(
|
||||
@@ -520,6 +621,37 @@ module.exports = {
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('gdjs.multiplayer.getPlayersInLobbyCount');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'IsPlayerConnected',
|
||||
_('Player is connected'),
|
||||
_('Check if the specified player is connected to the lobby.'),
|
||||
_('Player _PARAM0_ is connected'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.addParameter(
|
||||
'number',
|
||||
_('The position of the player in the lobby (1, 2, ...)'),
|
||||
'',
|
||||
false
|
||||
)
|
||||
.setHelpPath('/all-features/multiplayer')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.isPlayerConnected');
|
||||
|
||||
extension
|
||||
.addExpressionAndCondition(
|
||||
'number',
|
||||
|
@@ -34,8 +34,66 @@ namespace gdjs {
|
||||
this.cache.add(key);
|
||||
this.keys.push(key);
|
||||
}
|
||||
|
||||
clear = () => {
|
||||
this.cache.clear();
|
||||
this.keys = [];
|
||||
};
|
||||
}
|
||||
|
||||
class SavedSyncDataUpdates<T> {
|
||||
private _updates: T[] = [];
|
||||
|
||||
store(update: T) {
|
||||
this._updates.push(update);
|
||||
if (this._updates.length > 10) {
|
||||
this._updates.shift();
|
||||
}
|
||||
}
|
||||
|
||||
getUpdates() {
|
||||
return this._updates;
|
||||
}
|
||||
|
||||
remove(update: T) {
|
||||
const index = this._updates.indexOf(update);
|
||||
if (index !== -1) {
|
||||
this._updates.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._updates = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to clone an object without reassigning the target object.
|
||||
* It's mainly helpful for tests, where multiple instances of the MultiplayerMessageManager are created,
|
||||
* and prevents keeping references to the same object.
|
||||
*/
|
||||
const cloneObjectWithoutOverwriting = ({
|
||||
target,
|
||||
source,
|
||||
}: {
|
||||
target: Object;
|
||||
source: Object;
|
||||
}) => {
|
||||
// Add the new properties.
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key) && !target.hasOwnProperty(key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the properties that are not in the source.
|
||||
for (const key in target) {
|
||||
if (target.hasOwnProperty(key) && !source.hasOwnProperty(key)) {
|
||||
delete target[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export type MultiplayerMessageManager = ReturnType<
|
||||
typeof makeMultiplayerMessageManager
|
||||
>;
|
||||
@@ -91,24 +149,39 @@ namespace gdjs {
|
||||
let lastSceneSyncTimestamp = 0;
|
||||
let lastSentSceneSyncData: LayoutNetworkSyncData | null = null;
|
||||
let numberOfForcedSceneUpdates = 0;
|
||||
let lastReceivedSceneSyncDataUpdates = new SavedSyncDataUpdates<
|
||||
LayoutNetworkSyncData
|
||||
>();
|
||||
|
||||
// The number of times per second the game data should be synchronized.
|
||||
const gameSyncDataTickRate = 1;
|
||||
let lastGameSyncTimestamp = 0;
|
||||
let lastSentGameSyncData: GameNetworkSyncData | null = null;
|
||||
let numberOfForcedGameUpdates = 0;
|
||||
let lastReceivedGameSyncDataUpdates = new SavedSyncDataUpdates<
|
||||
GameNetworkSyncData
|
||||
>();
|
||||
|
||||
// Send heartbeat messages to host to ensure the connection is still alive.
|
||||
// Send heartbeat messages from host to players, ensuring their connection is still alive,
|
||||
// measure the ping, and send other useful info.
|
||||
const heartbeatTickRate = 1;
|
||||
let lastHeartbeatTimestamp = 0;
|
||||
let _playersLastHeartbeatInfo: {
|
||||
[playerNumber: number]: {
|
||||
lastRoundTripTimes: number[];
|
||||
};
|
||||
let _playersLastRoundTripTimes: {
|
||||
[playerNumber: number]: number[];
|
||||
} = {};
|
||||
let _peerIdToPlayerNumber: { [peerId: string]: number } = {};
|
||||
let _playersPings: { [playerNumber: number]: number } = { 1: 0 };
|
||||
let _playersInfo: {
|
||||
[playerNumber: number]: {
|
||||
ping: number;
|
||||
playerId: string;
|
||||
username: string;
|
||||
};
|
||||
} = {};
|
||||
let _playerNumbersWhoJustLeft: number[] = [];
|
||||
let _playerNumbersWhoJustJoined: number[] = [];
|
||||
let _temporaryPlayerNumberToUsername: {
|
||||
[playerNumber: number]: string;
|
||||
} = {};
|
||||
|
||||
const addExpectedMessageAcknowledgement = ({
|
||||
originalMessageName,
|
||||
@@ -157,11 +230,6 @@ namespace gdjs {
|
||||
});
|
||||
};
|
||||
|
||||
const clearExpectedMessageAcknowledgements = () => {
|
||||
expectedMessageAcknowledgements = {};
|
||||
_lastClockReceivedByInstanceByScene = {};
|
||||
};
|
||||
|
||||
const getLastClockReceivedForInstanceOnScene = ({
|
||||
sceneNetworkId,
|
||||
instanceNetworkId,
|
||||
@@ -290,27 +358,6 @@ namespace gdjs {
|
||||
(instance) => instance.networkId === instanceNetworkId
|
||||
) || null;
|
||||
|
||||
if (!instance) {
|
||||
debugLogger.info(
|
||||
`instance ${objectName} ${instanceNetworkId} not found with network ID, trying to find it with persistent UUID.`
|
||||
);
|
||||
instance =
|
||||
instances.find(
|
||||
(instance) =>
|
||||
// For objects created from the start, the network ID is not set yet.
|
||||
instance.persistentUuid &&
|
||||
instance.persistentUuid.substring(0, 8) === instanceNetworkId
|
||||
) || null;
|
||||
|
||||
if (instance) {
|
||||
debugLogger.info(
|
||||
`instance ${objectName} ${instanceNetworkId} found with persistent UUID. Assigning network ID.`
|
||||
);
|
||||
// Set the network ID, as it was not set yet.
|
||||
instance.networkId = instanceNetworkId;
|
||||
}
|
||||
}
|
||||
|
||||
// If we know the position of the object, we can try to find the closest instance not synchronized yet.
|
||||
if (!instance && instanceX !== undefined && instanceY !== undefined) {
|
||||
debugLogger.info(
|
||||
@@ -411,6 +458,12 @@ namespace gdjs {
|
||||
const handleChangeInstanceOwnerMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Change owner messages do not need to be saved for later use, as the game will automatically change the owner of
|
||||
// the instance when receiving an update message with a different owner.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
|
||||
@@ -563,6 +616,12 @@ namespace gdjs {
|
||||
const handleUpdateInstanceMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Update instance messages do not need to be saved for later use, as the updates are sent pretty often,
|
||||
// a new one will be received very quickly.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
|
||||
@@ -732,6 +791,12 @@ namespace gdjs {
|
||||
const handleChangeVariableOwnerMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Change owner messages do not need to be saved for later use, as the game will automatically change the owner of
|
||||
// the variable when receiving an update message with a different owner.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
|
||||
@@ -884,6 +949,12 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const handleAcknowledgeMessagesReceived = () => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,
|
||||
// which are not sent when the game is not ready.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
// When we receive acknowledgement messages, save it in the extension, to avoid sending the message again.
|
||||
@@ -958,6 +1029,12 @@ namespace gdjs {
|
||||
const resendClearOrCancelAcknowledgedMessages = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,
|
||||
// which are not sent when the game is not ready.
|
||||
return;
|
||||
}
|
||||
|
||||
// When all acknowledgments are received for an message, we can clear the message from our
|
||||
// list of expected acknowledgments.
|
||||
const expectedMessageNames = Object.keys(expectedMessageAcknowledgements);
|
||||
@@ -1179,6 +1256,12 @@ namespace gdjs {
|
||||
const handleDestroyInstanceMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Destroy messages do not need to be saved for later use, as the game will automatically destroy
|
||||
// the instance if it does not receive an update message from it. So we return early.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
const destroyInstanceMessageNames = messageNamesArray.filter(
|
||||
@@ -1453,6 +1536,11 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const handleCustomMessagesReceived = (): void => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Assume that the custom messages are not worth saving for later use.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
const customMessageNames = messageNamesArray.filter((messageName) =>
|
||||
@@ -1569,6 +1657,11 @@ namespace gdjs {
|
||||
const handleUpdateSceneMessagesToSend = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): void => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Don't send messages if the multiplayer is not ready.
|
||||
return;
|
||||
}
|
||||
|
||||
const sceneNetworkSyncData = runtimeScene.getNetworkSyncData({
|
||||
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
|
||||
});
|
||||
@@ -1622,16 +1715,26 @@ namespace gdjs {
|
||||
const messageSender = message.getSender();
|
||||
const sceneNetworkId = messageData.id;
|
||||
|
||||
if (sceneNetworkId !== runtimeScene.networkId) {
|
||||
if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
if (sceneNetworkId !== runtimeScene.networkId) {
|
||||
debugLogger.info(
|
||||
`Received update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
|
||||
);
|
||||
// The scene is not the current scene.
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeScene.updateFromNetworkSyncData(messageData);
|
||||
} else {
|
||||
// If the game is not ready to receive game update messages, we need to save the data for later use.
|
||||
// This can happen when joining a game that is already running.
|
||||
debugLogger.info(
|
||||
`Received update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
|
||||
`Saving scene ${sceneNetworkId} update message for later use.`
|
||||
);
|
||||
// The scene is not the current scene.
|
||||
lastReceivedSceneSyncDataUpdates.store(messageData);
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeScene.updateFromNetworkSyncData(messageData);
|
||||
|
||||
// If we are are the host,
|
||||
// we need to relay the scene update to others except the player who sent the update message.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
@@ -1717,6 +1820,11 @@ namespace gdjs {
|
||||
const handleUpdateGameMessagesToSend = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): void => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Don't send messages if the multiplayer is not ready.
|
||||
return;
|
||||
}
|
||||
|
||||
const gameNetworkSyncData = runtimeScene.getGame().getNetworkSyncData({
|
||||
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
|
||||
});
|
||||
@@ -1768,7 +1876,15 @@ namespace gdjs {
|
||||
messages.forEach((message) => {
|
||||
const messageData = message.getData();
|
||||
const messageSender = message.getSender();
|
||||
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
|
||||
if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
|
||||
} else {
|
||||
// If the game is not ready to receive game update messages, we need to save the data for later use.
|
||||
// This can happen when joining a game that is already running.
|
||||
debugLogger.info(`Saving game update message for later use.`);
|
||||
lastReceivedGameSyncDataUpdates.store(messageData);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are are the host,
|
||||
// we need to relay the game update to others except the player who sent the update message.
|
||||
@@ -1785,23 +1901,59 @@ namespace gdjs {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSavedUpdateMessages = (runtimeScene: gdjs.RuntimeScene) => {
|
||||
// Reapply the game saved updates.
|
||||
lastReceivedGameSyncDataUpdates.getUpdates().forEach((messageData) => {
|
||||
debugLogger.info(`Reapplying saved update of game.`);
|
||||
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
|
||||
});
|
||||
// Game updates are always applied properly, so we can clear them.
|
||||
lastReceivedGameSyncDataUpdates.clear();
|
||||
|
||||
// Then reapply the scene saved updates.
|
||||
lastReceivedSceneSyncDataUpdates.getUpdates().forEach((messageData) => {
|
||||
const sceneNetworkId = messageData.id;
|
||||
|
||||
if (sceneNetworkId !== runtimeScene.networkId) {
|
||||
debugLogger.info(
|
||||
`Trying to apply saved update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
|
||||
);
|
||||
// The scene is not the current scene.
|
||||
return;
|
||||
}
|
||||
|
||||
debugLogger.info(`Reapplying saved update of scene ${sceneNetworkId}.`);
|
||||
|
||||
runtimeScene.updateFromNetworkSyncData(messageData);
|
||||
// We only remove the message if it was successfully applied, so it can be reapplied later,
|
||||
// in case we were not on the right scene.
|
||||
lastReceivedSceneSyncDataUpdates.remove(messageData);
|
||||
});
|
||||
};
|
||||
|
||||
const heartbeatMessageNamePrefix = '#heartbeat';
|
||||
const heartbeastMessageRegex = /#heartbeat#(.+)/;
|
||||
const createHeartbeatMessage = (): {
|
||||
messageName: string;
|
||||
messageData: any;
|
||||
} => {
|
||||
const playersPings = {
|
||||
1: 0, // Player 1 is the host, so we don't need to compute the ping.
|
||||
// Ensure player 1 is correctly set when the first heartbeat is sent.
|
||||
_playersInfo[1] = {
|
||||
ping: 0, // Player 1 is the host, so we don't need to compute the ping.
|
||||
playerId: gdjs.playerAuthentication.getUserId(),
|
||||
username: gdjs.playerAuthentication.getUsername(),
|
||||
};
|
||||
for (const playerNumber in _playersLastHeartbeatInfo) {
|
||||
playersPings[playerNumber] = getPlayerPing(parseInt(playerNumber, 10));
|
||||
for (const playerNumber in _playersInfo) {
|
||||
_playersInfo[playerNumber] = {
|
||||
..._playersInfo[playerNumber],
|
||||
ping: getPlayerPing(parseInt(playerNumber, 10)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,
|
||||
messageData: {
|
||||
now: getTimeNow(), // we send the current time to compute the ping.
|
||||
playersPings,
|
||||
playersInfo: _playersInfo,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1817,11 +1969,16 @@ namespace gdjs {
|
||||
messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,
|
||||
messageData: {
|
||||
sentAt: heartbeatSentAt,
|
||||
playerId: gdjs.playerAuthentication.getUserId(),
|
||||
username: gdjs.playerAuthentication.getUsername(),
|
||||
},
|
||||
};
|
||||
};
|
||||
const hasSentHeartbeatRecently = () => {
|
||||
return getTimeNow() - lastHeartbeatTimestamp < 1000 / heartbeatTickRate;
|
||||
return (
|
||||
!!lastHeartbeatTimestamp &&
|
||||
getTimeNow() - lastHeartbeatTimestamp < 1000 / heartbeatTickRate
|
||||
);
|
||||
};
|
||||
const handleHeartbeatsToSend = () => {
|
||||
// Only host sends heartbeats to all players regularly:
|
||||
@@ -1865,10 +2022,53 @@ namespace gdjs {
|
||||
// Ensure we know who is who.
|
||||
_peerIdToPlayerNumber[messageSender] = playerNumber;
|
||||
|
||||
// If we are not the host, save what the host told us about the pings and respond
|
||||
// with a heartbeat immediately.
|
||||
// If we are not the host, save what the host told us about the other players info
|
||||
// and respond with a heartbeat immediately, informing the host of our playerId and username.
|
||||
if (!gdjs.multiplayer.isPlayerHost()) {
|
||||
_playersPings = messageData.playersPings;
|
||||
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
|
||||
const currentlyKnownPlayerNumbers = Object.keys(
|
||||
_playersInfo
|
||||
).map((playerNumber) => parseInt(playerNumber, 10));
|
||||
const receivedPlayerNumbers = Object.keys(
|
||||
messageData.playersInfo
|
||||
).map((playerNumber) => parseInt(playerNumber, 10));
|
||||
const currentlyKnownPingForCurrentUser =
|
||||
_playersInfo[currentPlayerNumber] &&
|
||||
_playersInfo[currentPlayerNumber].ping;
|
||||
// If there are no players info yet, we're probably just connecting.
|
||||
// This can happen when joining a game that is already running.
|
||||
// Do not handle this case to avoid displaying too many notifications.
|
||||
if (!!currentlyKnownPlayerNumbers.length) {
|
||||
// Look at the players info received to know if there are new players who just connected.
|
||||
const newPlayerNumbers = receivedPlayerNumbers.filter(
|
||||
(playerNumber) =>
|
||||
!currentlyKnownPlayerNumbers.includes(playerNumber) &&
|
||||
playerNumber !== currentPlayerNumber // Do not consider ourselves as a new player.
|
||||
);
|
||||
_playerNumbersWhoJustJoined.push(...newPlayerNumbers);
|
||||
// Or players who have disconnected.
|
||||
const playerNumbersWhoHaveDisconnected = currentlyKnownPlayerNumbers.filter(
|
||||
(playerNumber) => !receivedPlayerNumbers.includes(playerNumber)
|
||||
);
|
||||
_playerNumbersWhoJustLeft.push(
|
||||
...playerNumbersWhoHaveDisconnected
|
||||
);
|
||||
for (const playerNumber of playerNumbersWhoHaveDisconnected) {
|
||||
// Temporarily save the username in another variable to be used for the notification,
|
||||
// as we're deleting its playerInfo just after.
|
||||
_temporaryPlayerNumberToUsername[
|
||||
playerNumber
|
||||
] = getPlayerUsername(playerNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the players info received from the host.
|
||||
// Avoid overwriting the whole object as it can mess up tests that rely on the object reference.
|
||||
cloneObjectWithoutOverwriting({
|
||||
source: messageData.playersInfo,
|
||||
target: _playersInfo,
|
||||
});
|
||||
|
||||
const {
|
||||
messageName: answerMessageName,
|
||||
messageData: answerMessageData,
|
||||
@@ -1876,27 +2076,44 @@ namespace gdjs {
|
||||
heartbeatSentAt: messageData.now, // We send back the time we received, so that the host can compute the ping.
|
||||
});
|
||||
sendDataTo([messageSender], answerMessageName, answerMessageData);
|
||||
// We have received a heartbeat from the host, informing us of our ping,
|
||||
// so we can consider the connection as working.
|
||||
if (
|
||||
_playersInfo[currentPlayerNumber] !== undefined &&
|
||||
_playersInfo[currentPlayerNumber].ping !== undefined
|
||||
) {
|
||||
gdjs.multiplayer.markConnectionAsConnected();
|
||||
if (currentlyKnownPingForCurrentUser === undefined) {
|
||||
// We just connected, let's add ourselves to the list of players who just connected,
|
||||
// for the notification and the events.
|
||||
_playerNumbersWhoJustJoined.push(currentPlayerNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are the host, compute the pings based on:
|
||||
// If we are the host.
|
||||
|
||||
// If this is a new player, we're about to send them their ping, so we can consider them connected.
|
||||
if (!_playersInfo[playerNumber]) {
|
||||
_playerNumbersWhoJustJoined.push(playerNumber);
|
||||
}
|
||||
|
||||
// compute the pings based on:
|
||||
// - the time we received the heartbeat.
|
||||
// - the time the heartbeat was sent.
|
||||
const now = getTimeNow();
|
||||
const heartbeatSentAt = messageData.sentAt;
|
||||
const roundTripTime = Math.round(now - heartbeatSentAt);
|
||||
const playerLastHeartbeatInfo =
|
||||
_playersLastHeartbeatInfo[playerNumber] || {};
|
||||
const playerLastRoundTripTimes =
|
||||
playerLastHeartbeatInfo.lastRoundTripTimes || [];
|
||||
_playersLastRoundTripTimes[playerNumber] || [];
|
||||
playerLastRoundTripTimes.push(roundTripTime);
|
||||
if (playerLastRoundTripTimes.length > 5) {
|
||||
// Keep only the last 5 RTT to compute the average.
|
||||
playerLastRoundTripTimes.shift();
|
||||
}
|
||||
_playersLastHeartbeatInfo[playerNumber] = {
|
||||
lastRoundTripTimes: playerLastRoundTripTimes,
|
||||
};
|
||||
_playersLastRoundTripTimes[playerNumber] = playerLastRoundTripTimes;
|
||||
|
||||
let sum = 0;
|
||||
for (const lastRoundTripTime of playerLastRoundTripTimes) {
|
||||
@@ -1905,19 +2122,30 @@ namespace gdjs {
|
||||
const averagePing = Math.round(
|
||||
sum / playerLastRoundTripTimes.length / 2 // Divide by 2 to get the one way ping.
|
||||
);
|
||||
_playersPings[playerNumber] = averagePing;
|
||||
_playersInfo[playerNumber] = {
|
||||
ping: averagePing,
|
||||
playerId: messageData.playerId,
|
||||
username: messageData.username,
|
||||
};
|
||||
|
||||
// If there are new players, let's resend a heartbeat right away so that everyone is aware of them
|
||||
// on approximately the same frame.
|
||||
if (_playerNumbersWhoJustJoined.length) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
const { messageName, messageData } = createHeartbeatMessage();
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
lastHeartbeatTimestamp = getTimeNow();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getPlayerPing = (playerNumber: number) => {
|
||||
if (playerNumber < 1) {
|
||||
// Player 1 is the host, so we don't need to compute the ping.
|
||||
// Any negative number is invalid.
|
||||
const playerInfo = _playersInfo[playerNumber];
|
||||
if (!playerInfo) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _playersPings[playerNumber] || 0;
|
||||
return playerInfo.ping || 0;
|
||||
};
|
||||
|
||||
const getCurrentPlayerPing = () => {
|
||||
@@ -1928,36 +2156,55 @@ namespace gdjs {
|
||||
const markPlayerAsDisconnected = (playerNumber: number) => {
|
||||
logger.info(`Marking player ${playerNumber} as disconnected.`);
|
||||
_playerNumbersWhoJustLeft.push(playerNumber);
|
||||
// Temporarily save the username in another variable to be used for the notification,
|
||||
// as we're deleting its playerInfo just after.
|
||||
_temporaryPlayerNumberToUsername[playerNumber] = getPlayerUsername(
|
||||
playerNumber
|
||||
);
|
||||
|
||||
// If Player 1 has disconnected, just end the game.
|
||||
if (playerNumber === 1) {
|
||||
logger.info('Host has disconnected, ending the game.');
|
||||
_playersLastHeartbeatInfo = {};
|
||||
_playersPings = {};
|
||||
clearAllMessagesTempData();
|
||||
gdjs.multiplayer.handleLobbyGameEnded();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the player from the list of players.
|
||||
// This will cause the next hearbeat to not include this player
|
||||
// and the others will consider them as disconnected.
|
||||
delete _playersLastHeartbeatInfo[playerNumber];
|
||||
delete _playersPings[playerNumber];
|
||||
clearPlayerTempData(playerNumber);
|
||||
// If we are the host, send a heartbeat right away so that everyone is aware of the disconnection
|
||||
// on approximately the same frame.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
const { messageName, messageData } = createHeartbeatMessage();
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
lastHeartbeatTimestamp = getTimeNow();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnectedPeers = (runtimeScene: RuntimeScene) => {
|
||||
const getPlayerUsername = (playerNumber: number) => {
|
||||
return (
|
||||
(_playersInfo[playerNumber] || {}).username ||
|
||||
_temporaryPlayerNumberToUsername[playerNumber] ||
|
||||
`Player ${playerNumber}`
|
||||
);
|
||||
};
|
||||
|
||||
const getPlayerId = (playerNumber: number) => {
|
||||
return (_playersInfo[playerNumber] || {}).playerId || '';
|
||||
};
|
||||
|
||||
const handleJustDisconnectedPeers = (runtimeScene: RuntimeScene) => {
|
||||
// If the game is not running, we don't need to handle disconnected peers.
|
||||
if (!gdjs.multiplayer.isLobbyGameRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Players can disconnect if the P2P connection disconnects
|
||||
// or if we don't receive heartbeats for a while.
|
||||
const disconnectedPlayerNumbers: number[] = [];
|
||||
// We rely on the p2p helper to know who has disconnected.
|
||||
const justDisconnectedPlayerNumbers: number[] = [];
|
||||
|
||||
const disconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();
|
||||
if (disconnectedPeers.length) {
|
||||
for (const disconnectedPeer of disconnectedPeers) {
|
||||
const justDisconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();
|
||||
if (justDisconnectedPeers.length) {
|
||||
for (const disconnectedPeer of justDisconnectedPeers) {
|
||||
const disconnectedPlayerNumber =
|
||||
_peerIdToPlayerNumber[disconnectedPeer];
|
||||
if (!disconnectedPlayerNumber) {
|
||||
@@ -1965,11 +2212,11 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
logger.info(`Player ${disconnectedPlayerNumber} has disconnected.`);
|
||||
disconnectedPlayerNumbers.push(disconnectedPlayerNumber);
|
||||
justDisconnectedPlayerNumbers.push(disconnectedPlayerNumber);
|
||||
}
|
||||
}
|
||||
|
||||
for (const playerNumber of disconnectedPlayerNumbers) {
|
||||
for (const playerNumber of justDisconnectedPlayerNumbers) {
|
||||
// When a player disconnects, as the host, we look at all the instances
|
||||
// they own and decide what to do with them.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
@@ -2000,29 +2247,60 @@ namespace gdjs {
|
||||
}
|
||||
};
|
||||
|
||||
const clearDisconnectedPeers = () => {
|
||||
_playerNumbersWhoJustLeft = [];
|
||||
};
|
||||
|
||||
const hasAnyPlayerLeft = () => {
|
||||
const hasAnyPlayerJustLeft = (): boolean => {
|
||||
return _playerNumbersWhoJustLeft.length > 0;
|
||||
};
|
||||
|
||||
const hasPlayerLeft = (playerNumber: number) => {
|
||||
const hasPlayerJustLeft = (playerNumber: number): boolean => {
|
||||
return _playerNumbersWhoJustLeft.includes(playerNumber);
|
||||
};
|
||||
|
||||
const getDisconnectedPlayers = () => {
|
||||
const getPlayersWhoJustLeft = (): number[] => {
|
||||
return _playerNumbersWhoJustLeft;
|
||||
};
|
||||
|
||||
const getNumberOfConnectedPlayers = () => {
|
||||
// Look at the player pings as a way to know how many players are in the lobby.
|
||||
return Object.keys(_playersPings).length;
|
||||
const getLatestPlayerWhoJustLeft = (): number => {
|
||||
return _playerNumbersWhoJustLeft[0] || 0;
|
||||
};
|
||||
const removePlayerWhoJustLeft = (): void => {
|
||||
// Avoid using shift for test purposes, as it modifies the reference.
|
||||
const playerNumberWhoLeft = _playerNumbersWhoJustLeft[0];
|
||||
if (playerNumberWhoLeft !== undefined) {
|
||||
_playerNumbersWhoJustLeft = _playerNumbersWhoJustLeft.slice(1);
|
||||
delete _temporaryPlayerNumberToUsername[playerNumberWhoLeft];
|
||||
}
|
||||
};
|
||||
|
||||
const hasAnyPlayerJustJoined = () => {
|
||||
return _playerNumbersWhoJustJoined.length > 0;
|
||||
};
|
||||
const hasPlayerJustJoined = (playerNumber: number): boolean => {
|
||||
return _playerNumbersWhoJustJoined.includes(playerNumber);
|
||||
};
|
||||
const getPlayersWhoJustJoined = () => {
|
||||
return _playerNumbersWhoJustJoined;
|
||||
};
|
||||
const getLatestPlayerWhoJustJoined = (): number => {
|
||||
return _playerNumbersWhoJustJoined[0] || 0;
|
||||
};
|
||||
const removePlayerWhoJustJoined = (): void => {
|
||||
// Avoid using shift for test purposes, as it modifies the reference.
|
||||
const playerNumberWhoJoined = _playerNumbersWhoJustJoined[0];
|
||||
if (playerNumberWhoJoined !== undefined) {
|
||||
_playerNumbersWhoJustJoined = _playerNumbersWhoJustJoined.slice(1);
|
||||
}
|
||||
};
|
||||
|
||||
const getConnectedPlayers = () => {
|
||||
return Object.keys(_playersInfo).map((playerNumber) => ({
|
||||
playerNumber: parseInt(playerNumber, 10),
|
||||
playerId: _playersInfo[playerNumber].playerId,
|
||||
}));
|
||||
};
|
||||
const getNumberOfConnectedPlayers = () => {
|
||||
// Look at the player info as a way to know how many players are in the lobby.
|
||||
// This object is updated when heartbeats are sent and received.
|
||||
return Object.keys(_playersInfo).length;
|
||||
};
|
||||
const isPlayerConnected = (playerNumber: number) => {
|
||||
return _playersPings[playerNumber] !== undefined;
|
||||
return _playersInfo[playerNumber] !== undefined;
|
||||
};
|
||||
|
||||
const endGameMessageName = '#endGame';
|
||||
@@ -2066,20 +2344,34 @@ namespace gdjs {
|
||||
|
||||
// If the message is received more than 1 time, we just ignore it and end the game.
|
||||
|
||||
_playersLastHeartbeatInfo = {};
|
||||
_playersPings = {};
|
||||
clearAllMessagesTempData();
|
||||
gdjs.multiplayer.handleLobbyGameEnded();
|
||||
};
|
||||
|
||||
const updatePlayersPingsForTests = (playersPings) => {
|
||||
_playersPings = playersPings;
|
||||
const clearAllMessagesTempData = () => {
|
||||
_playersLastRoundTripTimes = {};
|
||||
_playersInfo = {};
|
||||
lastReceivedGameSyncDataUpdates.clear();
|
||||
lastReceivedSceneSyncDataUpdates.clear();
|
||||
processedCustomMessagesCache.clear();
|
||||
_playerNumbersWhoJustLeft = [];
|
||||
_playerNumbersWhoJustJoined = [];
|
||||
expectedMessageAcknowledgements = {};
|
||||
_lastClockReceivedByInstanceByScene = {};
|
||||
};
|
||||
|
||||
const clearPlayerTempData = (playerNumber: number) => {
|
||||
// Remove the player from the list of players.
|
||||
// This will cause the next hearbeat to not include this player
|
||||
// and the others will consider them as disconnected.
|
||||
delete _playersLastRoundTripTimes[playerNumber];
|
||||
delete _playersInfo[playerNumber];
|
||||
};
|
||||
|
||||
return {
|
||||
sendDataTo,
|
||||
// Acks.
|
||||
addExpectedMessageAcknowledgement,
|
||||
clearExpectedMessageAcknowledgements,
|
||||
handleAcknowledgeMessagesReceived,
|
||||
resendClearOrCancelAcknowledgedMessages,
|
||||
// Instance ownership.
|
||||
@@ -2113,23 +2405,36 @@ namespace gdjs {
|
||||
createUpdateGameMessage,
|
||||
handleUpdateGameMessagesToSend,
|
||||
handleUpdateGameMessagesReceived,
|
||||
handleSavedUpdateMessages,
|
||||
// Heartbeats.
|
||||
handleHeartbeatsToSend,
|
||||
handleHeartbeatsReceived,
|
||||
// Connection/Disonnection.
|
||||
// Pings & usernames.
|
||||
getPlayerPing,
|
||||
getCurrentPlayerPing,
|
||||
updatePlayersPingsForTests,
|
||||
handleDisconnectedPeers,
|
||||
clearDisconnectedPeers,
|
||||
hasAnyPlayerLeft,
|
||||
hasPlayerLeft,
|
||||
getDisconnectedPlayers,
|
||||
getPlayerUsername,
|
||||
getPlayerId,
|
||||
// Connected players.
|
||||
handleJustDisconnectedPeers,
|
||||
getConnectedPlayers,
|
||||
getNumberOfConnectedPlayers,
|
||||
isPlayerConnected,
|
||||
// Leaving players.
|
||||
hasAnyPlayerJustLeft,
|
||||
hasPlayerJustLeft,
|
||||
getPlayersWhoJustLeft,
|
||||
getLatestPlayerWhoJustLeft,
|
||||
removePlayerWhoJustLeft,
|
||||
// Joining players.
|
||||
hasAnyPlayerJustJoined,
|
||||
hasPlayerJustJoined,
|
||||
getPlayersWhoJustJoined,
|
||||
getLatestPlayerWhoJustJoined,
|
||||
removePlayerWhoJustJoined,
|
||||
// End game.
|
||||
sendEndGameMessage,
|
||||
handleEndGameMessages,
|
||||
clearAllMessagesTempData,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -208,7 +208,10 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const handleChangeVariableOwnerMessagesToSend = function () {
|
||||
if (!gdjs.multiplayer.isLobbyGameRunning()) {
|
||||
if (
|
||||
!gdjs.multiplayer.isLobbyGameRunning() ||
|
||||
!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,8 @@ namespace gdjs {
|
||||
|
||||
let canLobbyBeClosed = true;
|
||||
|
||||
const notificationContainerIds: string[] = [];
|
||||
|
||||
export const getDomElementContainer = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): HTMLDivElement | null => {
|
||||
@@ -387,7 +389,7 @@ namespace gdjs {
|
||||
// to allow the player to leave the lobby.
|
||||
setTimeout(() => {
|
||||
closeContainer.style.visibility = 'inherit';
|
||||
}, 5000);
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -398,7 +400,6 @@ namespace gdjs {
|
||||
) {
|
||||
showNotification(
|
||||
runtimeScene,
|
||||
'error-notification',
|
||||
'An error occurred while displaying the game lobbies, please try again.',
|
||||
'error'
|
||||
);
|
||||
@@ -411,37 +412,82 @@ namespace gdjs {
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
playerName: string
|
||||
) {
|
||||
showNotification(
|
||||
runtimeScene,
|
||||
'player-left-notification',
|
||||
`${playerName} has left the game.`,
|
||||
'warning'
|
||||
);
|
||||
showNotification(runtimeScene, `${playerName} left.`, 'warning');
|
||||
};
|
||||
|
||||
/**
|
||||
* Create, display, and hide a notification when a player leaves the game.
|
||||
* Create, display, and hide a notification when a player joins the game.
|
||||
*/
|
||||
export const displayPlayerJoinedNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
playerName: string
|
||||
) {
|
||||
showNotification(runtimeScene, `${playerName} joined.`, 'success');
|
||||
};
|
||||
|
||||
/**
|
||||
* Create, display, and hide a notification when an error happens on connection.
|
||||
*/
|
||||
export const displayConnectionErrorNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
showNotification(
|
||||
runtimeScene,
|
||||
'connection-error-notification',
|
||||
'Could not connect to other players.',
|
||||
'error'
|
||||
);
|
||||
};
|
||||
|
||||
const removeNotificationAndShiftOthers = function (
|
||||
notificationContainerId: string
|
||||
) {
|
||||
const notification = document.getElementById(notificationContainerId);
|
||||
if (!notification) {
|
||||
logger.error('Notification not found.');
|
||||
return;
|
||||
}
|
||||
const index = notificationContainerIds.indexOf(notificationContainerId);
|
||||
if (index !== -1) {
|
||||
notificationContainerIds.splice(index, 1);
|
||||
}
|
||||
notification.remove();
|
||||
|
||||
// Shift the other notifications up.
|
||||
for (let i = 0; i < notificationContainerIds.length; i++) {
|
||||
const notification = document.getElementById(
|
||||
notificationContainerIds[i]
|
||||
);
|
||||
if (!notification) {
|
||||
logger.error('Notification not found.');
|
||||
continue;
|
||||
}
|
||||
notification.style.top = `${12 + i * 32}px`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to show a notification to the user, that disappears automatically.
|
||||
*/
|
||||
export const showNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
id: string,
|
||||
content: string,
|
||||
type: 'success' | 'warning' | 'error'
|
||||
) {
|
||||
// When we show a notification, we add it below the other ones.
|
||||
// We also remove the oldest one if there are too many > 5.
|
||||
if (notificationContainerIds.length > 5) {
|
||||
const oldestNotificationId = notificationContainerIds.shift();
|
||||
if (!oldestNotificationId) {
|
||||
logger.error('No oldest notification ID found.');
|
||||
return;
|
||||
}
|
||||
|
||||
removeNotificationAndShiftOthers(oldestNotificationId);
|
||||
}
|
||||
|
||||
// We generate a random ID for the notification, so they can stack.
|
||||
const id = `notification-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
const domContainer = runtimeScene
|
||||
.getGame()
|
||||
.getRenderer()
|
||||
@@ -461,7 +507,8 @@ namespace gdjs {
|
||||
: type === 'warning'
|
||||
? '#FFA500'
|
||||
: '#FF0000';
|
||||
divContainer.style.top = '12px';
|
||||
// Space the notifications vertically, based on how many there are.
|
||||
divContainer.style.top = `${12 + notificationContainerIds.length * 32}px`;
|
||||
divContainer.style.right = '16px';
|
||||
divContainer.style.padding = '6px 32px 6px 6px';
|
||||
// Use zIndex 1 to make sure it is below the iframe.
|
||||
@@ -496,8 +543,9 @@ namespace gdjs {
|
||||
loggedText.style.margin = '0px';
|
||||
|
||||
divContainer.appendChild(loggedText);
|
||||
|
||||
domContainer.appendChild(divContainer);
|
||||
notificationContainerIds.push(id);
|
||||
|
||||
const animationTime = 700;
|
||||
const notificationTime = 5000;
|
||||
setTimeout(() => {
|
||||
@@ -518,7 +566,7 @@ namespace gdjs {
|
||||
}, notificationTime);
|
||||
// Use timeout because onanimationend listener does not work.
|
||||
setTimeout(() => {
|
||||
divContainer.remove();
|
||||
removeNotificationAndShiftOthers(id);
|
||||
}, notificationTime + animationTime);
|
||||
};
|
||||
|
||||
|
@@ -92,9 +92,14 @@ namespace gdjs {
|
||||
// To handle this case and avoid having an object not synchronized, we set a timeout to destroy the object
|
||||
// if it has not been assigned a networkId after a short delay.
|
||||
this._destroyInstanceTimeoutId = setTimeout(() => {
|
||||
if (!owner.networkId && gdjs.multiplayer.isLobbyGameRunning()) {
|
||||
const sceneNetworkId = this.owner.getRuntimeScene().networkId;
|
||||
if (
|
||||
!owner.networkId &&
|
||||
gdjs.multiplayer.isLobbyGameRunning() &&
|
||||
sceneNetworkId
|
||||
) {
|
||||
debugLogger.info(
|
||||
`Lobby game is running and object ${owner.getName()} has not been assigned a networkId after a short delay, destroying it.`
|
||||
`Lobby game is running on a synced scene and object ${owner.getName()} has not been assigned a networkId after a short delay, destroying it.`
|
||||
);
|
||||
owner.deleteFromScene(instanceContainer);
|
||||
}
|
||||
@@ -162,12 +167,9 @@ namespace gdjs {
|
||||
|
||||
private _getOrCreateInstanceNetworkId() {
|
||||
if (!this.owner.networkId) {
|
||||
// no ID for this object, let's generate one so it can be identified by other players.
|
||||
// Either use the persistentUuid if it exists, or generate a new one.
|
||||
// No ID for this object, let's generate one so it can be identified by other players.
|
||||
// Keep it short to avoid sending too much data.
|
||||
const newID = this.owner.persistentUuid
|
||||
? this.owner.persistentUuid.substring(0, 8)
|
||||
: gdjs.makeUuid().substring(0, 8);
|
||||
const newID = gdjs.makeUuid().substring(0, 8);
|
||||
this.owner.networkId = newID;
|
||||
}
|
||||
|
||||
@@ -250,7 +252,8 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
// If game is running and object belong to a player that is not connected, destroy the object.
|
||||
// If game is running and the object belongs to a player who is not connected, destroy the object.
|
||||
// As the game may create objects before the lobby game starts, we don't want to destroy them if it's not running.
|
||||
if (
|
||||
this.playerNumber !== 0 && // Host is always connected.
|
||||
!gdjs.multiplayerMessageManager.isPlayerConnected(this.playerNumber)
|
||||
@@ -405,8 +408,7 @@ namespace gdjs {
|
||||
this._destroyInstanceTimeoutId = null;
|
||||
}
|
||||
|
||||
// If the lobby game is not running, do not try to destroy the object,
|
||||
// as the game may create objects before the lobby game starts, and we don't want to destroy them.
|
||||
// If the lobby game is not running, no need to send a message to destroy the object.
|
||||
if (!gdjs.multiplayer.isLobbyGameRunning()) {
|
||||
return;
|
||||
}
|
||||
|
@@ -1,16 +1,12 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Multiplayer');
|
||||
|
||||
type Lobby = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
players: { playerId: string; status: string }[];
|
||||
};
|
||||
export namespace multiplayer {
|
||||
/** Set to true in testing to avoid relying on the multiplayer extension. */
|
||||
export let disableMultiplayerForTesting = false;
|
||||
|
||||
export let _isReadyToSendOrReceiveGameUpdateMessages = false;
|
||||
|
||||
let _isGameRegistered: boolean | null = null;
|
||||
let _isCheckingIfGameIsRegistered = false;
|
||||
let _isWaitingForLogin = false;
|
||||
@@ -20,8 +16,6 @@ namespace gdjs {
|
||||
let _hasLobbyGameJustEnded = false;
|
||||
let _lobbyId: string | null = null;
|
||||
let _connectionId: string | null = null;
|
||||
export let _lobby: Lobby | null = null;
|
||||
let _playerPublicProfiles: { id: string; username?: string }[] = [];
|
||||
|
||||
// Communication methods.
|
||||
let _lobbiesMessageCallback: ((event: MessageEvent) => void) | null = null;
|
||||
@@ -46,6 +40,11 @@ namespace gdjs {
|
||||
|
||||
if (disableMultiplayerForTesting) return;
|
||||
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsToSend();
|
||||
gdjs.multiplayerMessageManager.handleJustDisconnectedPeers(
|
||||
runtimeScene
|
||||
);
|
||||
|
||||
gdjs.multiplayerMessageManager.handleChangeInstanceOwnerMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
@@ -60,14 +59,21 @@ namespace gdjs {
|
||||
gdjs.multiplayerMessageManager.handleChangeVariableOwnerMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
// In case we're joining an existing lobby, it's possible we haven't
|
||||
// fully caught up with the game state yet, especially if a scene is loading.
|
||||
// We look at them every frame, from the moment the lobby has started,
|
||||
// to ensure we don't miss any.
|
||||
if (_isLobbyGameRunning) {
|
||||
gdjs.multiplayerMessageManager.handleSavedUpdateMessages(
|
||||
runtimeScene
|
||||
);
|
||||
}
|
||||
gdjs.multiplayerMessageManager.handleUpdateGameMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
gdjs.multiplayerMessageManager.handleUpdateSceneMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsToSend();
|
||||
gdjs.multiplayerMessageManager.handleDisconnectedPeers(runtimeScene);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -75,6 +81,13 @@ namespace gdjs {
|
||||
(runtimeScene: gdjs.RuntimeScene) => {
|
||||
if (disableMultiplayerForTesting) return;
|
||||
|
||||
// Handle joining and leaving players to show notifications accordingly.
|
||||
handleLeavingPlayer(runtimeScene);
|
||||
handleJoiningPlayer(runtimeScene);
|
||||
|
||||
// Then look at the heartbeats received to know if a new player has joined/left.
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
|
||||
|
||||
gdjs.multiplayerMessageManager.handleDestroyInstanceMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
@@ -85,9 +98,6 @@ namespace gdjs {
|
||||
gdjs.multiplayerMessageManager.handleUpdateSceneMessagesToSend(
|
||||
runtimeScene
|
||||
);
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
|
||||
handleLeavingPlayer(runtimeScene);
|
||||
gdjs.multiplayerMessageManager.clearDisconnectedPeers();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -144,6 +154,9 @@ namespace gdjs {
|
||||
if (playerToken) {
|
||||
url.searchParams.set('playerToken', playerToken);
|
||||
}
|
||||
// Increment this value when a new feature is introduced so we can
|
||||
// adapt the interface of the lobbies.
|
||||
url.searchParams.set('multiplayerVersion', '2');
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
@@ -156,6 +169,9 @@ namespace gdjs {
|
||||
|
||||
export const isLobbyGameRunning = () => _isLobbyGameRunning;
|
||||
|
||||
export const isReadyToSendOrReceiveGameUpdateMessages = () =>
|
||||
_isReadyToSendOrReceiveGameUpdateMessages;
|
||||
|
||||
/**
|
||||
* Returns true if the game has just ended,
|
||||
* useful to switch back to to the main menu.
|
||||
@@ -166,17 +182,16 @@ namespace gdjs {
|
||||
* Returns the number of players in the lobby.
|
||||
*/
|
||||
export const getPlayersInLobbyCount = () => {
|
||||
// If the game has not started yet, look at the lobby.
|
||||
if (!_isLobbyGameRunning && _lobby) {
|
||||
return _lobby.players.length;
|
||||
}
|
||||
// Whether the lobby game has started or not, the number of players in the lobby
|
||||
// is the number of connected players.
|
||||
return gdjs.multiplayerMessageManager.getNumberOfConnectedPlayers();
|
||||
};
|
||||
|
||||
// If the game has started, look at the pings received from the players.
|
||||
if (_isLobbyGameRunning) {
|
||||
return gdjs.multiplayerMessageManager.getNumberOfConnectedPlayers();
|
||||
}
|
||||
|
||||
return 0;
|
||||
/**
|
||||
* Returns true if the player at this position is connected to the lobby.
|
||||
*/
|
||||
export const isPlayerConnected = (playerNumber: number) => {
|
||||
return gdjs.multiplayerMessageManager.isPlayerConnected(playerNumber);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -195,38 +210,12 @@ namespace gdjs {
|
||||
return playerNumber === 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the player ID of the player at the given number in the lobby.
|
||||
* The number is shifted by one, so that the first player has number 1.
|
||||
*/
|
||||
const getPlayerId = (playerNumber: number) => {
|
||||
if (!_lobby) {
|
||||
return '';
|
||||
}
|
||||
const index = playerNumber - 1;
|
||||
if (index < 0 || index >= _lobby.players.length) {
|
||||
return '';
|
||||
}
|
||||
return _lobby.players[index].playerId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the player username at the given number in the lobby.
|
||||
* The number is shifted by one, so that the first player has number 1.
|
||||
*/
|
||||
export const getPlayerUsername = (playerNumber: number) => {
|
||||
const playerId = getPlayerId(playerNumber);
|
||||
if (!playerId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const playerPublicProfile = _playerPublicProfiles.find(
|
||||
(profile) => profile.id === playerId
|
||||
);
|
||||
|
||||
return playerPublicProfile
|
||||
? playerPublicProfile.username
|
||||
: `Player ${playerNumber}`;
|
||||
return gdjs.multiplayerMessageManager.getPlayerUsername(playerNumber);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -238,28 +227,33 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const handleLeavingPlayer = (runtimeScene: gdjs.RuntimeScene) => {
|
||||
const disconnectedPlayers = gdjs.multiplayerMessageManager.getDisconnectedPlayers();
|
||||
if (disconnectedPlayers.length > 0) {
|
||||
for (const playerNumber of disconnectedPlayers) {
|
||||
const playerLeftId = getPlayerId(playerNumber);
|
||||
|
||||
if (!playerLeftId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playerLeftPublicProfile = _playerPublicProfiles.find(
|
||||
(profile) => profile.id === playerLeftId
|
||||
);
|
||||
|
||||
if (playerLeftPublicProfile) {
|
||||
gdjs.multiplayerComponents.displayPlayerLeftNotification(
|
||||
runtimeScene,
|
||||
(playerLeftPublicProfile && playerLeftPublicProfile.username) ||
|
||||
'Player'
|
||||
);
|
||||
}
|
||||
}
|
||||
const lastestPlayerWhoJustLeft = gdjs.multiplayerMessageManager.getLatestPlayerWhoJustLeft();
|
||||
if (lastestPlayerWhoJustLeft) {
|
||||
const playerUsername = getPlayerUsername(lastestPlayerWhoJustLeft);
|
||||
gdjs.multiplayerComponents.displayPlayerLeftNotification(
|
||||
runtimeScene,
|
||||
playerUsername
|
||||
);
|
||||
}
|
||||
// We remove the players who just left 1 by 1, so that they can be treated in different frames.
|
||||
// This is especially important if the expression to know the latest player who just left is used,
|
||||
// to avoid missing a player leaving.
|
||||
gdjs.multiplayerMessageManager.removePlayerWhoJustLeft();
|
||||
};
|
||||
|
||||
const handleJoiningPlayer = (runtimeScene: gdjs.RuntimeScene) => {
|
||||
const lastestPlayerWhoJustJoined = gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined();
|
||||
if (lastestPlayerWhoJustJoined) {
|
||||
const playerUsername = getPlayerUsername(lastestPlayerWhoJustJoined);
|
||||
gdjs.multiplayerComponents.displayPlayerJoinedNotification(
|
||||
runtimeScene,
|
||||
playerUsername
|
||||
);
|
||||
}
|
||||
// We remove the players who just joined 1 by 1, so that they can be treated in different frames.
|
||||
// This is especially important if the expression to know the latest player who just joined is used,
|
||||
// to avoid missing a player joining.
|
||||
gdjs.multiplayerMessageManager.removePlayerWhoJustJoined();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -298,61 +292,7 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
const getUserPublicProfile = async (
|
||||
userId: string,
|
||||
isDev: boolean
|
||||
): Promise<{ id: string; username?: string }> => {
|
||||
const rootApi = isDev
|
||||
? 'https://api-dev.gdevelop.io'
|
||||
: 'https://api.gdevelop.io';
|
||||
const url = `${rootApi}/user/user-public-profile/${userId}`;
|
||||
const response = await fetch(url);
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const updatePlayerPublicProfiles = async (isDev: boolean) => {
|
||||
if (!_lobby) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playerIds = _lobby.players.map((player) => player.playerId);
|
||||
const currentPlayerPublicProfileIds = _playerPublicProfiles.map(
|
||||
(profile) => profile.id
|
||||
);
|
||||
const addedPlayerIds = playerIds.filter(
|
||||
(id) => !currentPlayerPublicProfileIds.includes(id)
|
||||
);
|
||||
const removedPlayerIds = currentPlayerPublicProfileIds.filter(
|
||||
(id) => !playerIds.includes(id)
|
||||
);
|
||||
if (addedPlayerIds.length === 0 && removedPlayerIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addedPlayerIds.length > 0) {
|
||||
const addedPlayerPublicProfiles = await Promise.all(
|
||||
addedPlayerIds.map(async (id) => {
|
||||
const userPublicProfile = await getUserPublicProfile(id, isDev);
|
||||
return userPublicProfile;
|
||||
})
|
||||
);
|
||||
|
||||
_playerPublicProfiles = [
|
||||
..._playerPublicProfiles,
|
||||
...addedPlayerPublicProfiles,
|
||||
];
|
||||
}
|
||||
|
||||
if (removedPlayerIds.length > 0) {
|
||||
const updatedPlayerPublicProfiles = _playerPublicProfiles.filter(
|
||||
(profile) => !removedPlayerIds.includes(profile.id)
|
||||
);
|
||||
|
||||
_playerPublicProfiles = updatedPlayerPublicProfiles;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLobbyJoinEvent = function (
|
||||
const handleJoinLobbyEvent = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
lobbyId: string
|
||||
) {
|
||||
@@ -367,7 +307,6 @@ namespace gdjs {
|
||||
_connectionId = null;
|
||||
playerNumber = null;
|
||||
_lobbyId = null;
|
||||
_lobby = null;
|
||||
_websocket = null;
|
||||
}
|
||||
|
||||
@@ -460,15 +399,9 @@ namespace gdjs {
|
||||
}
|
||||
case 'lobbyUpdated': {
|
||||
const messageData = messageContent.data;
|
||||
const lobby = messageData.lobby;
|
||||
const positionInLobby = messageData.positionInLobby;
|
||||
if (!lobby) {
|
||||
logger.error('No lobby received');
|
||||
return;
|
||||
}
|
||||
handleLobbyUpdatedEvent({
|
||||
runtimeScene,
|
||||
updatedLobby: lobby,
|
||||
positionInLobby,
|
||||
});
|
||||
break;
|
||||
@@ -502,12 +435,13 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
const peerId = messageData.peerId;
|
||||
if (!peerId) {
|
||||
const compressionMethod = messageData.compressionMethod;
|
||||
if (!peerId || !compressionMethod) {
|
||||
logger.error('Malformed message received');
|
||||
return;
|
||||
}
|
||||
|
||||
handlePeerIdEvent({ peerId });
|
||||
handlePeerIdEvent({ peerId, compressionMethod });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -632,37 +566,25 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
const handleLobbyLeaveEvent = function () {
|
||||
const handleLeaveLobbyEvent = function () {
|
||||
if (_websocket) {
|
||||
_websocket.close();
|
||||
}
|
||||
_connectionId = null;
|
||||
playerNumber = null;
|
||||
_lobbyId = null;
|
||||
_lobby = null;
|
||||
_websocket = null;
|
||||
};
|
||||
|
||||
const handleLobbyUpdatedEvent = function ({
|
||||
runtimeScene,
|
||||
updatedLobby,
|
||||
positionInLobby,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
updatedLobby: Lobby;
|
||||
positionInLobby: number;
|
||||
}) {
|
||||
// Update the object representing the lobby in the extension.
|
||||
_lobby = updatedLobby;
|
||||
|
||||
// If the lobby is playing, do not update anything.
|
||||
if (updatedLobby.status === 'playing') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the profiles so we can use the usernames of the players.
|
||||
updatePlayerPublicProfiles(isUsingGDevelopDevelopmentEnvironment);
|
||||
|
||||
// This is mainly useful when joining a lobby, or when the lobby is updated before the game starts.
|
||||
// The position in lobby should never change after the game has started (the WS is closed anyway).
|
||||
playerNumber = positionInLobby;
|
||||
|
||||
// If the player is in the lobby, tell the lobbies window that the lobby has been updated,
|
||||
@@ -672,7 +594,6 @@ namespace gdjs {
|
||||
);
|
||||
|
||||
if (!lobbiesIframe || !lobbiesIframe.contentWindow) {
|
||||
logger.info('The lobbies iframe is not opened, not sending message.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -745,7 +666,7 @@ namespace gdjs {
|
||||
runtimeScene
|
||||
);
|
||||
// Do as if the player left the lobby.
|
||||
handleLobbyLeaveEvent();
|
||||
handleLeaveLobbyEvent();
|
||||
removeLobbiesContainer(runtimeScene);
|
||||
focusOnGame(runtimeScene);
|
||||
return;
|
||||
@@ -774,15 +695,22 @@ namespace gdjs {
|
||||
let heartbeatUrl = `${rootApi}/play/game/${gameId}/public-lobby/${_lobbyId}/action/heartbeat`;
|
||||
headers['Authorization'] = `player-game-token ${playerToken}`;
|
||||
heartbeatUrl += `?playerId=${playerId}`;
|
||||
const players = gdjs.multiplayerMessageManager.getConnectedPlayers();
|
||||
await fetch(heartbeatUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
players,
|
||||
}),
|
||||
});
|
||||
}, heartbeatInterval);
|
||||
}
|
||||
|
||||
// If we are connected to players, then the game can start.
|
||||
logger.info('Lobby game has started.');
|
||||
// In case we're joining an existing lobby, read the saved messages to catch-up with the game state.
|
||||
gdjs.multiplayerMessageManager.handleSavedUpdateMessages(runtimeScene);
|
||||
_isReadyToSendOrReceiveGameUpdateMessages = true;
|
||||
_hasLobbyGameJustStarted = true;
|
||||
_isLobbyGameRunning = true;
|
||||
removeLobbiesContainer(runtimeScene);
|
||||
@@ -802,8 +730,8 @@ namespace gdjs {
|
||||
_hasLobbyGameJustEnded = true;
|
||||
_isLobbyGameRunning = false;
|
||||
_lobbyId = null;
|
||||
_lobby = null;
|
||||
playerNumber = null;
|
||||
_isReadyToSendOrReceiveGameUpdateMessages = false;
|
||||
if (_lobbyHeartbeatInterval) {
|
||||
clearInterval(_lobbyHeartbeatInterval);
|
||||
}
|
||||
@@ -812,15 +740,22 @@ namespace gdjs {
|
||||
gdjs.multiplayerPeerJsHelper.disconnectFromAllPeers();
|
||||
|
||||
// Clear the expected acknowledgments, as the game is ending.
|
||||
gdjs.multiplayerMessageManager.clearExpectedMessageAcknowledgements();
|
||||
gdjs.multiplayerMessageManager.clearAllMessagesTempData();
|
||||
};
|
||||
|
||||
/**
|
||||
* When the game receives the information of the peerId, then
|
||||
* the player can connect to the peer.
|
||||
*/
|
||||
const handlePeerIdEvent = function ({ peerId }: { peerId: string }) {
|
||||
// When a peerId is received, trigger a P2P connection with the peer.
|
||||
const handlePeerIdEvent = function ({
|
||||
peerId,
|
||||
compressionMethod,
|
||||
}: {
|
||||
peerId: string;
|
||||
compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;
|
||||
}) {
|
||||
// When a peerId is received, trigger a P2P connection with the peer, just after setting the compression method.
|
||||
gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);
|
||||
const currentPeerId = gdjs.multiplayerPeerJsHelper.getCurrentId();
|
||||
if (!currentPeerId) {
|
||||
logger.error(
|
||||
@@ -842,7 +777,7 @@ namespace gdjs {
|
||||
* players in the lobby via the websocket.
|
||||
* It will then trigger an event from the websocket to all players in the lobby.
|
||||
*/
|
||||
const handleGameCountdownStartMessage = function () {
|
||||
const handleStartGameCountdownMessage = function () {
|
||||
if (!_websocket) {
|
||||
logger.error(
|
||||
'No connection to send the start countdown message. Are you connected to a lobby?'
|
||||
@@ -863,7 +798,7 @@ namespace gdjs {
|
||||
* players in the lobby via the websocket.
|
||||
* It will then trigger an event from the websocket to all players in the lobby.
|
||||
*/
|
||||
const handleGameStartMessage = function () {
|
||||
const handleStartGameMessage = function () {
|
||||
if (!_websocket) {
|
||||
logger.error(
|
||||
'No connection to send the start countdown message. Are you connected to a lobby?'
|
||||
@@ -877,6 +812,48 @@ namespace gdjs {
|
||||
connectionType: 'lobby',
|
||||
})
|
||||
);
|
||||
|
||||
// As the host, start sending messages to the players.
|
||||
_isReadyToSendOrReceiveGameUpdateMessages = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* When the game receives a join game message from the lobby, send it via the WS
|
||||
* waiting for a peerId to be received and that the connection happens automatically.
|
||||
*/
|
||||
const handleJoinGameMessage = function () {
|
||||
if (!_websocket) {
|
||||
logger.error(
|
||||
'No connection to send the start countdown message. Are you connected to a lobby?'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_websocket.send(
|
||||
JSON.stringify({
|
||||
action: 'joinGame',
|
||||
connectionType: 'lobby',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the first heartbeat is received, we consider the connection to the host as working,
|
||||
* we inform the backend services that the connection is ready, so it can start the game when
|
||||
* everyone is ready.
|
||||
*/
|
||||
export const markConnectionAsConnected = function () {
|
||||
if (!_websocket) {
|
||||
return;
|
||||
}
|
||||
|
||||
_websocket.send(
|
||||
JSON.stringify({
|
||||
action: 'updateConnection',
|
||||
connectionType: 'lobby',
|
||||
status: 'connected',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -941,7 +918,7 @@ namespace gdjs {
|
||||
* Helper to send the ID from PeerJS to the lobby players.
|
||||
*/
|
||||
const sendPeerId = function () {
|
||||
if (!_websocket || !_lobby) {
|
||||
if (!_websocket) {
|
||||
logger.error(
|
||||
'No connection to send the message. Are you connected to a lobby?'
|
||||
);
|
||||
@@ -997,19 +974,23 @@ namespace gdjs {
|
||||
throw new Error('Malformed message.');
|
||||
}
|
||||
|
||||
handleLobbyJoinEvent(runtimeScene, event.data.lobbyId);
|
||||
handleJoinLobbyEvent(runtimeScene, event.data.lobbyId);
|
||||
break;
|
||||
}
|
||||
case 'startGameCountdown': {
|
||||
handleGameCountdownStartMessage();
|
||||
handleStartGameCountdownMessage();
|
||||
break;
|
||||
}
|
||||
case 'startGame': {
|
||||
handleGameStartMessage();
|
||||
handleStartGameMessage();
|
||||
break;
|
||||
}
|
||||
case 'leaveLobby': {
|
||||
handleLobbyLeaveEvent();
|
||||
handleLeaveLobbyEvent();
|
||||
break;
|
||||
}
|
||||
case 'joinGame': {
|
||||
handleJoinGameMessage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1247,9 +1228,9 @@ namespace gdjs {
|
||||
/**
|
||||
* Action to allow the player to leave the lobby in-game.
|
||||
*/
|
||||
export const leaveGameLobby = async (runtimeScene: gdjs.RuntimeScene) => {
|
||||
export const leaveGameLobby = async () => {
|
||||
// Handle the case where the game has not started yet, so the player is in the lobby.
|
||||
handleLobbyLeaveEvent();
|
||||
handleLeaveLobbyEvent();
|
||||
// Handle the case where the game has started, so the player is in the game and connected to other players.
|
||||
handleLobbyGameEnded();
|
||||
};
|
||||
|
@@ -254,6 +254,11 @@ namespace gdjs {
|
||||
connection.on('close', () => {
|
||||
_onDisconnect(connection.peer);
|
||||
});
|
||||
connection.on('iceStateChanged', (state) => {
|
||||
if (state === 'disconnected') {
|
||||
_onDisconnect(connection.peer);
|
||||
}
|
||||
});
|
||||
|
||||
// Regularly check for disconnection as the built in way is not reliable.
|
||||
(function disconnectChecker() {
|
||||
@@ -428,7 +433,7 @@ namespace gdjs {
|
||||
export const isReady = () => ready;
|
||||
|
||||
/**
|
||||
* Return any disconnected peers.
|
||||
* Return peers that have disconnected in the frame.
|
||||
*/
|
||||
export const getJustDisconnectedPeers = () => justDisconnectedPeers;
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -106,7 +106,9 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
onSceneResumed() {
|
||||
this._createElement();
|
||||
// The input must have been destroyed when the scene was paused,
|
||||
// in case it still exists, skip recreation.
|
||||
if (!this._input) this._createElement();
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
|
@@ -715,7 +715,7 @@ const defineSimpleTileMap = function (extension, _, gd) {
|
||||
const object = extension
|
||||
.addObject(
|
||||
'SimpleTileMap',
|
||||
_('Tilemap'),
|
||||
_('Tile map'),
|
||||
_('Displays a tiled-based map.'),
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
objectSimpleTileMap
|
||||
@@ -734,78 +734,304 @@ const defineSimpleTileMap = function (extension, _, gd) {
|
||||
.addIncludeFile('Extensions/TileMap/helper/TileMapHelper.js');
|
||||
|
||||
object
|
||||
.addCondition(
|
||||
'Tile',
|
||||
_('Tile id'),
|
||||
_('Check tile id at coordinates.'),
|
||||
_('The tile id at coordinates _PARAM3_;_PARAM3_'),
|
||||
.addExpression(
|
||||
'TileCenterX',
|
||||
_('Scene X coordinate of tile'),
|
||||
_('Get the scene X position of the center of the tile.'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tilemap'), '', false)
|
||||
.useStandardRelationalOperatorParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions()
|
||||
)
|
||||
.addParameter('number', _('X'), '', false)
|
||||
.addParameter('number', _('Y'), '', false)
|
||||
.setFunctionName('getTileAt');
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.setFunctionName('getSceneXCoordinateOfTileCenter');
|
||||
|
||||
object
|
||||
.addExpression(
|
||||
'Tile',
|
||||
_('Tile id'),
|
||||
_('Check tile id at coordinates.'),
|
||||
'TileCenterY',
|
||||
_('Scene Y coordinate of tile'),
|
||||
_('Get the scene Y position of the center of the tile.'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tilemap'), '', false)
|
||||
.addParameter('number', _('X'), '', false)
|
||||
.addParameter('number', _('Y'), '', false)
|
||||
.setFunctionName('getTileAt');
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.setFunctionName('getSceneYCoordinateOfTileCenter');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'Tile',
|
||||
_('Tile id'),
|
||||
_('Set tile id at coordinates.'),
|
||||
.addExpression(
|
||||
'GridX',
|
||||
_('Tile map grid column coordinate'),
|
||||
_(
|
||||
'tile at coordinates _PARAM3_;_PARAM4_ (flip horizontally _PARAM5_, flip vertically _PARAM6_)'
|
||||
'Get the grid column coordinates in the tile map corresponding to the scene coordinates.'
|
||||
),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tilemap'), '', false)
|
||||
.useStandardOperatorParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions()
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.setFunctionName('getColumnIndexAtPosition');
|
||||
|
||||
object
|
||||
.addExpression(
|
||||
'GridY',
|
||||
_('Tile map grid row coordinate'),
|
||||
_(
|
||||
'Get the grid row coordinates in the tile map corresponding to the scene coordinates.'
|
||||
),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('number', _('X'), '', false)
|
||||
.addParameter('number', _('Y'), '', false)
|
||||
.addParameter('yesorno', _('Flip horizontally'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.addParameter('yesorno', _('Flip vertically'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.setFunctionName('setTileAt')
|
||||
.setGetter('getTileAt');
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.setFunctionName('getRowIndexAtPosition');
|
||||
|
||||
object
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'TileIdAtPosition',
|
||||
_('Tile (at position)'),
|
||||
_('the id of the tile at the scene coordinates'),
|
||||
_('the tile id at scene coordinates _PARAM3_ ; _PARAM4_'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.addParameter('number', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.setFunctionName('setTileAtPosition')
|
||||
.setGetter('getTileAtPosition');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'removeTileAt',
|
||||
_('Remove tile'),
|
||||
_('Remove the tile at the given coordinates'),
|
||||
_('Remove tile at coordinates _PARAM1_;_PARAM2_'),
|
||||
'FlipTileOnYAtPosition',
|
||||
_('Flip tile vertically (at position)'),
|
||||
_('Flip tile vertically at scene coordinates.'),
|
||||
_(
|
||||
'Flip tile vertically at scene coordinates _PARAM1_ ; _PARAM2_: _PARAM3_'
|
||||
),
|
||||
_('Effects'),
|
||||
'res/actions/flipY24.png',
|
||||
'res/actions/flipY.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.addParameter('yesorno', _('Flip vertically'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.setFunctionName('flipTileOnYAtPosition');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'FlipTileOnXAtPosition',
|
||||
_('Flip tile horizontally (at position)'),
|
||||
_('Flip tile horizontally at scene coordinates.'),
|
||||
_(
|
||||
'Flip tile horizontally at scene coordinates _PARAM1_ ; _PARAM2_: _PARAM3_'
|
||||
),
|
||||
_('Effects'),
|
||||
'res/actions/flipX24.png',
|
||||
'res/actions/flipX.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.addParameter('yesorno', _('Flip horizontally'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.setFunctionName('flipTileOnXAtPosition');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'RemoveTileAtPosition',
|
||||
_('Remove tile (at position)'),
|
||||
_('Remove the tile at the scene coordinates.'),
|
||||
_('Remove tile at scene coordinates _PARAM1_ ; _PARAM2_'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('X'), '', false)
|
||||
.addParameter('number', _('Y'), '', false)
|
||||
.addParameter('number', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('removeTileAt');
|
||||
.setFunctionName('removeTileAtPosition');
|
||||
|
||||
object
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'TileIdAtGrid',
|
||||
_('Tile (on the grid)'),
|
||||
_('the id of the tile at the grid coordinates'),
|
||||
_('the tile id at grid coordinates _PARAM2_ ; _PARAM3_'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.setFunctionName('setTileAtGridCoordinates')
|
||||
.setGetter('getTileAtGridCoordinates');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'FlipTileOnYAtGridCoordinates',
|
||||
_('Flip tile vertically (on the grid)'),
|
||||
_('Flip tile vertically at grid coordinates.'),
|
||||
_(
|
||||
'Flip tile vertically at grid coordinates _PARAM1_ ; _PARAM2_: _PARAM3_'
|
||||
),
|
||||
_('Effects'),
|
||||
'res/actions/flipY24.png',
|
||||
'res/actions/flipY.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.addParameter('yesorno', _('Flip vertically'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.setFunctionName('flipTileOnYAtGridCoordinates');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'FlipTileOnXAtGridCoordinates',
|
||||
_('Flip tile horizontally (on the grid)'),
|
||||
_('Flip tile horizontally at grid coordinates.'),
|
||||
_(
|
||||
'Flip tile horizontally at grid coordinates _PARAM1_ ; _PARAM2_: _PARAM3_'
|
||||
),
|
||||
_('Effects'),
|
||||
'res/actions/flipX24.png',
|
||||
'res/actions/flipX.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.addParameter('yesorno', _('Flip horizontally'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.setFunctionName('flipTileOnXAtGridCoordinates');
|
||||
|
||||
object
|
||||
.addAction(
|
||||
'RemoveTileAtGridCoordinates',
|
||||
_('Remove tile (on the grid)'),
|
||||
_('Remove the tile at the grid coordinates.'),
|
||||
_('Remove tile at grid coordinates _PARAM1_ ; _PARAM2_'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('removeTileAtGridCoordinates');
|
||||
|
||||
object
|
||||
.addCondition(
|
||||
'IsTileFlippedOnXAtPosition',
|
||||
_('Tile flipped horizontally (at position)'),
|
||||
_('Check if tile at scene coordinates is flipped horizontally.'),
|
||||
_(
|
||||
'The tile at scene coordinates _PARAM1_ ; _PARAM2_ is flipped horizontally'
|
||||
),
|
||||
_('Effects'),
|
||||
'res/actions/flipX24.png',
|
||||
'res/actions/flipX.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('isTileFlippedOnXAtPosition');
|
||||
|
||||
object
|
||||
.addCondition(
|
||||
'IsTileFlippedOnYAtPosition',
|
||||
_('Tile flipped vertically (at position)'),
|
||||
_('Check if tile at scene coordinates is flipped vertically.'),
|
||||
_(
|
||||
'The tile at scene coordinates _PARAM1_ ; _PARAM2_ is flipped vertically'
|
||||
),
|
||||
_('Effects'),
|
||||
'res/actions/flipY24.png',
|
||||
'res/actions/flipY.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('isTileFlippedOnYAtPosition');
|
||||
|
||||
object
|
||||
.addCondition(
|
||||
'IsTileFlippedOnXAtGridCoordinates',
|
||||
_('Tile flipped horizontally (on the grid)'),
|
||||
_('Check if tile at grid coordinates is flipped horizontally.'),
|
||||
_(
|
||||
'The tile at grid coordinates _PARAM1_ ; _PARAM2_ is flipped horizontally'
|
||||
),
|
||||
_('Effects'),
|
||||
'res/actions/flipX24.png',
|
||||
'res/actions/flipX.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('isTileFlippedOnXAtGridCoordinates');
|
||||
|
||||
object
|
||||
.addCondition(
|
||||
'IsTileFlippedOnYAtGridCoordinates',
|
||||
_('Tile flipped vertically (on the grid)'),
|
||||
_('Check if tile at grid coordinates is flipped vertically.'),
|
||||
_(
|
||||
'The tile at grid coordinates _PARAM1_ ; _PARAM2_ is flipped vertically'
|
||||
),
|
||||
_('Effects'),
|
||||
'res/actions/flipY24.png',
|
||||
'res/actions/flipY.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('isTileFlippedOnYAtGridCoordinates');
|
||||
|
||||
object
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'GridRowCount',
|
||||
_('Grid row count'),
|
||||
_('the grid row count in the tile map'),
|
||||
_('the grid row count'),
|
||||
_('Size'),
|
||||
'res/actions/scaleHeight24_black.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('setGridRowCount')
|
||||
.setGetter('getGridRowCount');
|
||||
|
||||
object
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'GridColumnCount',
|
||||
_('Grid column count'),
|
||||
_('the grid column count in the tile map'),
|
||||
_('the grid column count'),
|
||||
_('Size'),
|
||||
'res/actions/scaleWidth24_black.png'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('setGridColumnCount')
|
||||
.setGetter('getGridColumnCount');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1237,7 +1463,7 @@ module.exports = {
|
||||
extension
|
||||
.setExtensionInformation(
|
||||
'TileMap',
|
||||
_('Tilemap'),
|
||||
_('Tile map'),
|
||||
_(
|
||||
"The Tilemap object can be used to display tile-based objects. It's a good way to create maps for RPG, strategy games or create objects by assembling tiles, useful for platformer, retro-looking games, etc..."
|
||||
),
|
||||
@@ -1309,12 +1535,6 @@ module.exports = {
|
||||
helpPagePath: '/objects/tilemap',
|
||||
})
|
||||
);
|
||||
objectsEditorService.registerEditorConfiguration(
|
||||
'TileMap::SimpleTileMap',
|
||||
objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({
|
||||
helpPagePath: '/objects/tilemap',
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Register renderers for instance of objects on the scene editor.
|
||||
@@ -1638,15 +1858,21 @@ module.exports = {
|
||||
* Renderer for instances of SimpleTileMap inside the IDE.
|
||||
*/
|
||||
class RenderedSimpleTileMapInstance extends RenderedInstance {
|
||||
_placeholderPixiObject = new PIXI.Text(
|
||||
'Select the instance\nand start painting',
|
||||
_placeholderTextPixiObject = new PIXI.Text(
|
||||
'Select this instance\nto start painting',
|
||||
new PIXI.TextStyle({
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 20,
|
||||
fontSize: 16,
|
||||
align: 'center',
|
||||
padding: 5,
|
||||
})
|
||||
);
|
||||
_placeholderImagePixiObject = new PIXI.Sprite(
|
||||
PIXI.Texture.from(
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAgMAAAAOFJJnAAAADFBMVEUAAAAkMoYsfqH///8FP6xgAAAAAXRSTlMAQObYZgAAAAFiS0dEAxEMTPIAAAAjSURBVBjTpcYxAQAADIMwTGISlTsmoVcCQClzSmvNo2ueGnMajGpBwI5BnwAAAABJRU5ErkJggg=='
|
||||
)
|
||||
);
|
||||
_placeholderPixiObject = new PIXI.Container();
|
||||
|
||||
constructor(
|
||||
project,
|
||||
@@ -1683,7 +1909,7 @@ module.exports = {
|
||||
localPosition
|
||||
);
|
||||
} else {
|
||||
this._placeholderPixiObject.worldTransform.applyInverse(
|
||||
this._placeholderImagePixiObject.worldTransform.applyInverse(
|
||||
position,
|
||||
localPosition
|
||||
);
|
||||
@@ -1696,9 +1922,15 @@ module.exports = {
|
||||
localPosition.y < this.height
|
||||
);
|
||||
};
|
||||
this._placeholderPixiObject.interactive = true;
|
||||
this._placeholderPixiObject.anchor.x = 0.5;
|
||||
this._placeholderPixiObject.anchor.y = 0.5;
|
||||
this._placeholderTextPixiObject.interactive = true;
|
||||
this._placeholderImagePixiObject.interactive = true;
|
||||
this._placeholderTextPixiObject.anchor.x = 0.5;
|
||||
this._placeholderTextPixiObject.anchor.y = 0.5;
|
||||
this._placeholderTextPixiObject.y = 30;
|
||||
this._placeholderImagePixiObject.y = -30;
|
||||
this._placeholderImagePixiObject.x = -16;
|
||||
this._placeholderPixiObject.addChild(this._placeholderTextPixiObject);
|
||||
this._placeholderPixiObject.addChild(this._placeholderImagePixiObject);
|
||||
this._pixiObject.addChild(this._placeholderPixiObject);
|
||||
this._pixiContainer.addChild(this._pixiObject);
|
||||
this.width = 48;
|
||||
|
@@ -80,7 +80,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: object,
|
||||
tileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
|
2
Extensions/TileMap/helper/TileMapHelper.d.ts
vendored
2
Extensions/TileMap/helper/TileMapHelper.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
EditableTileMap,
|
||||
EditableTileMapLayer,
|
||||
EditableTileMapAsJsObject,
|
||||
PixiTileMapHelper,
|
||||
TileDefinition,
|
||||
TileMapFileContent,
|
||||
@@ -14,6 +15,7 @@ declare global {
|
||||
export {
|
||||
EditableTileMap,
|
||||
EditableTileMapLayer,
|
||||
EditableTileMapAsJsObject,
|
||||
PixiTileMapHelper,
|
||||
TileDefinition,
|
||||
TileMapFileContent,
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,4 +2,16 @@ export declare type integer = number;
|
||||
export declare type float = number;
|
||||
export type FloatPoint = [float, float];
|
||||
export type PolygonVertices = FloatPoint[];
|
||||
export type EditableTileMapLayerAsJsObject = {
|
||||
id: number;
|
||||
alpha: number;
|
||||
tiles: number[][];
|
||||
};
|
||||
export type EditableTileMapAsJsObject = {
|
||||
tileWidth: number;
|
||||
tileHeight: number;
|
||||
dimX: number;
|
||||
dimY: number;
|
||||
layers: EditableTileMapLayerAsJsObject[];
|
||||
};
|
||||
//# sourceMappingURL=CommonTypes.d.ts.map
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"CommonTypes.d.ts","sourceRoot":"","sources":["../../src/model/CommonTypes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,MAAM,OAAO,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,MAAM,CAAC;AACnC,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,UAAU,EAAE,CAAC"}
|
||||
{"version":3,"file":"CommonTypes.d.ts","sourceRoot":"","sources":["../../src/model/CommonTypes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,MAAM,OAAO,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,MAAM,CAAC;AACnC,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,UAAU,EAAE,CAAC;AAE3C,MAAM,MAAM,8BAA8B,GAAG;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,8BAA8B,EAAE,CAAC;CAC1C,CAAC"}
|
@@ -1,4 +1,10 @@
|
||||
import { PolygonVertices, integer, float } from './CommonTypes';
|
||||
import {
|
||||
PolygonVertices,
|
||||
integer,
|
||||
float,
|
||||
EditableTileMapAsJsObject,
|
||||
EditableTileMapLayerAsJsObject,
|
||||
} from './CommonTypes';
|
||||
/**
|
||||
* A tile map model.
|
||||
*
|
||||
@@ -27,6 +33,10 @@ export declare class EditableTileMap {
|
||||
* The number of tile rows in the map.
|
||||
*/
|
||||
private dimY;
|
||||
/**
|
||||
* True if is allowed to set a tile out of the tile map's bounds.
|
||||
* Useful when editing the tile map easily.
|
||||
*/
|
||||
/**
|
||||
* @param tileWidth The width of a tile.
|
||||
* @param tileHeight The height of a tile.
|
||||
@@ -50,7 +60,7 @@ export declare class EditableTileMap {
|
||||
* @param objectConfiguration
|
||||
*/
|
||||
static from(
|
||||
editableTileMapAsJsObject: any,
|
||||
editableTileMapAsJsObject: EditableTileMapAsJsObject,
|
||||
{
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
@@ -87,10 +97,24 @@ export declare class EditableTileMap {
|
||||
*/
|
||||
getDimensionY(): integer;
|
||||
/**
|
||||
* Changes the number of columns in the tile map by adding/removing
|
||||
* columns at the end.
|
||||
* @param dim The number of tile columns in the map.
|
||||
*/
|
||||
setDimensionX(dim: integer): void;
|
||||
/**
|
||||
* Increases dimensions of the tile map by adding columns and rows
|
||||
* at the start and/or at the end of the grid.
|
||||
*/
|
||||
increaseDimensions(
|
||||
columnsToAppend: number,
|
||||
columnsToUnshift: number,
|
||||
rowsToAppend: number,
|
||||
rowsToUnshift: number
|
||||
): void;
|
||||
/**
|
||||
* Changes the number of row in the tile map by adding/removing
|
||||
* rows at the end.
|
||||
* @param dim The number of tile rows in the map.
|
||||
*/
|
||||
setDimensionY(dim: integer): void;
|
||||
@@ -107,11 +131,11 @@ export declare class EditableTileMap {
|
||||
* @param id The identifier of the new layer.
|
||||
* @returns The new layer.
|
||||
*/
|
||||
addTileLayer(id: integer): EditableTileMapLayer;
|
||||
addNewTileLayer(id: integer): EditableTileMapLayer;
|
||||
/**
|
||||
* @param layer the new layer to set.
|
||||
*/
|
||||
setTileLayer(layer: EditableTileMapLayer): void;
|
||||
addTileLayer(layer: EditableTileMapLayer): void;
|
||||
getTileLayer(id: integer): EditableTileMapLayer | null;
|
||||
/**
|
||||
* @param id The identifier of the new layer.
|
||||
@@ -146,6 +170,23 @@ export declare class EditableTileMap {
|
||||
* Returns true if all layers contain no defined tiled.
|
||||
*/
|
||||
isEmpty(): boolean;
|
||||
getTileId(x: integer, y: integer, layerId: integer): integer;
|
||||
setTile(x: integer, y: integer, layerId: integer, tileId: number): void;
|
||||
flipTileOnY(x: integer, y: integer, layerId: integer, flip: boolean): void;
|
||||
flipTileOnX(x: integer, y: integer, layerId: integer, flip: boolean): void;
|
||||
isTileFlippedOnX(x: integer, y: integer, layerId: integer): boolean;
|
||||
isTileFlippedOnY(x: integer, y: integer, layerId: integer): boolean;
|
||||
removeTile(x: integer, y: integer, layerId: integer): void;
|
||||
trimEmptyColumnsAndRowToFitLayer(
|
||||
layerId: integer
|
||||
):
|
||||
| {
|
||||
poppedRows: number;
|
||||
poppedColumns: number;
|
||||
shiftedRows: number;
|
||||
shiftedColumns: number;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
/**
|
||||
* A tile map layer.
|
||||
@@ -241,7 +282,7 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
constructor(tileMap: EditableTileMap, id: integer);
|
||||
buildEmptyLayer(dimensionX: number, dimensionY: number): void;
|
||||
static from(
|
||||
editableTileMapLayerAsJsObject: any,
|
||||
editableTileMapLayerAsJsObject: EditableTileMapLayerAsJsObject,
|
||||
tileMap: EditableTileMap,
|
||||
isTileIdValid: (tileId: number) => boolean
|
||||
): EditableTileMapLayer;
|
||||
@@ -271,38 +312,19 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileId The tile.
|
||||
* @param options Flipping options.
|
||||
*/
|
||||
setTile(
|
||||
x: integer,
|
||||
y: integer,
|
||||
tileId: integer,
|
||||
options?:
|
||||
| {
|
||||
flipVertically: boolean;
|
||||
flipHorizontally: boolean;
|
||||
flipDiagonally: boolean;
|
||||
}
|
||||
| undefined
|
||||
):
|
||||
| {
|
||||
unshiftedRows: number;
|
||||
unshiftedColumns: number;
|
||||
appendedRows: number;
|
||||
appendedColumns: number;
|
||||
}
|
||||
| undefined;
|
||||
setTile(x: integer, y: integer, tileId: integer): void;
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileGID The tile GID.
|
||||
*/
|
||||
setTileGID(x: integer, y: integer, tileGID: integer): void;
|
||||
trimEmptyColumnsAndRow(): {
|
||||
shiftedRows: number;
|
||||
shiftedColumns: number;
|
||||
poppedRows: number;
|
||||
poppedColumns: number;
|
||||
getTrimmingData(): {
|
||||
rowsToShift: number;
|
||||
columnsToShift: number;
|
||||
rowsToPop: number;
|
||||
columnsToPop: number;
|
||||
};
|
||||
/**
|
||||
* @param x The layer column.
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
import { EditableTileMap } from '../model/TileMapModel';
|
||||
import { TileTextureCache } from './TileTextureCache';
|
||||
import { TileMapFileContent } from '../load/TileMapFileContent';
|
||||
import { EditableTileMapAsJsObject } from '../model/CommonTypes';
|
||||
/**
|
||||
* A holder to share tile maps across the 2 extension objects.
|
||||
*
|
||||
@@ -44,7 +45,7 @@ export declare class TileMapManager {
|
||||
callback: (tileMap: EditableTileMap | null) => void
|
||||
): void;
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: object,
|
||||
tileMapAsJsObject: EditableTileMapAsJsObject,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
tileSetColumnCount: number,
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI;IAwBrD;;;;;;;OAOG;IACH,gBAAgB,CACd,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAiCP,sBAAsB,CACpB,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,MAAM,EAGvB,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAC3C,IAAI;IAYP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP;;;;;;;OAOG;IACH,kCAAkC,CAChC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAqBP,WAAW,IAAI,IAAI;CAIpB"}
|
||||
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEjE;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI;IAwBrD;;;;;;;OAOG;IACH,gBAAgB,CACd,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAiCP,sBAAsB,CACpB,iBAAiB,EAAE,yBAAyB,EAC5C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,MAAM,EAGvB,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAC3C,IAAI;IAeP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP;;;;;;;OAOG;IACH,kCAAkC,CAChC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAqBP,WAAW,IAAI,IAAI;CAIpB"}
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,yBAAiB,iBAAiB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI,CAuBzB;IAED;;;;;;;;OAQG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAC7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAiBlB;IAED;;;;;;;;;;;;OAYG;IACH,SAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI,CA0GN;IAED;;OAEG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI,CAgEN;CACF"}
|
||||
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,yBAAiB,iBAAiB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI,CAuBzB;IAED;;;;;;;;OAQG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAC7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAqBlB;IAED;;;;;;;;;;;;OAYG;IACH,SAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI,CA0GN;IAED;;OAEG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI,CAgEN;CACF"}
|
@@ -45,10 +45,11 @@ namespace gdjs {
|
||||
_tileSize: number;
|
||||
_displayMode = 'all';
|
||||
_layerIndex = 0;
|
||||
_initialTileMapAsJsObject: object | null = null;
|
||||
_initialTileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject | null = null;
|
||||
_initialTilesWithHitBox: number[];
|
||||
_isTileMapDirty: boolean = false;
|
||||
_sceneToTileMapTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
|
||||
_tileMapToSceneTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
|
||||
_collisionTileMap: gdjs.TileMap.TransformedCollisionTileMap | null = null;
|
||||
_hitBoxTag: string = 'collision';
|
||||
private _transformationIsUpToDate: boolean = false;
|
||||
@@ -130,6 +131,7 @@ namespace gdjs {
|
||||
// TODO: support changing the atlas texture
|
||||
return false;
|
||||
}
|
||||
// Map content is updated at hot-reload by extraInitializationFromInitialInstance.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -193,10 +195,18 @@ namespace gdjs {
|
||||
// 4. Update position (calculations based on renderer's dimensions).
|
||||
this._renderer.updatePosition();
|
||||
|
||||
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
|
||||
tileMap,
|
||||
this._hitBoxTag
|
||||
);
|
||||
if (this._collisionTileMap) {
|
||||
// If collision tile map is already defined, there's a good chance it means
|
||||
// extraInitializationFromInitialInstance is called when hot reloading the
|
||||
// scene so the collision is tile map is updated instead of being re-created.
|
||||
this._collisionTileMap.updateFromTileMap(tileMap);
|
||||
} else {
|
||||
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
|
||||
tileMap,
|
||||
this._hitBoxTag
|
||||
);
|
||||
}
|
||||
|
||||
this.updateTransformation();
|
||||
});
|
||||
}
|
||||
@@ -396,15 +406,9 @@ namespace gdjs {
|
||||
updateHitBoxes(): void {
|
||||
this.updateTransformation();
|
||||
if (!this._collisionTileMap) return;
|
||||
// Update the RuntimeObject hitboxes attribute.
|
||||
for (const _ of this._collisionTileMap.getAllHitboxes(this._hitBoxTag)) {
|
||||
// RuntimeObject.hitBoxes contains the same polygons instances as the
|
||||
// hitboxes from the tiles.
|
||||
//
|
||||
// When hitboxes for a tile is asked to the model, they are updated
|
||||
// according to the new object location if needed.
|
||||
// Iterating over all the tiles forces them to update their hitboxes.
|
||||
}
|
||||
this.hitBoxes = Array.from(
|
||||
this._collisionTileMap.getAllHitboxes(this._hitBoxTag)
|
||||
);
|
||||
this.hitBoxesDirty = false;
|
||||
this.updateAABB();
|
||||
}
|
||||
@@ -508,38 +512,73 @@ namespace gdjs {
|
||||
const absScaleX = Math.abs(this._renderer.getScaleX());
|
||||
const absScaleY = Math.abs(this._renderer.getScaleY());
|
||||
|
||||
this._sceneToTileMapTransformation.setToIdentity();
|
||||
this._tileMapToSceneTransformation.setToIdentity();
|
||||
|
||||
// Translation
|
||||
this._sceneToTileMapTransformation.translate(this.getX(), this.getY());
|
||||
this._tileMapToSceneTransformation.translate(this.getX(), this.getY());
|
||||
|
||||
// Rotation
|
||||
const angleInRadians = (this.getAngle() * Math.PI) / 180;
|
||||
this._sceneToTileMapTransformation.rotateAround(
|
||||
this._tileMapToSceneTransformation.rotateAround(
|
||||
angleInRadians,
|
||||
this.getCenterX(),
|
||||
this.getCenterY()
|
||||
);
|
||||
|
||||
// Scale
|
||||
this._sceneToTileMapTransformation.scale(absScaleX, absScaleY);
|
||||
this._tileMapToSceneTransformation.scale(absScaleX, absScaleY);
|
||||
if (this._collisionTileMap) {
|
||||
const collisionTileMapTransformation = this._collisionTileMap.getTransformation();
|
||||
collisionTileMapTransformation.copyFrom(
|
||||
this._sceneToTileMapTransformation
|
||||
this._tileMapToSceneTransformation
|
||||
);
|
||||
this._collisionTileMap.setTransformation(
|
||||
collisionTileMapTransformation
|
||||
);
|
||||
}
|
||||
this._sceneToTileMapTransformation.copyFrom(
|
||||
this._tileMapToSceneTransformation
|
||||
);
|
||||
this._sceneToTileMapTransformation.invert();
|
||||
this._transformationIsUpToDate = true;
|
||||
}
|
||||
|
||||
getSceneXCoordinateOfTileCenter(
|
||||
columnIndex: integer,
|
||||
rowIndex: integer
|
||||
): float {
|
||||
const sceneCoordinates: FloatPoint =
|
||||
SimpleTileMapRuntimeObject.workingPoint;
|
||||
this._tileMapToSceneTransformation.transform(
|
||||
[
|
||||
(columnIndex + 0.5) * this._tileSize,
|
||||
(rowIndex + 0.5) * this._tileSize,
|
||||
],
|
||||
sceneCoordinates
|
||||
);
|
||||
return sceneCoordinates[0];
|
||||
}
|
||||
|
||||
getSceneYCoordinateOfTileCenter(
|
||||
columnIndex: integer,
|
||||
rowIndex: integer
|
||||
): float {
|
||||
const sceneCoordinates: FloatPoint =
|
||||
SimpleTileMapRuntimeObject.workingPoint;
|
||||
this._tileMapToSceneTransformation.transform(
|
||||
[
|
||||
(columnIndex + 0.5) * this._tileSize,
|
||||
(rowIndex + 0.5) * this._tileSize,
|
||||
],
|
||||
sceneCoordinates
|
||||
);
|
||||
return sceneCoordinates[1];
|
||||
}
|
||||
|
||||
getGridCoordinatesFromSceneCoordinates(
|
||||
x: number,
|
||||
y: number
|
||||
): [number, number] {
|
||||
x: float,
|
||||
y: float
|
||||
): [integer, integer] {
|
||||
this.updateTransformation();
|
||||
|
||||
const gridCoordinates: FloatPoint =
|
||||
@@ -552,87 +591,142 @@ namespace gdjs {
|
||||
return [columnIndex, rowIndex];
|
||||
}
|
||||
|
||||
getTileAt(x: number, y: number): integer {
|
||||
getColumnIndexAtPosition(x: float, y: float): integer {
|
||||
return this.getGridCoordinatesFromSceneCoordinates(x, y)[0];
|
||||
}
|
||||
|
||||
getRowIndexAtPosition(x: float, y: float): integer {
|
||||
return this.getGridCoordinatesFromSceneCoordinates(x, y)[1];
|
||||
}
|
||||
|
||||
getTileAtPosition(x: float, y: float): integer {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
return this.getTileAtGridCoordinates(columnIndex, rowIndex);
|
||||
}
|
||||
|
||||
getTileAtGridCoordinates(columnIndex: integer, rowIndex: integer): integer {
|
||||
return this._renderer.getTileId(columnIndex, rowIndex, 0);
|
||||
}
|
||||
|
||||
setTileAt(
|
||||
tileId: number,
|
||||
x: number,
|
||||
y: number,
|
||||
flipHorizontally: boolean,
|
||||
flipVertically: boolean
|
||||
) {
|
||||
setTileAtPosition(tileId: number, x: float, y: float) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
const addedData = this._renderer.setTileId(
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
0,
|
||||
tileId,
|
||||
{ flipHorizontally, flipVertically, flipDiagonally: false }
|
||||
);
|
||||
this._isTileMapDirty = true;
|
||||
if (addedData) {
|
||||
const {
|
||||
unshiftedRows,
|
||||
unshiftedColumns,
|
||||
appendedColumns,
|
||||
appendedRows,
|
||||
} = addedData;
|
||||
const scaleX = this.getScaleX();
|
||||
const scaleY = this.getScaleY();
|
||||
this.setX(this.getX() - unshiftedColumns * (this._tileSize * scaleX));
|
||||
this.setY(this.getY() - unshiftedRows * (this._tileSize * scaleY));
|
||||
if (
|
||||
unshiftedColumns > 0 ||
|
||||
unshiftedRows > 0 ||
|
||||
appendedColumns > 0 ||
|
||||
appendedRows > 0
|
||||
) {
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
this.setTileAtGridCoordinates(tileId, columnIndex, rowIndex);
|
||||
}
|
||||
|
||||
removeTileAt(x: number, y: number) {
|
||||
setTileAtGridCoordinates(
|
||||
tileId: number,
|
||||
columnIndex: integer,
|
||||
rowIndex: integer
|
||||
) {
|
||||
this._renderer.setTileId(columnIndex, rowIndex, 0, tileId);
|
||||
this._isTileMapDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
flipTileOnYAtPosition(x: float, y: float, flip: boolean) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
this.flipTileOnYAtGridCoordinates(columnIndex, rowIndex, flip);
|
||||
}
|
||||
|
||||
flipTileOnXAtPosition(x: float, y: float, flip: boolean) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
this.flipTileOnXAtGridCoordinates(columnIndex, rowIndex, flip);
|
||||
}
|
||||
|
||||
flipTileOnYAtGridCoordinates(
|
||||
columnIndex: integer,
|
||||
rowIndex: integer,
|
||||
flip: boolean
|
||||
) {
|
||||
this._renderer.flipTileOnY(columnIndex, rowIndex, 0, flip);
|
||||
this._isTileMapDirty = true;
|
||||
// No need to invalidate hit boxes since at the moment, collision mask
|
||||
// cannot be configured on each tile.
|
||||
}
|
||||
|
||||
flipTileOnXAtGridCoordinates(
|
||||
columnIndex: integer,
|
||||
rowIndex: integer,
|
||||
flip: boolean
|
||||
) {
|
||||
this._renderer.flipTileOnX(columnIndex, rowIndex, 0, flip);
|
||||
this._isTileMapDirty = true;
|
||||
// No need to invalidate hit boxes since at the moment, collision mask
|
||||
// cannot be configured on each tile.
|
||||
}
|
||||
|
||||
isTileFlippedOnXAtPosition(x: float, y: float) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
|
||||
return this._renderer.isTileFlippedOnX(columnIndex, rowIndex, 0);
|
||||
}
|
||||
|
||||
isTileFlippedOnXAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
||||
return this._renderer.isTileFlippedOnX(columnIndex, rowIndex, 0);
|
||||
}
|
||||
|
||||
isTileFlippedOnYAtPosition(x: float, y: float) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
|
||||
return this._renderer.isTileFlippedOnY(columnIndex, rowIndex, 0);
|
||||
}
|
||||
|
||||
isTileFlippedOnYAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
||||
return this._renderer.isTileFlippedOnY(columnIndex, rowIndex, 0);
|
||||
}
|
||||
|
||||
removeTileAtPosition(x: float, y: float) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
this.removeTileAtGridCoordinates(columnIndex, rowIndex);
|
||||
}
|
||||
|
||||
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
||||
this._renderer.removeTile(columnIndex, rowIndex, 0);
|
||||
this._isTileMapDirty = true;
|
||||
const removedData = this._renderer.trimEmptyColumnsAndRows(0);
|
||||
if (removedData) {
|
||||
const {
|
||||
shiftedRows,
|
||||
shiftedColumns,
|
||||
poppedColumns,
|
||||
poppedRows,
|
||||
} = removedData;
|
||||
this.setX(
|
||||
this.getX() + shiftedColumns * (this._tileSize * this.getScaleX())
|
||||
);
|
||||
this.setY(
|
||||
this.getY() + shiftedRows * (this._tileSize * this.getScaleY())
|
||||
);
|
||||
if (
|
||||
shiftedColumns > 0 ||
|
||||
shiftedRows > 0 ||
|
||||
poppedColumns > 0 ||
|
||||
poppedRows > 0
|
||||
) {
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
setGridRowCount(targetRowCount: integer) {
|
||||
if (targetRowCount <= 0) return;
|
||||
this._renderer.setGridRowCount(targetRowCount);
|
||||
this._isTileMapDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
setGridColumnCount(targetColumnCount: integer) {
|
||||
if (targetColumnCount <= 0) return;
|
||||
this._renderer.setGridColumnCount(targetColumnCount);
|
||||
this._isTileMapDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
getGridRowCount(): integer {
|
||||
return this._renderer.getGridRowCount();
|
||||
}
|
||||
|
||||
getGridColumnCount(): integer {
|
||||
return this._renderer.getGridColumnCount();
|
||||
}
|
||||
}
|
||||
gdjs.registerObject(
|
||||
|
@@ -93,6 +93,9 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
updateOpacity(): void {
|
||||
// TODO: Currently, the renderer does not use the object alpha to set
|
||||
// opacity. Setting alpha on each layer tile might not be useful as
|
||||
// each layer would be separately transparent instead of the whole tilemap.
|
||||
this._pixiObject.alpha = this._object._opacity / 255;
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
@@ -167,9 +170,7 @@ namespace gdjs {
|
||||
getTileId(x: integer, y: integer, layerIndex: integer): integer {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return -1;
|
||||
const layer = tileMap.getTileLayer(layerIndex);
|
||||
if (!layer) return -1;
|
||||
return layer.getTileId(x, y) || -1;
|
||||
return tileMap.getTileId(x, y, layerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,24 +179,56 @@ namespace gdjs {
|
||||
* @param layerIndex The layer index.
|
||||
* @param tileId The tile's id.
|
||||
*/
|
||||
setTileId(
|
||||
x: integer,
|
||||
y: integer,
|
||||
layerIndex: integer,
|
||||
tileId: number,
|
||||
options?:
|
||||
| {
|
||||
flipVertically: boolean;
|
||||
flipHorizontally: boolean;
|
||||
flipDiagonally: boolean;
|
||||
}
|
||||
| undefined
|
||||
) {
|
||||
setTileId(x: integer, y: integer, layerIndex: integer, tileId: number) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
const layer = tileMap.getTileLayer(layerIndex);
|
||||
if (!layer) return;
|
||||
return layer.setTile(x, y, tileId, options);
|
||||
return tileMap.setTile(x, y, layerIndex, tileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
* @param flip true if the tile should be flipped.
|
||||
*/
|
||||
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
tileMap.flipTileOnY(x, y, layerIndex, flip);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
* @param flip true if the tile should be flipped.
|
||||
*/
|
||||
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
tileMap.flipTileOnX(x, y, layerIndex, flip);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
*/
|
||||
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return false;
|
||||
return tileMap.isTileFlippedOnX(x, y, layerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
*/
|
||||
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return false;
|
||||
return tileMap.isTileFlippedOnY(x, y, layerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,19 +239,36 @@ namespace gdjs {
|
||||
removeTile(x: integer, y: integer, layerIndex: integer) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
const layer = tileMap.getTileLayer(layerIndex);
|
||||
if (!layer) return;
|
||||
return layer.removeTile(x, y);
|
||||
return tileMap.removeTile(x, y, layerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param layerIndex The layer index.
|
||||
* @param targetRowCount The number of rows to have.
|
||||
*/
|
||||
trimEmptyColumnsAndRows(layerIndex: integer) {
|
||||
setGridRowCount(targetRowCount: integer) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
const layer = tileMap.getTileLayer(layerIndex);
|
||||
if (!layer) return;
|
||||
return layer.trimEmptyColumnsAndRow();
|
||||
return tileMap.setDimensionY(targetRowCount);
|
||||
}
|
||||
/**
|
||||
* @param targetColumnCount The number of rows to have.
|
||||
*/
|
||||
setGridColumnCount(targetColumnCount: integer) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
return tileMap.setDimensionX(targetColumnCount);
|
||||
}
|
||||
|
||||
getGridRowCount(): integer {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return 0;
|
||||
return tileMap.getDimensionY();
|
||||
}
|
||||
|
||||
getGridColumnCount(): integer {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return 0;
|
||||
return tileMap.getDimensionX();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
|
@@ -413,7 +413,6 @@ namespace gdjs {
|
||||
setOpacity(opacity: float): void {
|
||||
this._opacity = opacity;
|
||||
this._renderer.updateOpacity();
|
||||
// TODO: Set all tile layers alpha and re-render tilemap.
|
||||
}
|
||||
|
||||
getOpacity(): float {
|
||||
|
@@ -17,6 +17,50 @@ namespace gdjs {
|
||||
|
||||
constructor() {}
|
||||
|
||||
getNetworkSyncData(): TweenManagerSyncData {
|
||||
const tweens: TweenManagerSyncData = {};
|
||||
for (const [identifier, tween] of this._tweens) {
|
||||
tweens[identifier] = {
|
||||
progress: tween.getProgress(),
|
||||
value: tween.getValue(),
|
||||
isPlaying: tween.isPlaying(),
|
||||
hasFinished: tween.hasFinished(),
|
||||
};
|
||||
}
|
||||
return tweens;
|
||||
}
|
||||
|
||||
// We only handle tweens partially for network sync.
|
||||
// We only sync the props of existing tweens, but do not handle
|
||||
// adding or removing tweens.
|
||||
updateFromNetworkSyncData(networkSyncData: TweenManagerSyncData) {
|
||||
for (const [identifier, tweenData] of Object.entries(
|
||||
networkSyncData
|
||||
)) {
|
||||
const tween = this._tweens.get(identifier);
|
||||
if (!tween) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
tweenData.isPlaying !== undefined &&
|
||||
tweenData.isPlaying !== tween.isPlaying()
|
||||
) {
|
||||
tweenData.isPlaying
|
||||
? this.resumeTween(identifier)
|
||||
: this.pauseTween(identifier);
|
||||
}
|
||||
if (
|
||||
tweenData.hasFinished === true &&
|
||||
tweenData.hasFinished !== tween.hasFinished()
|
||||
) {
|
||||
this.stopTween(identifier, false);
|
||||
}
|
||||
if (tweenData.progress !== undefined) {
|
||||
tween.setProgress(tweenData.progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make all active tween step toward the end.
|
||||
* @param timeDelta the duration from the previous step in seconds
|
||||
@@ -267,6 +311,7 @@ namespace gdjs {
|
||||
stop(jumpToDest: boolean): void;
|
||||
resume(): void;
|
||||
pause(): void;
|
||||
setProgress(progress: float): void;
|
||||
getProgress(): float;
|
||||
getValue(): float;
|
||||
}
|
||||
@@ -339,6 +384,11 @@ namespace gdjs {
|
||||
getProgress(): float {
|
||||
return this.elapsedTime / this.totalDuration;
|
||||
}
|
||||
|
||||
setProgress(progress: float): void {
|
||||
this.elapsedTime = progress * this.totalDuration;
|
||||
this._updateValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,6 +4,23 @@ Copyright (c) 2010-2023 Florian Rival (Florian.Rival@gmail.com)
|
||||
*/
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Tween');
|
||||
|
||||
interface TweenData {
|
||||
progress: number;
|
||||
value: number;
|
||||
isPlaying: boolean;
|
||||
hasFinished: boolean;
|
||||
}
|
||||
export type TweenManagerSyncData = Record<string, TweenData>;
|
||||
|
||||
interface TweenNetworkSyncDataType {
|
||||
tweens: TweenManagerSyncData;
|
||||
}
|
||||
|
||||
export interface TweenNetworkSyncData extends BehaviorNetworkSyncData {
|
||||
props: TweenNetworkSyncDataType;
|
||||
}
|
||||
|
||||
interface IColorable extends gdjs.RuntimeObject {
|
||||
setColor(color: string): void;
|
||||
getColor(): string;
|
||||
@@ -84,6 +101,21 @@ namespace gdjs {
|
||||
return true;
|
||||
}
|
||||
|
||||
getNetworkSyncData(): TweenNetworkSyncData {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
props: {
|
||||
tweens: this._tweens.getNetworkSyncData(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateFromNetworkSyncData(networkSyncData: TweenNetworkSyncData) {
|
||||
super.updateFromNetworkSyncData(networkSyncData);
|
||||
|
||||
this._tweens.updateFromNetworkSyncData(networkSyncData.props.tweens);
|
||||
}
|
||||
|
||||
doStepPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
this._tweens.step();
|
||||
}
|
||||
|
@@ -892,22 +892,28 @@ void CommonInstructionsExtension::GenerateLocalVariableInitializationCode(
|
||||
code += variableCodeName + ".setString(" +
|
||||
EventsCodeGenerator::ConvertToStringExplicit(variable.GetString()) +
|
||||
");\n";
|
||||
} else if (variable.GetType() == gd::Variable::Structure ||
|
||||
variable.GetType() == gd::Variable::Array) {
|
||||
} else if (variable.GetType() == gd::Variable::Structure) {
|
||||
const auto &childrenNames = variable.GetAllChildrenNames();
|
||||
for (const auto& childName : variable.GetAllChildrenNames()) {
|
||||
for (const auto &childName : variable.GetAllChildrenNames()) {
|
||||
auto &child = variable.GetChild(childName);
|
||||
|
||||
code += "{\n";
|
||||
GenerateLocalVariableInitializationCode(child, code, depth + 1);
|
||||
auto childCodeName = "variable" + gd::String::From(depth + 1);
|
||||
code += variableCodeName + ".addChild(" +
|
||||
EventsCodeGenerator::ConvertToStringExplicit(childName) +
|
||||
", " + childCodeName + ");\n";
|
||||
EventsCodeGenerator::ConvertToStringExplicit(childName) + ", " +
|
||||
childCodeName + ");\n";
|
||||
code += "}\n";
|
||||
}
|
||||
if (variable.GetType() == gd::Variable::Array) {
|
||||
code += variableCodeName + ".castTo('array');\n";
|
||||
} else if (variable.GetType() == gd::Variable::Array) {
|
||||
for (std::size_t i = 0; i < variable.GetChildrenCount(); i++) {
|
||||
auto &child = variable.GetAtIndex(i);
|
||||
|
||||
code += "{\n";
|
||||
GenerateLocalVariableInitializationCode(child, code, depth + 1);
|
||||
auto childCodeName = "variable" + gd::String::From(depth + 1);
|
||||
code += variableCodeName + "._pushVariable(" + childCodeName + ");\n";
|
||||
code += "}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget id="GDJS_PACKAGENAME" version="GDJS_PROJECTVERSION" xmlns="http://www.w3.org/ns/widgets"
|
||||
xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:cdv="http://cordova.apache.org/ns/1.0"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<name>GDJS_PROJECTNAME</name>
|
||||
<content src="index.html" />
|
||||
<plugin name="cordova-plugin-whitelist" version="1" />
|
||||
@@ -13,19 +14,24 @@
|
||||
<allow-intent href="mailto:*" />
|
||||
<allow-intent href="geo:*" />
|
||||
|
||||
<!-- Allow iframes on iOS like leaderboards, including those in development that are not served via https. -->
|
||||
<!-- Allow iframes on iOS like leaderboards, including those in development that are not served
|
||||
via https. -->
|
||||
<allow-navigation href="*" />
|
||||
|
||||
<platform name="android">
|
||||
<allow-intent href="market:*" />
|
||||
|
||||
<!-- Required by admob-plus plugin > version 2 -->
|
||||
<preference name="GradlePluginKotlinEnabled" value="true" />
|
||||
|
||||
<!-- Increase timeout value for low-end android devices -->
|
||||
<preference name="loadUrlTimeoutValue" value="60000" />
|
||||
|
||||
<!-- GDJS_ICONS_ANDROID -->
|
||||
<preference name="AndroidWindowSplashScreenBackground" value="#000000" />
|
||||
|
||||
<!-- Required to get cordova-plugin-safariviewcontroller to call Chrome CustomTabs on Android. -->
|
||||
<!-- Required to get cordova-plugin-safariviewcontroller to call Chrome CustomTabs on
|
||||
Android. -->
|
||||
<config-file target="AndroidManifest.xml" parent="/manifest">
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -40,6 +46,9 @@
|
||||
|
||||
<preference name="SwiftVersion" value="5.3" />
|
||||
|
||||
<!-- Required by admob-plus plugin > version 2 -->
|
||||
<preference name="deployment-target" value="12.0" />
|
||||
|
||||
<!-- GDJS_ICONS_IOS -->
|
||||
</platform>
|
||||
|
||||
|
@@ -395,9 +395,10 @@ namespace gdjs {
|
||||
let minX = 0;
|
||||
if (this._forcedDefaultSize) {
|
||||
minX = this._forcedDefaultSize.min[0];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
} else {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
minX = this._unrotatedAABB.min[0];
|
||||
}
|
||||
const absScaleX = this.getScaleX();
|
||||
@@ -416,9 +417,10 @@ namespace gdjs {
|
||||
let minY = 0;
|
||||
if (this._forcedDefaultSize) {
|
||||
minY = this._forcedDefaultSize.min[1];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
} else {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
minY = this._unrotatedAABB.min[1];
|
||||
}
|
||||
const absScaleY = this.getScaleY();
|
||||
|
@@ -37,6 +37,18 @@ namespace gdjs {
|
||||
if (this._isNextLayoutLoading || this._stack.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasMadeChangeToStack = this.applyUpdateFromNetworkSyncDataIfAny();
|
||||
if (hasMadeChangeToStack) {
|
||||
debugLogger.info(
|
||||
'Scene stack has been updated from network sync data, skipping step.'
|
||||
);
|
||||
// If we have made changes to the stack as part of the network sync,
|
||||
// we trust the network to be the source of truth for the scene stack,
|
||||
// and skip the scene rendering (and so any other request to change the scene stack from it)
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentScene = this._stack[this._stack.length - 1];
|
||||
if (currentScene.renderAndStep(elapsedTime)) {
|
||||
const request = currentScene.getRequestedChange();
|
||||
@@ -58,8 +70,6 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
this.applyUpdateFromNetworkSyncDataIfAny();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -232,9 +242,10 @@ namespace gdjs {
|
||||
this._sceneStackSyncDataToApply = sceneStackSyncData;
|
||||
}
|
||||
|
||||
applyUpdateFromNetworkSyncDataIfAny(): void {
|
||||
applyUpdateFromNetworkSyncDataIfAny(): boolean {
|
||||
const sceneStackSyncData = this._sceneStackSyncDataToApply;
|
||||
if (!sceneStackSyncData) return;
|
||||
let hasMadeChangeToStack = false;
|
||||
if (!sceneStackSyncData) return hasMadeChangeToStack;
|
||||
|
||||
this._sceneStackSyncDataToApply = null;
|
||||
|
||||
@@ -251,11 +262,12 @@ namespace gdjs {
|
||||
debugLogger.info(
|
||||
`Scene at position ${i} with name ${sceneSyncData.name} is missing from the stack, adding it.`
|
||||
);
|
||||
// We have less scenes in the stack than the host, let's add the scene.
|
||||
// We have fewer scenes in the stack than the host, let's add the scene.
|
||||
const newScene = this.push(sceneSyncData.name);
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
}
|
||||
hasMadeChangeToStack = true;
|
||||
// Continue to the next scene in the stack received from the host.
|
||||
continue;
|
||||
}
|
||||
@@ -275,6 +287,7 @@ namespace gdjs {
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
}
|
||||
hasMadeChangeToStack = true;
|
||||
// Continue to the next scene in the stack received from the host.
|
||||
continue;
|
||||
}
|
||||
@@ -317,6 +330,7 @@ namespace gdjs {
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
}
|
||||
hasMadeChangeToStack = true;
|
||||
// Continue to the next scene in the stack received from the host.
|
||||
continue;
|
||||
}
|
||||
@@ -330,7 +344,10 @@ namespace gdjs {
|
||||
if (this._stack.length > sceneStackSyncData.length) {
|
||||
const popCount = this._stack.length - sceneStackSyncData.length;
|
||||
this.pop(popCount);
|
||||
hasMadeChangeToStack = true;
|
||||
}
|
||||
|
||||
return hasMadeChangeToStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -620,6 +620,16 @@ namespace gdjs {
|
||||
this._childrenArray.push(variable.clone());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a variable into the array without duplicating it first.
|
||||
* This should only be used by generated code.
|
||||
*/
|
||||
_pushVariable(variable: gdjs.Variable) {
|
||||
if (this._type !== 'array') this.castTo('array');
|
||||
|
||||
this._childrenArray.push(variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a value into the array.
|
||||
*/
|
||||
|
@@ -521,6 +521,7 @@ namespace gdjs {
|
||||
return [];
|
||||
},
|
||||
pushVariableCopy: () => {},
|
||||
_pushVariable: () => {},
|
||||
pushValue: () => {},
|
||||
removeAtIndex: function () {
|
||||
return;
|
||||
|
@@ -2433,43 +2433,82 @@ interface WholeProjectRefactorer {
|
||||
[Ref] Project project,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameLayer(
|
||||
void STATIC_RenameLayerInScene(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout layout,
|
||||
[Ref] Layout scene,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameLayerEffect(
|
||||
void STATIC_RenameLayerInEventsBasedObject(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout layout,
|
||||
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
|
||||
[Ref] EventsBasedObject eventsBasedObject,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameLayerEffectInScene(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout scene,
|
||||
[Ref] Layer layer,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameObjectAnimation(
|
||||
void STATIC_RenameLayerEffectInEventsBasedObject(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout layout,
|
||||
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
|
||||
[Ref] EventsBasedObject eventsBasedObject,
|
||||
[Ref] Layer layer,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameObjectAnimationInScene(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout scene,
|
||||
[Ref] gdObject gdObject,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameObjectPoint(
|
||||
void STATIC_RenameObjectAnimationInEventsBasedObject(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout layout,
|
||||
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
|
||||
[Ref] EventsBasedObject eventsBasedObject,
|
||||
[Ref] gdObject gdObject,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameObjectEffect(
|
||||
void STATIC_RenameObjectPointInScene(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout layout,
|
||||
[Ref] Layout scene,
|
||||
[Ref] gdObject gdObject,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_ObjectOrGroupRenamedInLayout([Ref] Project project, [Ref] Layout layout, [Const] DOMString oldName, [Const] DOMString newName, boolean isObjectGroup);
|
||||
void STATIC_ObjectRemovedInLayout(
|
||||
void STATIC_RenameObjectPointInEventsBasedObject(
|
||||
[Ref] Project project,
|
||||
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
|
||||
[Ref] EventsBasedObject eventsBasedObject,
|
||||
[Ref] gdObject gdObject,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameObjectEffectInScene(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout scene,
|
||||
[Ref] gdObject gdObject,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_RenameObjectEffectInEventsBasedObject(
|
||||
[Ref] Project project,
|
||||
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
|
||||
[Ref] EventsBasedObject eventsBasedObject,
|
||||
[Ref] gdObject gdObject,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName);
|
||||
void STATIC_ObjectOrGroupRenamedInScene(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout layout,
|
||||
[Ref] Layout scene,
|
||||
[Const] DOMString oldName,
|
||||
[Const] DOMString newName,
|
||||
boolean isObjectGroup);
|
||||
void STATIC_ObjectRemovedInScene(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout scene,
|
||||
[Const] DOMString objectName);
|
||||
void STATIC_BehaviorsAddedToObjectInLayout(
|
||||
void STATIC_BehaviorsAddedToObjectInScene(
|
||||
[Ref] Project project,
|
||||
[Ref] Layout layout,
|
||||
[Ref] Layout scene,
|
||||
[Const] DOMString objectName);
|
||||
void STATIC_ObjectOrGroupRenamedInEventsFunction(
|
||||
[Ref] Project project,
|
||||
|
@@ -646,9 +646,9 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
#define STATIC_Year Year
|
||||
#define STATIC_Month Month
|
||||
#define STATIC_Date Date
|
||||
#define STATIC_ObjectOrGroupRenamedInLayout ObjectOrGroupRenamedInLayout
|
||||
#define STATIC_ObjectRemovedInLayout ObjectRemovedInLayout
|
||||
#define STATIC_BehaviorsAddedToObjectInLayout BehaviorsAddedToObjectInLayout
|
||||
#define STATIC_ObjectOrGroupRenamedInScene ObjectOrGroupRenamedInScene
|
||||
#define STATIC_ObjectRemovedInScene ObjectRemovedInScene
|
||||
#define STATIC_BehaviorsAddedToObjectInScene BehaviorsAddedToObjectInScene
|
||||
#define STATIC_ObjectRemovedInEventsFunction \
|
||||
ObjectRemovedInEventsFunction
|
||||
#define STATIC_ObjectOrGroupRenamedInEventsFunction \
|
||||
@@ -746,11 +746,16 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
#define STATIC_RenameLayout RenameLayout
|
||||
#define STATIC_RenameExternalLayout RenameExternalLayout
|
||||
#define STATIC_RenameExternalEvents RenameExternalEvents
|
||||
#define STATIC_RenameLayer RenameLayer
|
||||
#define STATIC_RenameLayerEffect RenameLayerEffect
|
||||
#define STATIC_RenameObjectAnimation RenameObjectAnimation
|
||||
#define STATIC_RenameObjectPoint RenameObjectPoint
|
||||
#define STATIC_RenameObjectEffect RenameObjectEffect
|
||||
#define STATIC_RenameLayerInScene RenameLayerInScene
|
||||
#define STATIC_RenameLayerEffectInScene RenameLayerEffectInScene
|
||||
#define STATIC_RenameObjectAnimationInScene RenameObjectAnimationInScene
|
||||
#define STATIC_RenameObjectPointInScene RenameObjectPointInScene
|
||||
#define STATIC_RenameObjectEffectInScene RenameObjectEffectInScene
|
||||
#define STATIC_RenameLayerInEventsBasedObject RenameLayerInEventsBasedObject
|
||||
#define STATIC_RenameLayerEffectInEventsBasedObject RenameLayerEffectInEventsBasedObject
|
||||
#define STATIC_RenameObjectAnimationInEventsBasedObject RenameObjectAnimationInEventsBasedObject
|
||||
#define STATIC_RenameObjectPointInEventsBasedObject RenameObjectPointInEventsBasedObject
|
||||
#define STATIC_RenameObjectEffectInEventsBasedObject RenameObjectEffectInEventsBasedObject
|
||||
|
||||
#define STATIC_GetBehaviorPropertyGetterName GetBehaviorPropertyGetterName
|
||||
#define STATIC_GetBehaviorPropertySetterName GetBehaviorPropertySetterName
|
||||
|
@@ -252,6 +252,13 @@ class Variable {
|
||||
this._childrenArray.push(variable.clone());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Variable} variable
|
||||
*/
|
||||
_pushVariable(variable) {
|
||||
this._childrenArray.push(variable);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
|
@@ -3643,7 +3643,7 @@ describe('libGD.js', function () {
|
||||
instance1.setObjectName('Object1');
|
||||
instance2.setObjectName('Object2');
|
||||
|
||||
gd.WholeProjectRefactorer.objectOrGroupRenamedInLayout(
|
||||
gd.WholeProjectRefactorer.objectOrGroupRenamedInScene(
|
||||
project,
|
||||
layout,
|
||||
'Object1',
|
||||
@@ -3660,7 +3660,7 @@ describe('libGD.js', function () {
|
||||
true
|
||||
);
|
||||
|
||||
gd.WholeProjectRefactorer.objectRemovedInLayout(
|
||||
gd.WholeProjectRefactorer.objectRemovedInScene(
|
||||
project,
|
||||
layout,
|
||||
'Object3',
|
||||
|
@@ -420,7 +420,7 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local child-variable condition', function () {
|
||||
it('can generate a local child-variable condition on a structure', function () {
|
||||
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
const runtimeScene = generateAndRunEventsForFunction([
|
||||
{
|
||||
@@ -455,6 +455,41 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local child-variable condition on an array', function () {
|
||||
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
const runtimeScene = generateAndRunEventsForFunction([
|
||||
{
|
||||
type: 'BuiltinCommonInstructions::Standard',
|
||||
variables: [
|
||||
{
|
||||
name: 'MyLocalVariable',
|
||||
type: 'array',
|
||||
children: [{ name: '0', type: 'number', value: 123 }],
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
type: { inverted: false, value: 'NumberVariable' },
|
||||
parameters: ['MyLocalVariable[0]', '=', '123'],
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: { value: 'SetNumberVariable' },
|
||||
parameters: ['SuccessVariable', '=', '1'],
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
runtimeScene
|
||||
.getVariablesForExtension('Extension')
|
||||
.get('SuccessVariable')
|
||||
.getAsNumber()
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local variable condition giving precedence to the closest local variable', function () {
|
||||
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
|
||||
|
@@ -346,7 +346,7 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local child-variable condition', function () {
|
||||
it('can generate a local child-variable condition on a structure', function () {
|
||||
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
const runtimeScene = generateAndRunEventsForLayout([
|
||||
{
|
||||
@@ -378,6 +378,38 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local child-variable condition on an array', function () {
|
||||
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
const runtimeScene = generateAndRunEventsForLayout([
|
||||
{
|
||||
type: 'BuiltinCommonInstructions::Standard',
|
||||
variables: [
|
||||
{
|
||||
name: 'MyLocalVariable',
|
||||
type: 'array',
|
||||
children: [{ name: '0', type: 'number', value: 123 }],
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
type: { inverted: false, value: 'NumberVariable' },
|
||||
parameters: ['MyLocalVariable[0]', '=', '123'],
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: { value: 'SetNumberVariable' },
|
||||
parameters: ['SuccessVariable', '=', '1'],
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local variable condition giving precedence to the closest local variable', function () {
|
||||
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
|
||||
|
21
GDevelop.js/types.d.ts
vendored
21
GDevelop.js/types.d.ts
vendored
@@ -1810,14 +1810,19 @@ export class WholeProjectRefactorer extends EmscriptenObject {
|
||||
static renameLayout(project: Project, oldName: string, newName: string): void;
|
||||
static renameExternalLayout(project: Project, oldName: string, newName: string): void;
|
||||
static renameExternalEvents(project: Project, oldName: string, newName: string): void;
|
||||
static renameLayer(project: Project, layout: Layout, oldName: string, newName: string): void;
|
||||
static renameLayerEffect(project: Project, layout: Layout, layer: Layer, oldName: string, newName: string): void;
|
||||
static renameObjectAnimation(project: Project, layout: Layout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectPoint(project: Project, layout: Layout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectEffect(project: Project, layout: Layout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static objectOrGroupRenamedInLayout(project: Project, layout: Layout, oldName: string, newName: string, isObjectGroup: boolean): void;
|
||||
static objectRemovedInLayout(project: Project, layout: Layout, objectName: string): void;
|
||||
static behaviorsAddedToObjectInLayout(project: Project, layout: Layout, objectName: string): void;
|
||||
static renameLayerInScene(project: Project, scene: Layout, oldName: string, newName: string): void;
|
||||
static renameLayerInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, oldName: string, newName: string): void;
|
||||
static renameLayerEffectInScene(project: Project, scene: Layout, layer: Layer, oldName: string, newName: string): void;
|
||||
static renameLayerEffectInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, layer: Layer, oldName: string, newName: string): void;
|
||||
static renameObjectAnimationInScene(project: Project, scene: Layout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectAnimationInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectPointInScene(project: Project, scene: Layout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectPointInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectEffectInScene(project: Project, scene: Layout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectEffectInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static objectOrGroupRenamedInScene(project: Project, scene: Layout, oldName: string, newName: string, isObjectGroup: boolean): void;
|
||||
static objectRemovedInScene(project: Project, scene: Layout, objectName: string): void;
|
||||
static behaviorsAddedToObjectInScene(project: Project, scene: Layout, objectName: string): void;
|
||||
static objectOrGroupRenamedInEventsFunction(project: Project, projectScopedContainers: ProjectScopedContainers, eventsFunction: EventsFunction, oldName: string, newName: string, isObjectGroup: boolean): void;
|
||||
static objectRemovedInEventsFunction(project: Project, eventsFunction: EventsFunction, objectName: string): void;
|
||||
static objectOrGroupRenamedInEventsBasedObject(project: Project, projectScopedContainers: ProjectScopedContainers, eventsBasedObject: EventsBasedObject, oldName: string, newName: string, isObjectGroup: boolean): void;
|
||||
|
@@ -18,14 +18,19 @@ declare class gdWholeProjectRefactorer {
|
||||
static renameLayout(project: gdProject, oldName: string, newName: string): void;
|
||||
static renameExternalLayout(project: gdProject, oldName: string, newName: string): void;
|
||||
static renameExternalEvents(project: gdProject, oldName: string, newName: string): void;
|
||||
static renameLayer(project: gdProject, layout: gdLayout, oldName: string, newName: string): void;
|
||||
static renameLayerEffect(project: gdProject, layout: gdLayout, layer: gdLayer, oldName: string, newName: string): void;
|
||||
static renameObjectAnimation(project: gdProject, layout: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectPoint(project: gdProject, layout: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectEffect(project: gdProject, layout: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static objectOrGroupRenamedInLayout(project: gdProject, layout: gdLayout, oldName: string, newName: string, isObjectGroup: boolean): void;
|
||||
static objectRemovedInLayout(project: gdProject, layout: gdLayout, objectName: string): void;
|
||||
static behaviorsAddedToObjectInLayout(project: gdProject, layout: gdLayout, objectName: string): void;
|
||||
static renameLayerInScene(project: gdProject, scene: gdLayout, oldName: string, newName: string): void;
|
||||
static renameLayerInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, oldName: string, newName: string): void;
|
||||
static renameLayerEffectInScene(project: gdProject, scene: gdLayout, layer: gdLayer, oldName: string, newName: string): void;
|
||||
static renameLayerEffectInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, layer: gdLayer, oldName: string, newName: string): void;
|
||||
static renameObjectAnimationInScene(project: gdProject, scene: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectAnimationInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectPointInScene(project: gdProject, scene: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectPointInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectEffectInScene(project: gdProject, scene: gdLayout, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static renameObjectEffectInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, gdObject: gdObject, oldName: string, newName: string): void;
|
||||
static objectOrGroupRenamedInScene(project: gdProject, scene: gdLayout, oldName: string, newName: string, isObjectGroup: boolean): void;
|
||||
static objectRemovedInScene(project: gdProject, scene: gdLayout, objectName: string): void;
|
||||
static behaviorsAddedToObjectInScene(project: gdProject, scene: gdLayout, objectName: string): void;
|
||||
static objectOrGroupRenamedInEventsFunction(project: gdProject, projectScopedContainers: gdProjectScopedContainers, eventsFunction: gdEventsFunction, oldName: string, newName: string, isObjectGroup: boolean): void;
|
||||
static objectRemovedInEventsFunction(project: gdProject, eventsFunction: gdEventsFunction, objectName: string): void;
|
||||
static objectOrGroupRenamedInEventsBasedObject(project: gdProject, projectScopedContainers: gdProjectScopedContainers, eventsBasedObject: gdEventsBasedObject, oldName: string, newName: string, isObjectGroup: boolean): void;
|
||||
|
@@ -75,7 +75,7 @@ export namespace LDtkTileMapLoader {
|
||||
gridSize,
|
||||
dimX,
|
||||
dimY,
|
||||
tileSet
|
||||
tileSet,
|
||||
);
|
||||
const composedTileMap = new Map<string, TileDefinition>();
|
||||
let nextComposedTileId = 0xfffffff;
|
||||
@@ -89,7 +89,7 @@ export namespace LDtkTileMapLoader {
|
||||
const gridSize = layer.__gridSize;
|
||||
const tilesetId = layer.__tilesetDefUid;
|
||||
|
||||
const editableTileLayer = editableTileMap.addTileLayer(iLayer);
|
||||
const editableTileLayer = editableTileMap.addNewTileLayer(iLayer);
|
||||
editableTileLayer.setAlpha(layer.__opacity);
|
||||
editableTileLayer.setVisible(layer.visible);
|
||||
|
||||
|
@@ -117,7 +117,7 @@ export namespace TiledTileMapLoader {
|
||||
tiledTileMap.tileheight,
|
||||
tiledTileMap.width,
|
||||
tiledTileMap.height,
|
||||
definitions
|
||||
definitions,
|
||||
);
|
||||
|
||||
for (const tiledLayer of tiledTileMap.layers) {
|
||||
@@ -156,7 +156,7 @@ export namespace TiledTileMapLoader {
|
||||
layerData = tiledLayer.data as integer[];
|
||||
}
|
||||
if (layerData) {
|
||||
const collisionTileLayer = collisionTileMap.addTileLayer(
|
||||
const collisionTileLayer = collisionTileMap.addNewTileLayer(
|
||||
tiledLayer.id
|
||||
);
|
||||
collisionTileLayer.setAlpha(tiledLayer.opacity);
|
||||
|
@@ -3,3 +3,17 @@ export declare type float = number;
|
||||
export type FloatPoint = [float, float];
|
||||
|
||||
export type PolygonVertices = FloatPoint[];
|
||||
|
||||
export type EditableTileMapLayerAsJsObject = {
|
||||
id: number;
|
||||
alpha: number;
|
||||
tiles: number[][];
|
||||
};
|
||||
|
||||
export type EditableTileMapAsJsObject = {
|
||||
tileWidth: number;
|
||||
tileHeight: number;
|
||||
dimX: number;
|
||||
dimY: number;
|
||||
layers: EditableTileMapLayerAsJsObject[];
|
||||
};
|
||||
|
@@ -1,4 +1,10 @@
|
||||
import { PolygonVertices, integer, float } from "./CommonTypes";
|
||||
import {
|
||||
PolygonVertices,
|
||||
integer,
|
||||
float,
|
||||
EditableTileMapAsJsObject,
|
||||
EditableTileMapLayerAsJsObject,
|
||||
} from "./CommonTypes";
|
||||
import { FlippingHelper } from "./GID";
|
||||
|
||||
/**
|
||||
@@ -29,6 +35,10 @@ export class EditableTileMap {
|
||||
* The number of tile rows in the map.
|
||||
*/
|
||||
private dimY: integer;
|
||||
/**
|
||||
* True if is allowed to set a tile out of the tile map's bounds.
|
||||
* Useful when editing the tile map easily.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param tileWidth The width of a tile.
|
||||
@@ -63,12 +73,16 @@ export class EditableTileMap {
|
||||
* @param objectConfiguration
|
||||
*/
|
||||
static from(
|
||||
editableTileMapAsJsObject: any,
|
||||
editableTileMapAsJsObject: EditableTileMapAsJsObject,
|
||||
{
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
}: { tileSize: number; tileSetColumnCount: number; tileSetRowCount: number }
|
||||
}: {
|
||||
tileSize: number;
|
||||
tileSetColumnCount: number;
|
||||
tileSetRowCount: number;
|
||||
}
|
||||
): EditableTileMap {
|
||||
const tileSet = new Map<number, TileDefinition>();
|
||||
|
||||
@@ -89,7 +103,7 @@ export class EditableTileMap {
|
||||
|
||||
if (editableTileMapAsJsObject.layers) {
|
||||
editableTileMapAsJsObject.layers.forEach((layerAsJsObject: any) => {
|
||||
tileMap.setTileLayer(
|
||||
tileMap.addTileLayer(
|
||||
EditableTileMapLayer.from(
|
||||
layerAsJsObject,
|
||||
tileMap,
|
||||
@@ -98,7 +112,7 @@ export class EditableTileMap {
|
||||
);
|
||||
});
|
||||
} else {
|
||||
tileMap.addTileLayer(0);
|
||||
tileMap.addNewTileLayer(0);
|
||||
}
|
||||
|
||||
return tileMap;
|
||||
@@ -157,16 +171,81 @@ export class EditableTileMap {
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the number of columns in the tile map by adding/removing
|
||||
* columns at the end.
|
||||
* @param dim The number of tile columns in the map.
|
||||
*/
|
||||
setDimensionX(dim: integer): void {
|
||||
if (dim === this.dimX) return;
|
||||
const columnDelta = dim - this.dimX;
|
||||
for (const layer of this.getLayers()) {
|
||||
// TODO: Implement dimensions changes for EditableObjectLayer.
|
||||
if (layer instanceof EditableTileMapLayer) {
|
||||
if (columnDelta > 0) {
|
||||
layer.increaseDimensions(columnDelta, 0, 0, 0);
|
||||
} else {
|
||||
layer.reduceDimensions(-columnDelta, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dimX = dim;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases dimensions of the tile map by adding columns and rows
|
||||
* at the start and/or at the end of the grid.
|
||||
*/
|
||||
increaseDimensions(
|
||||
columnsToAppend: number,
|
||||
columnsToUnshift: number,
|
||||
rowsToAppend: number,
|
||||
rowsToUnshift: number
|
||||
): void {
|
||||
if (
|
||||
columnsToAppend < 0 ||
|
||||
columnsToUnshift < 0 ||
|
||||
rowsToAppend < 0 ||
|
||||
rowsToUnshift < 0 ||
|
||||
(columnsToAppend === 0 &&
|
||||
columnsToUnshift === 0 &&
|
||||
rowsToAppend === 0 &&
|
||||
rowsToUnshift === 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (const layer of this.getLayers()) {
|
||||
// TODO: Implement dimensions changes for EditableObjectLayer.
|
||||
if (layer instanceof EditableTileMapLayer) {
|
||||
layer.increaseDimensions(
|
||||
columnsToAppend,
|
||||
columnsToUnshift,
|
||||
rowsToAppend,
|
||||
rowsToUnshift
|
||||
);
|
||||
}
|
||||
}
|
||||
this.dimX = this.dimX + columnsToAppend + columnsToUnshift;
|
||||
this.dimY = this.dimY + rowsToAppend + rowsToUnshift;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the number of row in the tile map by adding/removing
|
||||
* rows at the end.
|
||||
* @param dim The number of tile rows in the map.
|
||||
*/
|
||||
setDimensionY(dim: integer): void {
|
||||
if (dim === this.dimY) return;
|
||||
const rowDelta = dim - this.dimY;
|
||||
for (const layer of this.getLayers()) {
|
||||
// TODO: Implement dimensions changes for EditableObjectLayer.
|
||||
if (layer instanceof EditableTileMapLayer) {
|
||||
if (rowDelta > 0) {
|
||||
layer.increaseDimensions(0, 0, rowDelta, 0);
|
||||
} else {
|
||||
layer.reduceDimensions(0, 0, -rowDelta, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dimY = dim;
|
||||
}
|
||||
|
||||
@@ -189,7 +268,7 @@ export class EditableTileMap {
|
||||
* @param id The identifier of the new layer.
|
||||
* @returns The new layer.
|
||||
*/
|
||||
addTileLayer(id: integer): EditableTileMapLayer {
|
||||
addNewTileLayer(id: integer): EditableTileMapLayer {
|
||||
const layer = new EditableTileMapLayer(this, id);
|
||||
this._layers.push(layer);
|
||||
return layer;
|
||||
@@ -198,7 +277,7 @@ export class EditableTileMap {
|
||||
/**
|
||||
* @param layer the new layer to set.
|
||||
*/
|
||||
setTileLayer(layer: EditableTileMapLayer): void {
|
||||
addTileLayer(layer: EditableTileMapLayer): void {
|
||||
this._layers.push(layer);
|
||||
}
|
||||
|
||||
@@ -276,6 +355,105 @@ export class EditableTileMap {
|
||||
isEmpty(): boolean {
|
||||
return this._layers.every((layer) => layer.isEmpty());
|
||||
}
|
||||
|
||||
getTileId(x: integer, y: integer, layerId: integer): integer {
|
||||
const layer = this.getTileLayer(layerId);
|
||||
if (!layer) return -1;
|
||||
const tileId = layer.getTileId(x, y);
|
||||
return tileId === undefined ? -1 : tileId;
|
||||
}
|
||||
|
||||
setTile(x: integer, y: integer, layerId: integer, tileId: number) {
|
||||
const layer = this.getTileLayer(layerId);
|
||||
if (!layer) return;
|
||||
|
||||
layer.setTile(x, y, tileId);
|
||||
}
|
||||
|
||||
flipTileOnY(x: integer, y: integer, layerId: integer, flip: boolean) {
|
||||
const layer = this.getTileLayer(layerId);
|
||||
if (!layer) return;
|
||||
layer.setFlippedVertically(x, y, flip);
|
||||
}
|
||||
flipTileOnX(x: integer, y: integer, layerId: integer, flip: boolean) {
|
||||
const layer = this.getTileLayer(layerId);
|
||||
if (!layer) return;
|
||||
layer.setFlippedHorizontally(x, y, flip);
|
||||
}
|
||||
isTileFlippedOnX(x: integer, y: integer, layerId: integer): boolean {
|
||||
const layer = this.getTileLayer(layerId);
|
||||
if (!layer) return false;
|
||||
return layer.isFlippedHorizontally(x, y);
|
||||
}
|
||||
isTileFlippedOnY(x: integer, y: integer, layerId: integer): boolean {
|
||||
const layer = this.getTileLayer(layerId);
|
||||
if (!layer) return false;
|
||||
return layer.isFlippedVertically(x, y);
|
||||
}
|
||||
removeTile(x: integer, y: integer, layerId: integer) {
|
||||
if (x < 0 || x >= this.dimX || y < 0 || y >= this.dimY) {
|
||||
return;
|
||||
}
|
||||
const layer = this.getTileLayer(layerId);
|
||||
if (!layer) return;
|
||||
layer.removeTile(x, y);
|
||||
}
|
||||
|
||||
trimEmptyColumnsAndRowToFitLayer(
|
||||
layerId: integer
|
||||
):
|
||||
| {
|
||||
poppedRows: number;
|
||||
poppedColumns: number;
|
||||
shiftedRows: number;
|
||||
shiftedColumns: number;
|
||||
}
|
||||
| undefined {
|
||||
const layer = this.getTileLayer(layerId);
|
||||
if (!layer) return;
|
||||
const initialRowCount = this.dimY;
|
||||
const initialColumnCount = this.dimX;
|
||||
if (layer.isEmpty() && this._layers.length === 1) {
|
||||
// The tile map is empty. Instead of having an object with null width and height,
|
||||
// the tile map is resized to have a size of 1x1 with an empty tile. This is useful
|
||||
// in the editor. It might need to have a different behavior in the runtime.
|
||||
layer.buildEmptyLayer(1, 1);
|
||||
this.dimX = 1;
|
||||
this.dimY = 1;
|
||||
return {
|
||||
shiftedRows: 0,
|
||||
shiftedColumns: 0,
|
||||
poppedRows: initialRowCount - 1,
|
||||
poppedColumns: initialColumnCount - 1,
|
||||
};
|
||||
}
|
||||
const trimmingData = layer.getTrimmingData();
|
||||
|
||||
for (const layer of this.getLayers()) {
|
||||
// TODO: Implement dimensions changes for EditableObjectLayer.
|
||||
if (layer instanceof EditableTileMapLayer) {
|
||||
layer.reduceDimensions(
|
||||
trimmingData.columnsToPop,
|
||||
trimmingData.columnsToShift,
|
||||
trimmingData.rowsToPop,
|
||||
trimmingData.rowsToShift
|
||||
);
|
||||
}
|
||||
}
|
||||
this.dimX =
|
||||
initialColumnCount -
|
||||
trimmingData.columnsToPop -
|
||||
trimmingData.columnsToShift;
|
||||
this.dimY =
|
||||
initialRowCount - trimmingData.rowsToPop - trimmingData.rowsToShift;
|
||||
|
||||
return {
|
||||
poppedRows: trimmingData.rowsToPop,
|
||||
poppedColumns: trimmingData.columnsToPop,
|
||||
shiftedRows: trimmingData.rowsToShift,
|
||||
shiftedColumns: trimmingData.columnsToShift,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -452,7 +630,7 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
}
|
||||
|
||||
static from(
|
||||
editableTileMapLayerAsJsObject: any,
|
||||
editableTileMapLayerAsJsObject: EditableTileMapLayerAsJsObject,
|
||||
tileMap: EditableTileMap,
|
||||
isTileIdValid: (tileId: number) => boolean
|
||||
): EditableTileMapLayer {
|
||||
@@ -461,7 +639,7 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
editableTileMapLayerAsJsObject.id
|
||||
);
|
||||
layer.setAlpha(editableTileMapLayerAsJsObject.alpha);
|
||||
editableTileMapLayerAsJsObject.tiles.forEach((row: Int32Array, y: number) =>
|
||||
editableTileMapLayerAsJsObject.tiles.forEach((row: number[], y: number) =>
|
||||
row.forEach((tileGID, x) => {
|
||||
const tileId = FlippingHelper.getTileId(tileGID);
|
||||
if (isTileIdValid(tileId)) {
|
||||
@@ -480,11 +658,12 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
// Array.from is needed to convert Int32Array to Array. Otherwise, JSON.stringify
|
||||
// serializes it as an object with index as keys.
|
||||
Array.from(
|
||||
row.map(
|
||||
(_, x) =>
|
||||
// -1 corresponds to null value
|
||||
this.getTileGID(x, y) || -1
|
||||
)
|
||||
row.map((_, x) => {
|
||||
const tileGID = this.getTileGID(x, y);
|
||||
// -1 corresponds to null value
|
||||
if (tileGID === undefined) return -1;
|
||||
return tileGID;
|
||||
})
|
||||
)
|
||||
),
|
||||
};
|
||||
@@ -514,9 +693,6 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
rowsToPop: number,
|
||||
rowsToShift: number
|
||||
) {
|
||||
const initialRowCount = this._tiles.length;
|
||||
const initialColumnCount = this._tiles[0].length;
|
||||
|
||||
if (rowsToPop > 0 || rowsToShift > 0) {
|
||||
this._tiles = this._tiles.slice(
|
||||
rowsToShift,
|
||||
@@ -531,12 +707,6 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
);
|
||||
});
|
||||
}
|
||||
// TODO: Instead of setting the dimensions directly, should it call a method on
|
||||
// EditableTileMap that will iterates over all the layers to change their dimensions?
|
||||
this.tileMap.setDimensionX(
|
||||
initialColumnCount - columnsToPop - columnsToShift
|
||||
);
|
||||
this.tileMap.setDimensionY(initialRowCount - rowsToPop - rowsToShift);
|
||||
}
|
||||
|
||||
increaseDimensions(
|
||||
@@ -557,6 +727,10 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
});
|
||||
}
|
||||
if (rowsToAppend > 0 || rowsToUnshift > 0) {
|
||||
// TODO: Consider over-provisioning columns and rows to avoid this operation being made
|
||||
// too often, especially in a case where tiles are added towards the outside.
|
||||
// Beware of over-provisioning rows above and/or columns on the left as it is supposed
|
||||
// to change the object position.
|
||||
this._tiles.unshift(
|
||||
...new Array(rowsToUnshift)
|
||||
.fill(0)
|
||||
@@ -579,86 +753,27 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
).fill(0);
|
||||
}
|
||||
}
|
||||
// TODO: Instead of setting the dimensions directly, should it call a method on
|
||||
// EditableTileMap that will iterates over all the layers to change their dimensions?
|
||||
this.tileMap.setDimensionX(
|
||||
initialColumnCount + columnsToAppend + columnsToUnshift
|
||||
);
|
||||
this.tileMap.setDimensionY(initialRowCount + rowsToAppend + rowsToUnshift);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileId The tile.
|
||||
* @param options Flipping options.
|
||||
*/
|
||||
setTile(
|
||||
x: integer,
|
||||
y: integer,
|
||||
tileId: integer,
|
||||
options:
|
||||
| {
|
||||
flipVertically: boolean;
|
||||
flipHorizontally: boolean;
|
||||
flipDiagonally: boolean;
|
||||
}
|
||||
| undefined = {
|
||||
flipVertically: false,
|
||||
flipHorizontally: false,
|
||||
flipDiagonally: false,
|
||||
}
|
||||
):
|
||||
| {
|
||||
unshiftedRows: number;
|
||||
unshiftedColumns: number;
|
||||
appendedRows: number;
|
||||
appendedColumns: number;
|
||||
}
|
||||
| undefined {
|
||||
setTile(x: integer, y: integer, tileId: integer) {
|
||||
const definition = this.tileMap.getTileDefinition(tileId);
|
||||
if (!definition) {
|
||||
console.error(`Invalid tile definition index: ${tileId}`);
|
||||
return;
|
||||
}
|
||||
const rowsToAdd = Math.max(0, y - (this._tiles.length - 1));
|
||||
const columnsToAdd = Math.max(0, x - (this._tiles[0].length - 1));
|
||||
const rowsToUnshift = Math.abs(Math.min(0, y));
|
||||
const columnsToUnshift = Math.abs(Math.min(0, x));
|
||||
if (rowsToAdd || columnsToAdd || rowsToUnshift || columnsToUnshift) {
|
||||
this.increaseDimensions(
|
||||
columnsToAdd,
|
||||
columnsToUnshift,
|
||||
rowsToAdd,
|
||||
rowsToUnshift
|
||||
);
|
||||
}
|
||||
// Dimensions have been changed to support setting tiles in positions below 0.
|
||||
// So we adapt the indices.
|
||||
const newX = x + columnsToUnshift;
|
||||
const newY = y + rowsToUnshift;
|
||||
const tilesRow = this._tiles[newY];
|
||||
const tilesRow = this._tiles[y];
|
||||
if (!tilesRow || x >= tilesRow.length) {
|
||||
// Coordinates are out of bounds, don't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
tilesRow[newX] =
|
||||
FlippingHelper.setFlippedHorizontally(
|
||||
FlippingHelper.setFlippedVertically(
|
||||
FlippingHelper.setFlippedDiagonally(tileId, options.flipDiagonally),
|
||||
options.flipVertically
|
||||
),
|
||||
options.flipHorizontally
|
||||
) +
|
||||
// +1 because 0 mean null
|
||||
1;
|
||||
return {
|
||||
unshiftedRows: rowsToUnshift,
|
||||
unshiftedColumns: columnsToUnshift,
|
||||
appendedRows: rowsToAdd,
|
||||
appendedColumns: columnsToAdd,
|
||||
};
|
||||
// +1 because 0 means null
|
||||
tilesRow[x] = tileId + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -673,15 +788,15 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
// +1 because 0 mean null
|
||||
// +1 because 0 means null
|
||||
tilesRow[x] = tileGID + 1;
|
||||
}
|
||||
|
||||
trimEmptyColumnsAndRow(): {
|
||||
shiftedRows: number;
|
||||
shiftedColumns: number;
|
||||
poppedRows: number;
|
||||
poppedColumns: number;
|
||||
getTrimmingData(): {
|
||||
rowsToShift: number;
|
||||
columnsToShift: number;
|
||||
rowsToPop: number;
|
||||
columnsToPop: number;
|
||||
} {
|
||||
let rowsToShift = 0,
|
||||
rowsToPop = 0;
|
||||
@@ -717,29 +832,20 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
}
|
||||
}
|
||||
if (!isFirstNonEmptyRowFound) {
|
||||
// The tile map is empty. Instead of having an object with null width and height,
|
||||
// the tile map is resized to have a size of 1x1 with an empty tile. This is useful
|
||||
// in the editor. It might need to have a different behavior in the runtime.
|
||||
this.buildEmptyLayer(1, 1);
|
||||
// TODO: Instead of setting the dimensions directly, should it call a method on
|
||||
// EditableTileMap that will iterates over all the layers to change their dimensions?
|
||||
this.tileMap.setDimensionX(1);
|
||||
this.tileMap.setDimensionY(1);
|
||||
return {
|
||||
shiftedColumns: 0,
|
||||
shiftedRows: 0,
|
||||
poppedColumns: initialDimensionX - 1,
|
||||
poppedRows: initialDimensionY - 1,
|
||||
columnsToShift: 0,
|
||||
rowsToShift: 0,
|
||||
columnsToPop: initialDimensionX - 1,
|
||||
rowsToPop: initialDimensionY - 1,
|
||||
};
|
||||
}
|
||||
const columnsToShift = Math.min(...columnsToShiftByRow);
|
||||
const columnsToPop = Math.min(...columnsToPopByRow);
|
||||
this.reduceDimensions(columnsToPop, columnsToShift, rowsToPop, rowsToShift);
|
||||
return {
|
||||
shiftedRows: rowsToShift,
|
||||
shiftedColumns: columnsToShift,
|
||||
poppedRows: rowsToPop,
|
||||
poppedColumns: columnsToPop,
|
||||
rowsToShift,
|
||||
columnsToShift,
|
||||
rowsToPop,
|
||||
columnsToPop,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -754,7 +860,7 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
// 0 mean null
|
||||
// 0 means null
|
||||
tilesRow[x] = 0;
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { TileTextureCache } from "./TileTextureCache";
|
||||
import { PixiTileMapHelper } from "./TileMapPixiHelper";
|
||||
import { TileMapLoader } from "../load/TileMapLoader";
|
||||
import { TileMapFileContent } from "../load/TileMapFileContent";
|
||||
import { EditableTileMapAsJsObject } from "../model/CommonTypes";
|
||||
|
||||
/**
|
||||
* A holder to share tile maps across the 2 extension objects.
|
||||
@@ -118,7 +119,7 @@ export class TileMapManager {
|
||||
}
|
||||
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: object,
|
||||
tileMapAsJsObject: EditableTileMapAsJsObject,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
tileSetColumnCount: number,
|
||||
@@ -130,11 +131,14 @@ export class TileMapManager {
|
||||
// TODO: Is it useful to cache the tilemap since it belongs to an instance?
|
||||
// const key = `${objectName}|${tileSize}|${tileSetColumnCount}|${tileSetRowCount}`;
|
||||
|
||||
const editableTileMap = EditableTileMap.from(tileMapAsJsObject, {
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
});
|
||||
const editableTileMap = EditableTileMap.from(
|
||||
tileMapAsJsObject,
|
||||
{
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
},
|
||||
);
|
||||
callback(editableTileMap);
|
||||
}
|
||||
|
||||
|
@@ -77,7 +77,11 @@ export namespace PixiTileMapHelper {
|
||||
|
||||
const texture = new PIXI.Texture(atlasTexture, rect);
|
||||
|
||||
textureCache.setTexture(rowCount * x + y, texture);
|
||||
textureCache.setTexture(
|
||||
// Id of the tile
|
||||
rowCount * x + y,
|
||||
texture
|
||||
);
|
||||
}
|
||||
}
|
||||
return textureCache;
|
||||
|
@@ -164,7 +164,21 @@ export const useShopNavigation = (): NavigationState => {
|
||||
previousHistory.previousPages[
|
||||
previousHistory.previousPages.length - 1
|
||||
];
|
||||
if (!isSearchResultPage(currentPage)) {
|
||||
if (isSearchResultPage(currentPage)) {
|
||||
const updatedCurrentPage = {
|
||||
...currentPage,
|
||||
pageBreakIndex: 0,
|
||||
scrollPosition: 0,
|
||||
};
|
||||
return {
|
||||
...previousHistory,
|
||||
previousPages: [
|
||||
// All pages except the last one
|
||||
...previousHistory.previousPages.slice(0, -1),
|
||||
updatedCurrentPage,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...previousHistory,
|
||||
previousPages: [
|
||||
@@ -173,8 +187,6 @@ export const useShopNavigation = (): NavigationState => {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return previousHistory;
|
||||
});
|
||||
},
|
||||
openTagPage: (tag: string) => {
|
||||
|
@@ -224,6 +224,7 @@ const PageBreakNavigation = ({
|
||||
export type AssetsListInterface = {|
|
||||
getScrollPosition: () => number,
|
||||
scrollToPosition: (y: number) => void,
|
||||
setPageBreakIndex: (index: number) => void,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
@@ -306,6 +307,9 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
isNavigatingInsideFolder,
|
||||
setIsNavigatingInsideFolder,
|
||||
] = React.useState<boolean>(false);
|
||||
const [pageBreakIndex, setPageBreakIndex] = React.useState<number>(
|
||||
(currentPage && currentPage.pageBreakIndex) || 0
|
||||
);
|
||||
const { openedAssetPack, selectedFolders } = React.useMemo(
|
||||
() => {
|
||||
if (!currentPage) {
|
||||
@@ -333,6 +337,9 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
|
||||
scrollViewElement.scrollToPosition(y);
|
||||
},
|
||||
setPageBreakIndex: (index: number) => {
|
||||
setPageBreakIndex(0);
|
||||
},
|
||||
}));
|
||||
|
||||
const fetchAssetsAndGameTemplates = React.useCallback(
|
||||
@@ -458,10 +465,6 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
[allPrivateAssetPackListingDatas, openedAssetPack]
|
||||
);
|
||||
|
||||
const [pageBreakIndex, setPageBreakIndex] = React.useState<number>(
|
||||
(currentPage && currentPage.pageBreakIndex) || 0
|
||||
);
|
||||
|
||||
const assetTiles = React.useMemo(
|
||||
() => {
|
||||
// Loading
|
||||
|
@@ -574,23 +574,27 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
hideGameTemplates ? t`Search assets` : `Search the shop`
|
||||
}
|
||||
value={searchText}
|
||||
onChange={
|
||||
isOnSearchResultPage
|
||||
? // An existing search is already being done: just update the
|
||||
// search text and the store will update the search results.
|
||||
setSearchText
|
||||
: (newValue: string) => {
|
||||
setSearchText(newValue);
|
||||
|
||||
// A new search is being initiated: navigate to the search page,
|
||||
// and clear the history as a new search was launched.
|
||||
if (!!newValue) {
|
||||
shopNavigationState.clearHistory();
|
||||
shopNavigationState.openSearchResultPage();
|
||||
openFiltersPanelIfAppropriate();
|
||||
}
|
||||
}
|
||||
}
|
||||
onChange={(newValue: string) => {
|
||||
setSearchText(newValue);
|
||||
if (isOnSearchResultPage) {
|
||||
// An existing search is already being done: just move to the
|
||||
// top search results.
|
||||
shopNavigationState.openSearchResultPage();
|
||||
const assetsListInterface = assetsList.current;
|
||||
if (assetsListInterface) {
|
||||
assetsListInterface.scrollToPosition(0);
|
||||
assetsListInterface.setPageBreakIndex(0);
|
||||
}
|
||||
} else {
|
||||
// A new search is being initiated: navigate to the search page,
|
||||
// and clear the history as a new search was launched.
|
||||
if (!!newValue) {
|
||||
shopNavigationState.clearHistory();
|
||||
shopNavigationState.openSearchResultPage();
|
||||
openFiltersPanelIfAppropriate();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onRequestSearch={() => {}}
|
||||
ref={searchBar}
|
||||
id="asset-store-search-bar"
|
||||
|
@@ -14,7 +14,7 @@ type Props = {|
|
||||
value: string,
|
||||
onChange: string => void,
|
||||
disabled?: boolean,
|
||||
eventsFunctionsExtension?: gdEventsFunctionsExtension,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
|};
|
||||
type State = {|
|
||||
behaviorMetadata: Array<EnumeratedBehaviorMetadata>,
|
||||
|
@@ -18,7 +18,7 @@ export type EnumeratedBehaviorMetadata = {|
|
||||
export const enumerateBehaviorsMetadata = (
|
||||
platform: gdPlatform,
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension?: ?gdEventsFunctionsExtension
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null
|
||||
): Array<EnumeratedBehaviorMetadata> => {
|
||||
const extensionsList = platform.getAllPlatformExtensions();
|
||||
|
||||
|
@@ -27,7 +27,7 @@ const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension?: ?gdEventsFunctionsExtension,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
open: boolean,
|
||||
|
@@ -269,7 +269,7 @@ const BehaviorConfigurationEditor = React.forwardRef<
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension?: gdEventsFunctionsExtension,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
object: gdObject,
|
||||
onUpdateBehaviorsSharedData: () => void,
|
||||
onSizeUpdated?: ?() => void,
|
||||
|
@@ -39,7 +39,6 @@ export type CommandName =
|
||||
| 'TOGGLE_LAYERS_PANEL'
|
||||
| 'SCENE_EDITOR_UNDO'
|
||||
| 'SCENE_EDITOR_REDO'
|
||||
| 'RENAME_SCENE_OBJECT'
|
||||
| 'DELETE_INSTANCES'
|
||||
| 'TOGGLE_WINDOW_MASK'
|
||||
| 'TOGGLE_GRID'
|
||||
@@ -243,10 +242,6 @@ const commandsList: { [CommandName]: CommandMetadata } = {
|
||||
displayText: t`Redo the last changes`,
|
||||
noShortcut: true,
|
||||
},
|
||||
RENAME_SCENE_OBJECT: {
|
||||
area: 'SCENE',
|
||||
displayText: t`Rename the selected object`,
|
||||
},
|
||||
DELETE_INSTANCES: {
|
||||
area: 'SCENE',
|
||||
displayText: t`Delete the selected instances from the scene`,
|
||||
|
@@ -731,6 +731,7 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
{property.getType() === 'Behavior' && (
|
||||
<BehaviorTypeSelector
|
||||
project={project}
|
||||
eventsFunctionsExtension={extension}
|
||||
objectType={
|
||||
behaviorObjectType || ''
|
||||
}
|
||||
|
@@ -283,7 +283,9 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
object={this.state.editedObjectWithContext.object}
|
||||
initialTab={this.state.editedObjectInitialTab}
|
||||
project={project}
|
||||
layout={null}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
projectScopedContainersAccessor={
|
||||
this.props.projectScopedContainersAccessor
|
||||
}
|
||||
|
@@ -0,0 +1,105 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import { type EventsBasedObjectCreationParameters } from '../EventsFunctionsList/EventsBasedObjectTreeViewItemContent';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { List, ListItem } from '../UI/List';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import Object2DIcon from '../UI/CustomSvgIcons/Object2d';
|
||||
import Object3DIcon from '../UI/CustomSvgIcons/Object3d';
|
||||
|
||||
type Props = {|
|
||||
onCancel: () => void,
|
||||
onChoose: (parameters: EventsBasedObjectCreationParameters) => void,
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
icon: { width: 40, height: 40 },
|
||||
disabledItem: { opacity: 0.6 },
|
||||
};
|
||||
|
||||
const FunctionListItem = ({
|
||||
icon,
|
||||
disabled,
|
||||
onChoose,
|
||||
name,
|
||||
description,
|
||||
}: {|
|
||||
icon: React.Node,
|
||||
disabled?: boolean,
|
||||
onChoose: () => void,
|
||||
name: React.Node,
|
||||
description: React.Node,
|
||||
|}) => {
|
||||
return (
|
||||
<ListItem
|
||||
leftIcon={icon}
|
||||
primaryText={name}
|
||||
secondaryText={description}
|
||||
secondaryTextLines={2}
|
||||
onClick={onChoose}
|
||||
style={disabled ? styles.disabledItem : undefined}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default function EventsBasedObjectSelectorDialog({
|
||||
onChoose,
|
||||
onCancel,
|
||||
}: Props) {
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>Choose a new object type</Trans>}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Cancel</Trans>}
|
||||
keyboardFocused={true}
|
||||
onClick={onCancel}
|
||||
key={'close'}
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath="/objects/custom-objects-prefab-template"
|
||||
/>,
|
||||
]}
|
||||
open
|
||||
onRequestClose={onCancel}
|
||||
>
|
||||
<List>
|
||||
<FunctionListItem
|
||||
icon={<Object2DIcon style={styles.icon} />}
|
||||
name={<Trans>2D object</Trans>}
|
||||
onChoose={() =>
|
||||
onChoose({
|
||||
isRenderedIn3D: false,
|
||||
})
|
||||
}
|
||||
description={
|
||||
<Trans>
|
||||
An object that can be moved, rotated and scaled in 2D.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<FunctionListItem
|
||||
icon={<Object3DIcon style={styles.icon} />}
|
||||
name={<Trans>3D object</Trans>}
|
||||
onChoose={() =>
|
||||
onChoose({
|
||||
isRenderedIn3D: true,
|
||||
})
|
||||
}
|
||||
description={
|
||||
<Trans>
|
||||
An object that can be moved, rotated and scaled in 3D.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</List>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -28,9 +28,10 @@ const gd: libGDevelop = global.gd;
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunction: gdEventsFunction,
|
||||
eventsBasedBehavior: ?gdEventsBasedBehavior,
|
||||
eventsBasedObject: ?gdEventsBasedObject,
|
||||
eventsFunctionsContainer: ?gdEventsFunctionsContainer,
|
||||
eventsBasedBehavior: gdEventsBasedBehavior | null,
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
eventsFunctionsContainer: gdEventsFunctionsContainer | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
onParametersUpdated: () => void,
|
||||
helpPagePath?: string,
|
||||
freezeParameters?: boolean,
|
||||
@@ -68,6 +69,7 @@ export const EventsFunctionParametersEditor = ({
|
||||
eventsBasedBehavior,
|
||||
eventsBasedObject,
|
||||
eventsFunctionsContainer,
|
||||
eventsFunctionsExtension,
|
||||
onParametersUpdated,
|
||||
helpPagePath,
|
||||
freezeParameters,
|
||||
@@ -400,6 +402,7 @@ export const EventsFunctionParametersEditor = ({
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<ValueTypeEditor
|
||||
project={project}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
valueTypeMetadata={parameter.getValueTypeMetadata()}
|
||||
disabled={isParameterDisabled(i)}
|
||||
isTypeSelectorShown={isParameterTypeShown(i)}
|
||||
|
@@ -29,9 +29,10 @@ const gd: libGDevelop = global.gd;
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunction: gdEventsFunction,
|
||||
eventsBasedBehavior: ?gdEventsBasedBehavior,
|
||||
eventsBasedObject: ?gdEventsBasedObject,
|
||||
eventsFunctionsContainer: ?gdEventsFunctionsContainer,
|
||||
eventsBasedBehavior: gdEventsBasedBehavior | null,
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
eventsFunctionsContainer: gdEventsFunctionsContainer | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
helpPagePath?: string,
|
||||
onConfigurationUpdated?: (?ExtensionItemConfigurationAttribute) => void,
|
||||
renderConfigurationHeader?: () => React.Node,
|
||||
@@ -143,6 +144,7 @@ const getDescriptionHintText = (
|
||||
|
||||
export const EventsFunctionPropertiesEditor = ({
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction,
|
||||
freezeEventsFunctionType,
|
||||
onConfigurationUpdated,
|
||||
@@ -500,6 +502,7 @@ export const EventsFunctionPropertiesEditor = ({
|
||||
<ValueTypeEditor
|
||||
isExpressionType
|
||||
project={project}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
valueTypeMetadata={eventsFunction.getExpressionType()}
|
||||
isTypeSelectorShown={true}
|
||||
onTypeUpdated={() => {
|
||||
|
@@ -14,6 +14,7 @@ import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
valueTypeMetadata: gdValueTypeMetadata,
|
||||
onTypeUpdated: () => void,
|
||||
disabled?: boolean,
|
||||
@@ -44,6 +45,7 @@ const getIdentifierName = (scopedIdentifier: string) =>
|
||||
|
||||
export default function ValueTypeEditor({
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
valueTypeMetadata,
|
||||
disabled,
|
||||
isTypeSelectorShown,
|
||||
@@ -191,6 +193,7 @@ export default function ValueTypeEditor({
|
||||
{valueTypeMetadata.isBehavior() && (
|
||||
<BehaviorTypeSelector
|
||||
project={project}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
objectType={getLastObjectParameterObjectType()}
|
||||
value={valueTypeMetadata.getExtraInfo()}
|
||||
onChange={(value: string) => {
|
||||
|
@@ -21,9 +21,10 @@ type Props = {|
|
||||
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
eventsFunction: gdEventsFunction,
|
||||
eventsBasedBehavior: ?gdEventsBasedBehavior,
|
||||
eventsBasedObject: ?gdEventsBasedObject,
|
||||
eventsBasedBehavior: gdEventsBasedBehavior | null,
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
eventsFunctionsContainer: gdEventsFunctionsContainer,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
onParametersOrGroupsUpdated: () => void,
|
||||
helpPagePath?: string,
|
||||
onConfigurationUpdated?: (?ExtensionItemConfigurationAttribute) => void,
|
||||
@@ -147,6 +148,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
|
||||
onMoveObjectEventsParameter,
|
||||
getFunctionGroupNames,
|
||||
eventsFunctionsContainer,
|
||||
eventsFunctionsExtension,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -182,6 +184,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
|
||||
eventsBasedBehavior={eventsBasedBehavior}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
eventsFunctionsContainer={eventsFunctionsContainer}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
helpPagePath={helpPagePath}
|
||||
onConfigurationUpdated={onConfigurationUpdated}
|
||||
renderConfigurationHeader={renderConfigurationHeader}
|
||||
@@ -200,6 +203,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
|
||||
eventsBasedBehavior={eventsBasedBehavior}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
eventsFunctionsContainer={eventsFunctionsContainer}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
onParametersUpdated={onParametersOrGroupsUpdated}
|
||||
helpPagePath={helpPagePath}
|
||||
freezeParameters={freezeParameters}
|
||||
|
@@ -16,6 +16,7 @@ import EventsFunctionsListWithErrorBoundary, {
|
||||
type EventsFunctionsListInterface,
|
||||
} from '../EventsFunctionsList';
|
||||
import { type EventsFunctionCreationParameters } from '../EventsFunctionsList/EventsFunctionTreeViewItemContent';
|
||||
import { type EventsBasedObjectCreationParameters } from '../EventsFunctionsList/EventsBasedObjectTreeViewItemContent';
|
||||
import Background from '../UI/Background';
|
||||
import OptionsEditorDialog from './OptionsEditorDialog';
|
||||
import EventsBasedBehaviorEditorPanel from '../EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel';
|
||||
@@ -24,6 +25,7 @@ import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||
import BehaviorMethodSelectorDialog from './BehaviorMethodSelectorDialog';
|
||||
import ObjectMethodSelectorDialog from './ObjectMethodSelectorDialog';
|
||||
import ExtensionFunctionSelectorDialog from './ExtensionFunctionSelectorDialog';
|
||||
import EventsBasedObjectSelectorDialog from './EventsBasedObjectSelectorDialog';
|
||||
import { ResponsiveWindowMeasurer } from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import EditorNavigator, {
|
||||
type EditorNavigatorInterface,
|
||||
@@ -85,10 +87,14 @@ type State = {|
|
||||
behaviorMethodSelectorDialogOpen: boolean,
|
||||
objectMethodSelectorDialogOpen: boolean,
|
||||
extensionFunctionSelectorDialogOpen: boolean,
|
||||
eventsBasedObjectSelectorDialogOpen: boolean,
|
||||
variablesEditorOpen: { isGlobalTabInitiallyOpen: boolean } | null,
|
||||
onAddEventsFunctionCb: ?(
|
||||
parameters: ?EventsFunctionCreationParameters
|
||||
) => void,
|
||||
onAddEventsBasedObjectCb: ?(
|
||||
parameters: ?EventsBasedObjectCreationParameters
|
||||
) => void,
|
||||
|};
|
||||
|
||||
const extensionEditIconReactNode = <ExtensionEditIcon />;
|
||||
@@ -121,8 +127,10 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
behaviorMethodSelectorDialogOpen: false,
|
||||
objectMethodSelectorDialogOpen: false,
|
||||
extensionFunctionSelectorDialogOpen: false,
|
||||
eventsBasedObjectSelectorDialogOpen: false,
|
||||
variablesEditorOpen: null,
|
||||
onAddEventsFunctionCb: null,
|
||||
onAddEventsBasedObjectCb: null,
|
||||
};
|
||||
editor: ?EventsSheetInterface;
|
||||
eventsFunctionList: ?EventsFunctionsListInterface;
|
||||
@@ -772,6 +780,32 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
);
|
||||
};
|
||||
|
||||
_onCloseEventsBasedObjectSelectorDialog = (
|
||||
parameters: ?EventsBasedObjectCreationParameters
|
||||
) => {
|
||||
const { onAddEventsBasedObjectCb } = this.state;
|
||||
this.setState(
|
||||
{
|
||||
eventsBasedObjectSelectorDialogOpen: false,
|
||||
onAddEventsBasedObjectCb: null,
|
||||
},
|
||||
() => {
|
||||
if (onAddEventsBasedObjectCb) onAddEventsBasedObjectCb(parameters);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
_onAddEventsBasedObject = (
|
||||
onAddEventsBasedObjectCb: (
|
||||
parameters: ?EventsBasedObjectCreationParameters
|
||||
) => void
|
||||
) => {
|
||||
this.setState({
|
||||
eventsBasedObjectSelectorDialogOpen: true,
|
||||
onAddEventsBasedObjectCb,
|
||||
});
|
||||
};
|
||||
|
||||
_onAddEventsFunction = (
|
||||
eventsBasedBehavior: ?gdEventsBasedBehavior,
|
||||
eventsBasedObject: ?gdEventsBasedObject,
|
||||
@@ -1156,6 +1190,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
behaviorMethodSelectorDialogOpen,
|
||||
objectMethodSelectorDialogOpen,
|
||||
extensionFunctionSelectorDialogOpen,
|
||||
eventsBasedObjectSelectorDialogOpen,
|
||||
variablesEditorOpen,
|
||||
} = this.state;
|
||||
|
||||
@@ -1197,6 +1232,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
selectedEventsBasedEntity.getEventsFunctions()) ||
|
||||
eventsFunctionsExtension
|
||||
}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
objectsContainer={this._objectsContainer}
|
||||
onConfigurationUpdated={this._onConfigurationUpdated}
|
||||
helpPagePath={
|
||||
@@ -1384,6 +1420,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
i18n
|
||||
)}
|
||||
onEventsBasedObjectRenamed={this._onEventsBasedObjectRenamed}
|
||||
onAddEventsBasedObject={this._onAddEventsBasedObject}
|
||||
onSelectExtensionProperties={() => this._editOptions(true)}
|
||||
onSelectExtensionGlobalVariables={() =>
|
||||
this._editVariables({ isGlobalTabInitiallyOpen: true })
|
||||
@@ -1516,6 +1553,14 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{eventsBasedObjectSelectorDialogOpen && (
|
||||
<EventsBasedObjectSelectorDialog
|
||||
onCancel={() => this._onCloseEventsBasedObjectSelectorDialog(null)}
|
||||
onChoose={parameters =>
|
||||
this._onCloseEventsBasedObjectSelectorDialog(parameters)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@@ -19,6 +19,10 @@ import Add from '../UI/CustomSvgIcons/Add';
|
||||
|
||||
const EVENTS_BASED_OBJECT_CLIPBOARD_KIND = 'Events Based Object';
|
||||
|
||||
export type EventsBasedObjectCreationParameters = {|
|
||||
isRenderedIn3D: boolean,
|
||||
|};
|
||||
|
||||
export type EventsBasedObjectCallbacks = {|
|
||||
onSelectEventsBasedObject: (eventsBasedObject: ?gdEventsBasedObject) => void,
|
||||
onDeleteEventsBasedObject: (
|
||||
@@ -30,6 +34,9 @@ export type EventsBasedObjectCallbacks = {|
|
||||
newName: string,
|
||||
cb: (boolean) => void
|
||||
) => void,
|
||||
onAddEventsBasedObject: (
|
||||
(parameters: ?EventsBasedObjectCreationParameters) => void
|
||||
) => void,
|
||||
onEventsBasedObjectRenamed: (eventsBasedObject: gdEventsBasedObject) => void,
|
||||
onOpenCustomObjectEditor: (eventsBasedObject: gdEventsBasedObject) => void,
|
||||
|};
|
||||
|
@@ -45,6 +45,7 @@ import {
|
||||
getObjectTreeViewItemId,
|
||||
type EventsBasedObjectProps,
|
||||
type EventsBasedObjectCallbacks,
|
||||
type EventsBasedObjectCreationParameters,
|
||||
} from './EventsBasedObjectTreeViewItemContent';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
import { type MenuItemTemplate } from '../UI/Menu/Menu.flow';
|
||||
@@ -538,6 +539,7 @@ const EventsFunctionsList = React.forwardRef<
|
||||
onDeleteEventsBasedObject,
|
||||
onRenameEventsBasedObject,
|
||||
onEventsBasedObjectRenamed,
|
||||
onAddEventsBasedObject,
|
||||
selectedEventsFunction,
|
||||
selectedEventsBasedBehavior,
|
||||
selectedEventsBasedObject,
|
||||
@@ -774,47 +776,57 @@ const EventsFunctionsList = React.forwardRef<
|
||||
|
||||
const addNewEventsBasedObject = React.useCallback(
|
||||
() => {
|
||||
const eventBasedObjects = eventsFunctionsExtension.getEventsBasedObjects();
|
||||
onAddEventsBasedObject(
|
||||
(parameters: ?EventsBasedObjectCreationParameters) => {
|
||||
if (!parameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = newNameGenerator('MyObject', name =>
|
||||
eventBasedObjects.has(name)
|
||||
const eventBasedObjects = eventsFunctionsExtension.getEventsBasedObjects();
|
||||
|
||||
const name = newNameGenerator('MyObject', name =>
|
||||
eventBasedObjects.has(name)
|
||||
);
|
||||
const newEventsBasedObject = eventBasedObjects.insertNew(
|
||||
name,
|
||||
eventBasedObjects.getCount()
|
||||
);
|
||||
newEventsBasedObject.markAsRenderedIn3D(parameters.isRenderedIn3D);
|
||||
if (unsavedChanges) {
|
||||
unsavedChanges.triggerUnsavedChanges();
|
||||
}
|
||||
forceUpdate();
|
||||
|
||||
const objectItemId = getObjectTreeViewItemId(newEventsBasedObject);
|
||||
|
||||
if (treeViewRef.current) {
|
||||
treeViewRef.current.openItems([
|
||||
objectItemId,
|
||||
extensionObjectsRootFolderId,
|
||||
]);
|
||||
}
|
||||
// Scroll to the new function.
|
||||
// Ideally, we'd wait for the list to be updated to scroll, but
|
||||
// to simplify the code, we just wait a few ms for a new render
|
||||
// to be done.
|
||||
setTimeout(() => {
|
||||
scrollToItem(objectItemId);
|
||||
}, 100); // A few ms is enough for a new render to be done.
|
||||
|
||||
// We focus it so the user can edit the name directly.
|
||||
onSelectEventsBasedObject(newEventsBasedObject);
|
||||
editName(objectItemId);
|
||||
}
|
||||
);
|
||||
const newEventsBasedObject = eventBasedObjects.insertNew(
|
||||
name,
|
||||
eventBasedObjects.getCount()
|
||||
);
|
||||
if (unsavedChanges) {
|
||||
unsavedChanges.triggerUnsavedChanges();
|
||||
}
|
||||
forceUpdate();
|
||||
|
||||
const objectItemId = getObjectTreeViewItemId(newEventsBasedObject);
|
||||
|
||||
if (treeViewRef.current) {
|
||||
treeViewRef.current.openItems([
|
||||
objectItemId,
|
||||
extensionObjectsRootFolderId,
|
||||
]);
|
||||
}
|
||||
// Scroll to the new function.
|
||||
// Ideally, we'd wait for the list to be updated to scroll, but
|
||||
// to simplify the code, we just wait a few ms for a new render
|
||||
// to be done.
|
||||
setTimeout(() => {
|
||||
scrollToItem(objectItemId);
|
||||
}, 100); // A few ms is enough for a new render to be done.
|
||||
|
||||
// We focus it so the user can edit the name directly.
|
||||
onSelectEventsBasedObject(newEventsBasedObject);
|
||||
editName(objectItemId);
|
||||
},
|
||||
[
|
||||
editName,
|
||||
onAddEventsBasedObject,
|
||||
eventsFunctionsExtension,
|
||||
forceUpdate,
|
||||
scrollToItem,
|
||||
onSelectEventsBasedObject,
|
||||
unsavedChanges,
|
||||
forceUpdate,
|
||||
onSelectEventsBasedObject,
|
||||
editName,
|
||||
scrollToItem,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -986,6 +998,7 @@ const EventsFunctionsList = React.forwardRef<
|
||||
onDeleteEventsBasedObject,
|
||||
onRenameEventsBasedObject,
|
||||
onEventsBasedObjectRenamed,
|
||||
onAddEventsBasedObject,
|
||||
addNewEventsFunction,
|
||||
selectedEventsBasedBehavior,
|
||||
selectedEventsBasedObject,
|
||||
@@ -1009,6 +1022,7 @@ const EventsFunctionsList = React.forwardRef<
|
||||
onDeleteEventsBasedObject,
|
||||
onRenameEventsBasedObject,
|
||||
onEventsBasedObjectRenamed,
|
||||
onAddEventsBasedObject,
|
||||
addNewEventsFunction,
|
||||
selectedEventsBasedBehavior,
|
||||
selectedEventsBasedObject,
|
||||
|
@@ -294,6 +294,7 @@ export default class EventsFunctionExtractorDialog extends React.Component<
|
||||
eventsBasedBehavior={null}
|
||||
eventsBasedObject={null}
|
||||
eventsFunctionsContainer={null}
|
||||
eventsFunctionsExtension={null}
|
||||
onConfigurationUpdated={() => {
|
||||
// Force re-running logic to see if Create button is disabled.
|
||||
this.forceUpdate();
|
||||
@@ -307,6 +308,7 @@ export default class EventsFunctionExtractorDialog extends React.Component<
|
||||
eventsBasedBehavior={null}
|
||||
eventsBasedObject={null}
|
||||
eventsFunctionsContainer={null}
|
||||
eventsFunctionsExtension={null}
|
||||
onParametersUpdated={() => {
|
||||
// Force the dialog to adapt its size
|
||||
this.forceUpdate();
|
||||
|
@@ -427,7 +427,7 @@ const InstructionEditorDialog = ({
|
||||
{newBehaviorDialogOpen && chosenObject && (
|
||||
<NewBehaviorDialog
|
||||
project={project}
|
||||
eventsFunctionsExtension={scope.eventsFunctionsExtension}
|
||||
eventsFunctionsExtension={scope.eventsFunctionsExtension || null}
|
||||
open={newBehaviorDialogOpen}
|
||||
objectType={chosenObject.getType()}
|
||||
objectBehaviorsTypes={listObjectBehaviorsTypes(chosenObject)}
|
||||
|
@@ -33,13 +33,13 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
focus,
|
||||
}));
|
||||
const { scope, instruction, expression, parameterIndex } = props;
|
||||
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
|
||||
|
||||
// We don't memo/callback this, as we want to recompute it every time something changes.
|
||||
// Because of the function getPreviousParameterValue.
|
||||
const getEffectNames = () => {
|
||||
const { layout } = scope;
|
||||
if (!layout) return [];
|
||||
|
||||
const layersSource = layout || eventsBasedObject;
|
||||
if (!layersSource) return [];
|
||||
const layerName =
|
||||
tryExtractStringLiteralContent(
|
||||
getPreviousParameterValue({
|
||||
@@ -48,8 +48,9 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
parameterIndex,
|
||||
})
|
||||
) || ''; // If no layer name is provided, this is the Base layer.
|
||||
if (!layout.hasLayerNamed(layerName)) return [];
|
||||
const layer = layout.getLayer(layerName);
|
||||
const layersContainer = layersSource.getLayers();
|
||||
if (!layersContainer.hasLayerNamed(layerName)) return [];
|
||||
const layer = layersContainer.getLayer(layerName);
|
||||
|
||||
return enumerateEffectNames(layer.getEffects()).sort();
|
||||
};
|
||||
@@ -60,10 +61,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
effectName => `"${effectName}"` === props.value
|
||||
);
|
||||
|
||||
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
|
||||
|
||||
// If the current value is not in the list, display an expression field.
|
||||
const [isExpressionField, setIsExpressionField] = React.useState(
|
||||
(!!props.value && !isCurrentValueInEffectNamesList) ||
|
||||
props.scope.eventsFunctionsExtension
|
||||
(!!props.value && !isCurrentValueInEffectNamesList) || !canAutocomplete
|
||||
);
|
||||
|
||||
const switchFieldType = () => {
|
||||
@@ -133,25 +135,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
)
|
||||
}
|
||||
renderButton={style =>
|
||||
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an effect</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
canAutocomplete ? (
|
||||
isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an effect</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@@ -35,12 +35,13 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
}));
|
||||
|
||||
const { project, scope, instruction, expression, parameterIndex } = props;
|
||||
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
|
||||
|
||||
// We don't memo/callback this, as we want to recompute it every time something changes.
|
||||
// Because of the function getPreviousParameterValue.
|
||||
const getEffectParameterNames = () => {
|
||||
const { layout } = scope;
|
||||
if (!layout || !project) return [];
|
||||
const layersSource = layout || eventsBasedObject;
|
||||
if (!layersSource || !project) return [];
|
||||
|
||||
const layerName =
|
||||
tryExtractStringLiteralContent(
|
||||
@@ -50,8 +51,9 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
parameterIndex: parameterIndex ? parameterIndex - 1 : null,
|
||||
})
|
||||
) || ''; // If no layer name is provided, this is the Base layer.
|
||||
if (!layout.hasLayerNamed(layerName)) return [];
|
||||
const layer = layout.getLayer(layerName);
|
||||
const layersContainer = layersSource.getLayers();
|
||||
if (!layersContainer.hasLayerNamed(layerName)) return [];
|
||||
const layer = layersContainer.getLayer(layerName);
|
||||
|
||||
const effectName = tryExtractStringLiteralContent(
|
||||
getPreviousParameterValue({
|
||||
@@ -82,10 +84,12 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
effectParameterName => `"${effectParameterName}"` === props.value
|
||||
);
|
||||
|
||||
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
|
||||
|
||||
// If the current value is not in the list, display an expression field.
|
||||
const [isExpressionField, setIsExpressionField] = React.useState(
|
||||
(!!props.value && !isCurrentValueInEffectParameterNamesList) ||
|
||||
props.scope.eventsFunctionsExtension
|
||||
!canAutocomplete
|
||||
);
|
||||
|
||||
const switchFieldType = () => {
|
||||
@@ -159,25 +163,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
)
|
||||
}
|
||||
renderButton={style =>
|
||||
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an effect property</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
canAutocomplete ? (
|
||||
isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an effect property</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@@ -31,12 +31,16 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
focus,
|
||||
}));
|
||||
|
||||
const { layout } = props.scope;
|
||||
const { layout, eventsFunctionsExtension, eventsBasedObject } = props.scope;
|
||||
|
||||
// The list is not kept with a memo because layers could be changed by
|
||||
// another component without this one to know.
|
||||
const layerNames = layout
|
||||
? mapFor(0, layout.getLayersCount(), i => {
|
||||
const layer = layout.getLayerAt(i);
|
||||
|
||||
const layersSource = layout || eventsBasedObject;
|
||||
const layersContainer = layersSource ? layersSource.getLayers() : null;
|
||||
const layerNames = layersContainer
|
||||
? mapFor(0, layersContainer.getLayersCount(), i => {
|
||||
const layer = layersContainer.getLayerAt(i);
|
||||
return layer.getName();
|
||||
})
|
||||
: [];
|
||||
@@ -45,10 +49,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
layerName => `"${layerName}"` === props.value
|
||||
);
|
||||
|
||||
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
|
||||
|
||||
// If the current value is not in the list of layers, display an expression field.
|
||||
const [isExpressionField, setIsExpressionField] = React.useState(
|
||||
(!!props.value && !isCurrentValueInLayersList) ||
|
||||
props.scope.eventsFunctionsExtension
|
||||
(!!props.value && !isCurrentValueInLayersList) || !canAutocomplete
|
||||
);
|
||||
|
||||
const switchFieldType = () => {
|
||||
@@ -122,25 +127,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
)
|
||||
}
|
||||
renderButton={style =>
|
||||
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select a layer</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
canAutocomplete ? (
|
||||
isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select a layer</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@@ -43,6 +43,8 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
parameterIndex,
|
||||
} = props;
|
||||
|
||||
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
|
||||
|
||||
// We don't memo/callback this, as we want to recompute it every time something changes.
|
||||
// Because of the function getLastObjectParameterValue.
|
||||
const getAnimationNames = () => {
|
||||
@@ -60,7 +62,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
|
||||
const object = getObjectByName(
|
||||
project.getObjects(),
|
||||
scope.layout ? scope.layout.getObjects() : null,
|
||||
layout ? layout.getObjects() : null,
|
||||
objectName
|
||||
);
|
||||
if (!object) {
|
||||
@@ -129,10 +131,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
animationName => `"${animationName}"` === props.value
|
||||
);
|
||||
|
||||
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
|
||||
|
||||
// If the current value is not in the list of animation names, display an expression field.
|
||||
const [isExpressionField, setIsExpressionField] = React.useState(
|
||||
(!!props.value && !isCurrentValueInAnimationNamesList) ||
|
||||
props.scope.eventsFunctionsExtension
|
||||
(!!props.value && !isCurrentValueInAnimationNamesList) || !canAutocomplete
|
||||
);
|
||||
|
||||
const switchFieldType = () => {
|
||||
@@ -202,25 +205,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
)
|
||||
}
|
||||
renderButton={style =>
|
||||
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an animation</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
canAutocomplete ? (
|
||||
isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an animation</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@@ -42,6 +42,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
expression,
|
||||
parameterIndex,
|
||||
} = props;
|
||||
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
|
||||
|
||||
// We don't memo/callback this, as we want to recompute it every time something changes.
|
||||
// Because of the function getLastObjectParameterValue.
|
||||
@@ -59,7 +60,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
|
||||
const object = getObjectByName(
|
||||
project.getObjects(),
|
||||
scope.layout ? scope.layout.getObjects() : null,
|
||||
layout ? layout.getObjects() : null,
|
||||
objectOrGroupName
|
||||
);
|
||||
if (object) {
|
||||
@@ -67,7 +68,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
}
|
||||
const group = getObjectGroupByName(
|
||||
project.getObjects(),
|
||||
scope.layout ? scope.layout.getObjects() : null,
|
||||
layout ? layout.getObjects() : null,
|
||||
objectOrGroupName
|
||||
);
|
||||
if (group) {
|
||||
@@ -76,7 +77,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
objectName => {
|
||||
const object = getObjectByName(
|
||||
project.getObjects(),
|
||||
scope.layout ? scope.layout.getObjects() : null,
|
||||
layout ? layout.getObjects() : null,
|
||||
objectName
|
||||
);
|
||||
if (!object) {
|
||||
@@ -97,10 +98,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
effectName => `"${effectName}"` === props.value
|
||||
);
|
||||
|
||||
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
|
||||
|
||||
// If the current value is not in the list, display an expression field.
|
||||
const [isExpressionField, setIsExpressionField] = React.useState(
|
||||
(!!props.value && !isCurrentValueInEffectNamesList) ||
|
||||
props.scope.eventsFunctionsExtension
|
||||
(!!props.value && !isCurrentValueInEffectNamesList) || !canAutocomplete
|
||||
);
|
||||
|
||||
const switchFieldType = () => {
|
||||
@@ -170,25 +172,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
)
|
||||
}
|
||||
renderButton={style =>
|
||||
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an effect</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
canAutocomplete ? (
|
||||
isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an effect</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@@ -38,12 +38,14 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
focus,
|
||||
}));
|
||||
|
||||
const { scope } = props;
|
||||
const { layout, eventsFunctionsExtension, eventsBasedObject } = scope;
|
||||
|
||||
// We don't memo/callback this, as we want to recompute it every time something changes.
|
||||
// Because of the function getLastObjectParameterValue.
|
||||
const getEffectParameterNames = (): Array<string> => {
|
||||
const {
|
||||
project,
|
||||
scope,
|
||||
instructionMetadata,
|
||||
instruction,
|
||||
expressionMetadata,
|
||||
@@ -72,7 +74,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
let effectType: string | null = null;
|
||||
const object = getObjectByName(
|
||||
project.getObjects(),
|
||||
scope.layout ? scope.layout.getObjects() : null,
|
||||
layout ? layout.getObjects() : null,
|
||||
objectOrGroupName
|
||||
);
|
||||
if (object && object.getEffects().hasEffectNamed(effectName)) {
|
||||
@@ -85,7 +87,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
if (!effectType) {
|
||||
const group = getObjectGroupByName(
|
||||
project.getObjects(),
|
||||
scope.layout ? scope.layout.getObjects() : null,
|
||||
layout ? layout.getObjects() : null,
|
||||
objectOrGroupName
|
||||
);
|
||||
if (group) {
|
||||
@@ -96,7 +98,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
objectName => {
|
||||
const object = getObjectByName(
|
||||
project.getObjects(),
|
||||
scope.layout ? scope.layout.getObjects() : null,
|
||||
layout ? layout.getObjects() : null,
|
||||
objectName
|
||||
);
|
||||
if (!object) {
|
||||
@@ -139,10 +141,12 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
effectParameterName => `"${effectParameterName}"` === props.value
|
||||
);
|
||||
|
||||
const canAutocomplete = !eventsFunctionsExtension || eventsBasedObject;
|
||||
|
||||
// If the current value is not in the list, display an expression field.
|
||||
const [isExpressionField, setIsExpressionField] = React.useState(
|
||||
(!!props.value && !isCurrentValueInEffectParameterNamesList) ||
|
||||
props.scope.eventsFunctionsExtension
|
||||
!canAutocomplete
|
||||
);
|
||||
|
||||
const switchFieldType = () => {
|
||||
@@ -216,25 +220,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
)
|
||||
}
|
||||
renderButton={style =>
|
||||
props.scope.eventsFunctionsExtension ? null : isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an effect property</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
canAutocomplete ? (
|
||||
isExpressionField ? (
|
||||
<FlatButton
|
||||
id="switch-expression-select"
|
||||
leftIcon={<TypeCursorSelect />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Select an effect property</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
id="switch-expression-select"
|
||||
icon={<Functions />}
|
||||
style={style}
|
||||
primary
|
||||
label={<Trans>Use an expression</Trans>}
|
||||
onClick={switchFieldType}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@@ -22,6 +22,7 @@ import { getHelpLink } from '../Utils/HelpLink';
|
||||
import Window from '../Utils/Window';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import InlineCheckbox from '../UI/InlineCheckbox';
|
||||
|
||||
const defaultMaximumNumberOfPlayers = 4;
|
||||
const minimumValueForMaximumNumberOfPlayers = 2;
|
||||
@@ -37,6 +38,9 @@ const MultiplayerAdmin = ({ gameId }: Props) => {
|
||||
const [isSaving, setIsSaving] = React.useState<boolean>(false);
|
||||
const [maxPlayersValue, setMaxPlayersValue] = React.useState<number>(2);
|
||||
const [minPlayersValue, setMinPlayersValue] = React.useState<number>(1);
|
||||
const [canJoinAfterStart, setCanJoinAfterStart] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const { getAuthorizationHeader, profile, limits } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
@@ -55,6 +59,7 @@ const MultiplayerAdmin = ({ gameId }: Props) => {
|
||||
if (lobbyConfiguration) {
|
||||
setMaxPlayersValue(lobbyConfiguration.maxPlayers);
|
||||
setMinPlayersValue(lobbyConfiguration.minPlayers);
|
||||
setCanJoinAfterStart(lobbyConfiguration.canJoinAfterStart);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,6 +170,7 @@ const MultiplayerAdmin = ({ gameId }: Props) => {
|
||||
gameId,
|
||||
maxPlayers: maxPlayersValue,
|
||||
minPlayers: minPlayersValue,
|
||||
canJoinAfterStart,
|
||||
}
|
||||
);
|
||||
setLobbyConfiguration(updatedLobbyConfiguration);
|
||||
@@ -183,13 +189,21 @@ const MultiplayerAdmin = ({ gameId }: Props) => {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[getAuthorizationHeader, gameId, userId, maxPlayersValue, minPlayersValue]
|
||||
[
|
||||
getAuthorizationHeader,
|
||||
gameId,
|
||||
userId,
|
||||
maxPlayersValue,
|
||||
minPlayersValue,
|
||||
canJoinAfterStart,
|
||||
]
|
||||
);
|
||||
|
||||
const hasUnsavedModifications =
|
||||
lobbyConfiguration &&
|
||||
(lobbyConfiguration.maxPlayers !== maxPlayersValue ||
|
||||
lobbyConfiguration.minPlayers !== minPlayersValue);
|
||||
lobbyConfiguration.minPlayers !== minPlayersValue ||
|
||||
lobbyConfiguration.canJoinAfterStart !== canJoinAfterStart);
|
||||
const canSave = hasUnsavedModifications;
|
||||
|
||||
const helpLink = getHelpLink('/all-features/multiplayer/');
|
||||
@@ -256,6 +270,17 @@ const MultiplayerAdmin = ({ gameId }: Props) => {
|
||||
{maxPlayersSelectOptions}
|
||||
</SelectField>
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<InlineCheckbox
|
||||
label={
|
||||
<Trans>Allow players to join after the game has started</Trans>
|
||||
}
|
||||
checked={canJoinAfterStart}
|
||||
onCheck={(e, checked) => {
|
||||
setCanJoinAfterStart(checked);
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
<Line noMargin justifyContent="flex-end">
|
||||
<LeftLoader isLoading={isSaving}>
|
||||
<RaisedButton
|
||||
|
@@ -186,10 +186,32 @@ const CompactInstancePropertiesEditor = ({
|
||||
]
|
||||
);
|
||||
|
||||
if (!object || !instance || !instanceSchema) return null;
|
||||
|
||||
const shouldDisplayTileSetVisualizer =
|
||||
object.getType() === 'TileMap::SimpleTileMap';
|
||||
!!object && object.getType() === 'TileMap::SimpleTileMap';
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!shouldDisplayTileSetVisualizer) {
|
||||
// Reset tile map tile selection if tile set visualizer should
|
||||
// not be displayed (an instance that is not a tile map is selected).
|
||||
onSelectTileMapTile(null);
|
||||
}
|
||||
// Reset tile map tile selection if the component is unmounted
|
||||
// (Useful when component is unmounted on an Undo user command).
|
||||
return () => onSelectTileMapTile(null);
|
||||
},
|
||||
[shouldDisplayTileSetVisualizer, onSelectTileMapTile]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
onSelectTileMapTile(null);
|
||||
},
|
||||
// Reset tile map tile selection if instance changes.
|
||||
[instance.ptr, onSelectTileMapTile]
|
||||
);
|
||||
|
||||
if (!object || !instance || !instanceSchema) return null;
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
@@ -211,12 +233,17 @@ const CompactInstancePropertiesEditor = ({
|
||||
instances={instances}
|
||||
onInstancesModified={onInstancesModified}
|
||||
/>
|
||||
<Spacer />
|
||||
</Column>
|
||||
{shouldDisplayTileSetVisualizer && (
|
||||
<>
|
||||
<Column>
|
||||
<Spacer />
|
||||
<Separator />
|
||||
<Line alignItems="center" justifyContent="space-between">
|
||||
<Text size="sub-title" noMargin>
|
||||
<Trans>Tilemap painter</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<TileSetVisualizer
|
||||
project={project}
|
||||
objectConfiguration={object.getConfiguration()}
|
||||
@@ -232,7 +259,6 @@ const CompactInstancePropertiesEditor = ({
|
||||
{object && shouldDisplayVariablesList ? (
|
||||
<>
|
||||
<Column>
|
||||
<Spacer />
|
||||
<Separator />
|
||||
<Line alignItems="center" justifyContent="space-between">
|
||||
<Text size="sub-title" noMargin>
|
||||
|
@@ -274,7 +274,15 @@ class TileMapTilePreview {
|
||||
tileSize
|
||||
);
|
||||
|
||||
texture = new PIXI.Texture(atlasTexture, rect);
|
||||
try {
|
||||
texture = new PIXI.Texture(atlasTexture, rect);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Tile could not be extracted from atlas texture:`,
|
||||
error
|
||||
);
|
||||
texture = PixiResourcesLoader.getInvalidPIXITexture();
|
||||
}
|
||||
this.cache.set(cacheKey, texture);
|
||||
}
|
||||
} else if (tileMapTileSelection.kind === 'erase') {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import { Column, Line, Spacer } from '../UI/Grid';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import ResourcesLoader from '../ResourcesLoader';
|
||||
import Erase from '../UI/CustomSvgIcons/Erase';
|
||||
@@ -12,11 +12,29 @@ import { LineStackLayout } from '../UI/Layout';
|
||||
import FlipHorizontal from '../UI/CustomSvgIcons/FlipHorizontal';
|
||||
import FlipVertical from '../UI/CustomSvgIcons/FlipVertical';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import { useLongTouch, type ClientCoordinates } from '../Utils/UseLongTouch';
|
||||
import Text from '../UI/Text';
|
||||
|
||||
const styles = {
|
||||
tileContainer: { flex: 1, position: 'relative', display: 'flex' },
|
||||
tileContainer: {
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
overflow: 'auto',
|
||||
},
|
||||
atlasImage: { flex: 1, imageRendering: 'pixelated' },
|
||||
icon: { fontSize: 18 },
|
||||
tooltipContent: {
|
||||
position: 'absolute',
|
||||
// Outside of theme.
|
||||
background: 'white',
|
||||
border: '1px solid black',
|
||||
color: 'black',
|
||||
padding: '1px 3px',
|
||||
},
|
||||
tooltipAnchor: {
|
||||
position: 'relative',
|
||||
},
|
||||
};
|
||||
|
||||
const useStylesForTile = (highlighted: boolean) =>
|
||||
@@ -27,7 +45,9 @@ const useStylesForTile = (highlighted: boolean) =>
|
||||
boxSizing: 'border-box',
|
||||
border: highlighted ? '2px solid red' : undefined,
|
||||
'&:hover': {
|
||||
border: highlighted ? '2px solid pink' : '1px solid white',
|
||||
border: highlighted
|
||||
? '2px solid orange'
|
||||
: `1px solid ${theme.palette.type === 'dark' ? 'white' : 'black'}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -97,6 +117,21 @@ const getGridCoordinatesFromPointerCoordinates = ({
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const getImageCoordinatesFromPointerEvent = (
|
||||
event: PointerEvent | MouseEvent | ClientCoordinates
|
||||
) => {
|
||||
const divContainer = event.currentTarget;
|
||||
if (!(divContainer instanceof HTMLDivElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = divContainer.getBoundingClientRect();
|
||||
|
||||
const mouseX = event.clientX + divContainer.scrollLeft - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
return { mouseX, mouseY };
|
||||
};
|
||||
|
||||
const addOrRemoveCoordinatesInArray = (
|
||||
array: TileMapCoordinates[],
|
||||
newCoordinates: TileMapCoordinates
|
||||
@@ -120,6 +155,7 @@ type TileProps = {|
|
||||
width?: number,
|
||||
height?: number,
|
||||
title?: string,
|
||||
displayTooltip?: boolean,
|
||||
|};
|
||||
|
||||
const Tile = ({
|
||||
@@ -130,8 +166,13 @@ const Tile = ({
|
||||
height = 1,
|
||||
highlighted,
|
||||
title,
|
||||
displayTooltip,
|
||||
}: TileProps) => {
|
||||
const classes = useStylesForTile(!!highlighted);
|
||||
// Tooltip position has to be adapted because the image is in a overflow auto parent
|
||||
// that hides the tooltip if it is displayed outside of itself.
|
||||
const position =
|
||||
x <= 1 && y <= 1 ? { right: -30 } : y <= 1 ? { left: -40 } : { top: -40 };
|
||||
return (
|
||||
<div
|
||||
className={classes.tile}
|
||||
@@ -141,9 +182,23 @@ const Tile = ({
|
||||
width: size * width,
|
||||
height: size * height,
|
||||
}}
|
||||
// TODO: find a way to display title on mobile.
|
||||
title={title}
|
||||
/>
|
||||
>
|
||||
{displayTooltip && (
|
||||
<div style={styles.tooltipAnchor}>
|
||||
<div
|
||||
style={{
|
||||
...styles.tooltipContent,
|
||||
...position,
|
||||
}}
|
||||
>
|
||||
<Text color="inherit" noMargin>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -220,6 +275,17 @@ const TileSetVisualizer = ({
|
||||
x: number,
|
||||
y: number,
|
||||
|}>(null);
|
||||
const [touchStartCoordinates, setTouchStartCoordinates] = React.useState<?{|
|
||||
x: number,
|
||||
y: number,
|
||||
|}>(null);
|
||||
const [shouldCancelClick, setShouldCancelClick] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [
|
||||
tileIdDisplayGridCoordinates,
|
||||
setTileIdDisplayGridCoordinates,
|
||||
] = React.useState<?TileMapCoordinates>(null);
|
||||
const [
|
||||
rectangularSelectionTilePreview,
|
||||
setRectangularSelectionTilePreview,
|
||||
@@ -233,14 +299,37 @@ const TileSetVisualizer = ({
|
||||
x: number,
|
||||
y: number,
|
||||
}>(null);
|
||||
|
||||
const imageWidth = tileContainerRef.current
|
||||
? parseFloat(
|
||||
getComputedStyle(tileContainerRef.current).width.replace('px', '')
|
||||
)
|
||||
const imageElement = tileContainerRef.current
|
||||
? tileContainerRef.current.getElementsByTagName('img')[0]
|
||||
: null;
|
||||
const imageWidth = imageElement
|
||||
? parseFloat(getComputedStyle(imageElement).width.replace('px', ''))
|
||||
: 0;
|
||||
const displayedTileSize = imageWidth ? imageWidth / columnCount : null;
|
||||
|
||||
const displayTileIdTooltip = React.useCallback(
|
||||
(e: ClientCoordinates) => {
|
||||
setShouldCancelClick(true);
|
||||
if (!displayedTileSize) return;
|
||||
|
||||
const imageCoordinates = getImageCoordinatesFromPointerEvent(e);
|
||||
if (!imageCoordinates) return;
|
||||
|
||||
const { x, y } = getGridCoordinatesFromPointerCoordinates({
|
||||
pointerX: imageCoordinates.mouseX,
|
||||
pointerY: imageCoordinates.mouseY,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
});
|
||||
|
||||
setTileIdDisplayGridCoordinates({ x, y });
|
||||
},
|
||||
[displayedTileSize, columnCount, rowCount]
|
||||
);
|
||||
|
||||
const longTouchProps = useLongTouch(displayTileIdTooltip);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
forceUpdate();
|
||||
@@ -251,33 +340,33 @@ const TileSetVisualizer = ({
|
||||
);
|
||||
|
||||
const onPointerDown = React.useCallback((event: PointerEvent) => {
|
||||
if (!(event.currentTarget instanceof HTMLDivElement)) {
|
||||
return;
|
||||
if (event.pointerType === 'touch') {
|
||||
setTouchStartCoordinates({ x: event.pageX, y: event.pageY });
|
||||
}
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
const mouseX = event.clientX - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
|
||||
setClickStartCoordinates({ x: mouseX, y: mouseY });
|
||||
const imageCoordinates = getImageCoordinatesFromPointerEvent(event);
|
||||
if (!imageCoordinates) return;
|
||||
setClickStartCoordinates({
|
||||
x: imageCoordinates.mouseX,
|
||||
y: imageCoordinates.mouseY,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onPointerMove = React.useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (
|
||||
!clickStartCoordinates ||
|
||||
!(event.currentTarget instanceof HTMLDivElement) ||
|
||||
!displayedTileSize ||
|
||||
!allowMultipleSelection
|
||||
!allowMultipleSelection ||
|
||||
event.pointerType === 'touch'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
const imageCoordinates = getImageCoordinatesFromPointerEvent(event);
|
||||
if (!imageCoordinates) return;
|
||||
|
||||
const mouseX = event.clientX - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
const { x, y } = getGridCoordinatesFromPointerCoordinates({
|
||||
pointerX: mouseX,
|
||||
pointerY: mouseY,
|
||||
pointerX: imageCoordinates.mouseX,
|
||||
pointerY: imageCoordinates.mouseY,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
@@ -311,21 +400,34 @@ const TileSetVisualizer = ({
|
||||
);
|
||||
|
||||
const onPointerUp = React.useCallback(
|
||||
(event: MouseEvent) => {
|
||||
(event: PointerEvent) => {
|
||||
try {
|
||||
if (
|
||||
!(event.currentTarget instanceof HTMLDivElement) ||
|
||||
!displayedTileSize
|
||||
) {
|
||||
if (!displayedTileSize) return;
|
||||
if (shouldCancelClick) {
|
||||
setShouldCancelClick(false);
|
||||
setTileIdDisplayGridCoordinates(null);
|
||||
return;
|
||||
}
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
|
||||
const mouseX = event.clientX - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
let isTouchDevice = false;
|
||||
|
||||
if (event.pointerType === 'touch') {
|
||||
isTouchDevice = true;
|
||||
if (
|
||||
!touchStartCoordinates ||
|
||||
Math.abs(event.pageX - touchStartCoordinates.x) > 30 ||
|
||||
Math.abs(event.pageY - touchStartCoordinates.y) > 30
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const imageCoordinates = getImageCoordinatesFromPointerEvent(event);
|
||||
if (!imageCoordinates) return;
|
||||
|
||||
const { x, y } = getGridCoordinatesFromPointerCoordinates({
|
||||
pointerX: mouseX,
|
||||
pointerY: mouseY,
|
||||
pointerX: imageCoordinates.mouseX,
|
||||
pointerY: imageCoordinates.mouseY,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
@@ -364,10 +466,14 @@ const TileSetVisualizer = ({
|
||||
tileMapTileSelection && tileMapTileSelection.kind === 'multiple'
|
||||
? { ...tileMapTileSelection }
|
||||
: { kind: 'multiple', coordinates: [] };
|
||||
if (startX === x && startY === y) {
|
||||
// Click on a tile.
|
||||
if (
|
||||
(startX === x && startY === y) ||
|
||||
// Do not allow rectangular select on touch device as it conflicts with basic scrolling gestures.
|
||||
isTouchDevice
|
||||
) {
|
||||
if (
|
||||
tileMapTileSelection &&
|
||||
// Click on a tile.
|
||||
tileMapTileSelection.kind === 'multiple'
|
||||
) {
|
||||
addOrRemoveCoordinatesInArray(newSelection.coordinates, {
|
||||
@@ -399,6 +505,7 @@ const TileSetVisualizer = ({
|
||||
} finally {
|
||||
setClickStartCoordinates(null);
|
||||
setRectangularSelectionTilePreview(null);
|
||||
setTouchStartCoordinates(null);
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -411,17 +518,11 @@ const TileSetVisualizer = ({
|
||||
shouldFlipVertically,
|
||||
allowMultipleSelection,
|
||||
clickStartCoordinates,
|
||||
shouldCancelClick,
|
||||
touchStartCoordinates,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
// On dismount, remove tile map selection.
|
||||
return () => onSelectTileMapTile(null);
|
||||
},
|
||||
[onSelectTileMapTile]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (tileMapTileSelection && tileMapTileSelection.kind === 'single') {
|
||||
@@ -436,19 +537,13 @@ const TileSetVisualizer = ({
|
||||
|
||||
const onHoverAtlas = React.useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (
|
||||
!(event.currentTarget instanceof HTMLDivElement) ||
|
||||
!displayedTileSize
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
if (!displayedTileSize) return;
|
||||
|
||||
const mouseX = event.clientX - bounds.left + 1;
|
||||
const mouseY = event.clientY - bounds.top + 1;
|
||||
const imageCoordinates = getImageCoordinatesFromPointerEvent(event);
|
||||
if (!imageCoordinates) return;
|
||||
const { x, y } = getGridCoordinatesFromPointerCoordinates({
|
||||
pointerX: mouseX,
|
||||
pointerY: mouseY,
|
||||
pointerX: imageCoordinates.mouseX,
|
||||
pointerY: imageCoordinates.mouseY,
|
||||
columnCount,
|
||||
rowCount,
|
||||
displayedTileSize,
|
||||
@@ -460,99 +555,114 @@ const TileSetVisualizer = ({
|
||||
|
||||
const interactionCallbacks = {
|
||||
onMouseMove: onHoverAtlas,
|
||||
onPointerDown: onPointerDown,
|
||||
onPointerUp: onPointerUp,
|
||||
onPointerMove: onPointerMove,
|
||||
onPointerDown,
|
||||
onPointerUp,
|
||||
onPointerMove,
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
{showPaintingToolbar && (
|
||||
<LineStackLayout alignItems="center">
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={
|
||||
!!tileMapTileSelection && tileMapTileSelection.kind === 'erase'
|
||||
}
|
||||
onClick={e => {
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'erase'
|
||||
)
|
||||
onSelectTileMapTile(null);
|
||||
else onSelectTileMapTile({ kind: 'erase' });
|
||||
}}
|
||||
>
|
||||
<Erase style={styles.icon} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={
|
||||
!!tileMapTileSelection && tileMapTileSelection.kind === 'single'
|
||||
}
|
||||
onClick={e => {
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
)
|
||||
onSelectTileMapTile(null);
|
||||
else
|
||||
onSelectTileMapTile({
|
||||
kind: 'single',
|
||||
coordinates: lastSelectedTile || { x: 0, y: 0 },
|
||||
flipHorizontally: shouldFlipHorizontally,
|
||||
flipVertically: shouldFlipVertically,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Brush style={styles.icon} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={shouldFlipHorizontally}
|
||||
onClick={e => {
|
||||
const newShouldFlipHorizontally = !shouldFlipHorizontally;
|
||||
setShouldFlipHorizontally(newShouldFlipHorizontally);
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
) {
|
||||
onSelectTileMapTile({
|
||||
...tileMapTileSelection,
|
||||
flipHorizontally: newShouldFlipHorizontally,
|
||||
});
|
||||
<>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<LineStackLayout alignItems="center" noMargin>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
}
|
||||
onClick={e => {
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
)
|
||||
onSelectTileMapTile(null);
|
||||
else
|
||||
onSelectTileMapTile({
|
||||
kind: 'single',
|
||||
coordinates: lastSelectedTile || { x: 0, y: 0 },
|
||||
flipHorizontally: shouldFlipHorizontally,
|
||||
flipVertically: shouldFlipVertically,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Brush style={styles.icon} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={shouldFlipHorizontally}
|
||||
disabled={
|
||||
!tileMapTileSelection ||
|
||||
tileMapTileSelection.kind !== 'single'
|
||||
}
|
||||
onClick={e => {
|
||||
const newShouldFlipHorizontally = !shouldFlipHorizontally;
|
||||
setShouldFlipHorizontally(newShouldFlipHorizontally);
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
) {
|
||||
onSelectTileMapTile({
|
||||
...tileMapTileSelection,
|
||||
flipHorizontally: newShouldFlipHorizontally,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FlipHorizontal style={styles.icon} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={shouldFlipVertically}
|
||||
disabled={
|
||||
!tileMapTileSelection ||
|
||||
tileMapTileSelection.kind !== 'single'
|
||||
}
|
||||
onClick={e => {
|
||||
const newShouldFlipVertically = !shouldFlipVertically;
|
||||
setShouldFlipVertically(newShouldFlipVertically);
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
) {
|
||||
onSelectTileMapTile({
|
||||
...tileMapTileSelection,
|
||||
flipVertically: newShouldFlipVertically,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FlipVertical style={styles.icon} />
|
||||
</IconButton>
|
||||
</LineStackLayout>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={
|
||||
!!tileMapTileSelection && tileMapTileSelection.kind === 'erase'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FlipHorizontal style={styles.icon} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
selected={shouldFlipVertically}
|
||||
onClick={e => {
|
||||
const newShouldFlipVertically = !shouldFlipVertically;
|
||||
setShouldFlipVertically(newShouldFlipVertically);
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'single'
|
||||
) {
|
||||
onSelectTileMapTile({
|
||||
...tileMapTileSelection,
|
||||
flipVertically: newShouldFlipVertically,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FlipVertical style={styles.icon} />
|
||||
</IconButton>
|
||||
</LineStackLayout>
|
||||
onClick={e => {
|
||||
if (
|
||||
!!tileMapTileSelection &&
|
||||
tileMapTileSelection.kind === 'erase'
|
||||
)
|
||||
onSelectTileMapTile(null);
|
||||
else onSelectTileMapTile({ kind: 'erase' });
|
||||
}}
|
||||
>
|
||||
<Erase style={styles.icon} />
|
||||
</IconButton>
|
||||
</Line>
|
||||
<Spacer />
|
||||
</>
|
||||
)}
|
||||
<Line justifyContent="stretch">
|
||||
<Line justifyContent="stretch" noMargin>
|
||||
{atlasResourceName && (
|
||||
<div
|
||||
style={styles.tileContainer}
|
||||
ref={tileContainerRef}
|
||||
{...(interactive ? interactionCallbacks : undefined)}
|
||||
{...longTouchProps}
|
||||
>
|
||||
<CorsAwareImage
|
||||
style={styles.atlasImage}
|
||||
@@ -630,6 +740,21 @@ const TileSetVisualizer = ({
|
||||
height={rectangularSelectionTilePreview.height}
|
||||
/>
|
||||
)}
|
||||
{tileIdDisplayGridCoordinates && displayedTileSize && (
|
||||
<Tile
|
||||
key={`id-tooltip-tile`}
|
||||
highlighted
|
||||
size={displayedTileSize}
|
||||
x={tileIdDisplayGridCoordinates.x}
|
||||
y={tileIdDisplayGridCoordinates.y}
|
||||
title={getTileIdFromGridCoordinates({
|
||||
x: tileIdDisplayGridCoordinates.x,
|
||||
y: tileIdDisplayGridCoordinates.y,
|
||||
rowCount,
|
||||
}).toString()}
|
||||
displayTooltip
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Line>
|
||||
|
@@ -47,7 +47,10 @@ import TileMapTilePreview, {
|
||||
getTilesGridCoordinatesFromPointerSceneCoordinates,
|
||||
updateSceneToTileMapTransformation,
|
||||
} from './TileMapTilePreview';
|
||||
import { type TileMapTileSelection } from './TileSetVisualizer';
|
||||
import {
|
||||
getTileIdFromGridCoordinates,
|
||||
type TileMapTileSelection,
|
||||
} from './TileSetVisualizer';
|
||||
import ClickInterceptor from './ClickInterceptor';
|
||||
import getObjectByName from '../Utils/GetObjectByName';
|
||||
import { AffineTransformation } from '../Utils/AffineTransformation';
|
||||
@@ -776,6 +779,7 @@ export default class InstancesEditor extends Component<Props> {
|
||||
renderedInstance &&
|
||||
renderedInstance.constructor.name === 'RenderedSimpleTileMapInstance'
|
||||
) {
|
||||
// $FlowIgnore
|
||||
const editableTileMap = renderedInstance.getEditableTileMap();
|
||||
if (!editableTileMap) {
|
||||
console.error(
|
||||
@@ -794,7 +798,6 @@ export default class InstancesEditor extends Component<Props> {
|
||||
if (!scales) return;
|
||||
const { scaleX, scaleY } = scales;
|
||||
const tileSet = getTileSet(object);
|
||||
const editableTileMapLayer = editableTileMap.getTileLayer(0);
|
||||
const tileMapGridCoordinates = getTilesGridCoordinatesFromPointerSceneCoordinates(
|
||||
{
|
||||
coordinates: sceneCoordinates,
|
||||
@@ -803,87 +806,140 @@ export default class InstancesEditor extends Component<Props> {
|
||||
}
|
||||
);
|
||||
|
||||
let shouldTrimAfterOperations = false;
|
||||
|
||||
if (tileMapTileSelection.kind === 'single') {
|
||||
shouldTrimAfterOperations = editableTileMap.isEmpty();
|
||||
// TODO: Optimize list execution to make sure the most important size changing operations are done first.
|
||||
let cumulatedUnshiftedRows = 0,
|
||||
cumulatedUnshiftedColumns = 0;
|
||||
const tileId = getTileIdFromGridCoordinates({
|
||||
rowCount: tileSet.rowCount,
|
||||
...tileMapTileSelection.coordinates,
|
||||
});
|
||||
|
||||
const tileDefinition = editableTileMap.getTileDefinition(tileId);
|
||||
if (!tileDefinition) return;
|
||||
|
||||
const layer = editableTileMap.getTileLayer(0);
|
||||
if (!layer) return;
|
||||
|
||||
tileMapGridCoordinates.forEach(({ x: gridX, y: gridY }) => {
|
||||
const {
|
||||
unshiftedRows,
|
||||
unshiftedColumns,
|
||||
appendedRows,
|
||||
appendedColumns,
|
||||
} = editableTileMapLayer.setTile(
|
||||
// If rows or columns have been unshifted in the previous tile setting operations,
|
||||
// we have to take them into account for the current coordinates.
|
||||
gridX + cumulatedUnshiftedColumns,
|
||||
gridY + cumulatedUnshiftedRows,
|
||||
tileSet.rowCount * tileMapTileSelection.coordinates.x +
|
||||
tileMapTileSelection.coordinates.y,
|
||||
{
|
||||
flipVertically: tileMapTileSelection.flipVertically,
|
||||
flipHorizontally: tileMapTileSelection.flipHorizontally,
|
||||
flipDiagonally: false,
|
||||
}
|
||||
// If rows or columns have been unshifted in the previous tile setting operations,
|
||||
// we have to take them into account for the current coordinates.
|
||||
const x = gridX + cumulatedUnshiftedColumns;
|
||||
const y = gridY + cumulatedUnshiftedRows;
|
||||
const rowsToAppend = Math.max(
|
||||
0,
|
||||
y - (editableTileMap.getDimensionY() - 1)
|
||||
);
|
||||
cumulatedUnshiftedRows += unshiftedRows;
|
||||
cumulatedUnshiftedColumns += unshiftedColumns;
|
||||
const columnsToAppend = Math.max(
|
||||
0,
|
||||
x - (editableTileMap.getDimensionX() - 1)
|
||||
);
|
||||
const rowsToUnshift = Math.abs(Math.min(0, y));
|
||||
const columnsToUnshift = Math.abs(Math.min(0, x));
|
||||
if (
|
||||
rowsToAppend > 0 ||
|
||||
columnsToAppend > 0 ||
|
||||
rowsToUnshift > 0 ||
|
||||
columnsToUnshift > 0
|
||||
) {
|
||||
editableTileMap.increaseDimensions(
|
||||
columnsToAppend,
|
||||
columnsToUnshift,
|
||||
rowsToAppend,
|
||||
rowsToUnshift
|
||||
);
|
||||
}
|
||||
const newX = x + columnsToUnshift;
|
||||
const newY = y + rowsToUnshift;
|
||||
|
||||
editableTileMap.setTile(newX, newY, 0, tileId);
|
||||
editableTileMap.flipTileOnX(
|
||||
newX,
|
||||
newY,
|
||||
0,
|
||||
tileMapTileSelection.flipHorizontally
|
||||
);
|
||||
editableTileMap.flipTileOnY(
|
||||
newX,
|
||||
newY,
|
||||
0,
|
||||
tileMapTileSelection.flipVertically
|
||||
);
|
||||
|
||||
cumulatedUnshiftedRows += rowsToUnshift;
|
||||
cumulatedUnshiftedColumns += columnsToUnshift;
|
||||
// The instance angle is not considered when moving the instance after
|
||||
// rows/columns were added/removed because the instance position does not
|
||||
// include the rotation transformation. Otherwise, we could have used
|
||||
// tileMapToSceneTransformation to get the new position.
|
||||
selectedInstance.setX(
|
||||
selectedInstance.getX() -
|
||||
unshiftedColumns * (tileSet.tileSize * scaleX)
|
||||
columnsToUnshift * (tileSet.tileSize * scaleX)
|
||||
);
|
||||
selectedInstance.setY(
|
||||
selectedInstance.getY() -
|
||||
unshiftedRows * (tileSet.tileSize * scaleY)
|
||||
rowsToUnshift * (tileSet.tileSize * scaleY)
|
||||
);
|
||||
if (selectedInstance.hasCustomSize()) {
|
||||
selectedInstance.setCustomWidth(
|
||||
selectedInstance.getCustomWidth() +
|
||||
tileSet.tileSize * scaleX * (appendedColumns + unshiftedColumns)
|
||||
tileSet.tileSize * scaleX * (columnsToAppend + columnsToUnshift)
|
||||
);
|
||||
selectedInstance.setCustomHeight(
|
||||
selectedInstance.getCustomHeight() +
|
||||
tileSet.tileSize * scaleY * (appendedRows + unshiftedRows)
|
||||
tileSet.tileSize * scaleY * (rowsToAppend + rowsToUnshift)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.props.onInstancesResized([selectedInstance]);
|
||||
} else if (tileMapTileSelection.kind === 'erase') {
|
||||
tileMapGridCoordinates.forEach(({ x: gridX, y: gridY }) => {
|
||||
editableTileMapLayer.removeTile(gridX, gridY);
|
||||
editableTileMap.removeTile(gridX, gridY, 0);
|
||||
});
|
||||
const {
|
||||
shiftedRows,
|
||||
shiftedColumns,
|
||||
poppedRows,
|
||||
poppedColumns,
|
||||
} = editableTileMapLayer.trimEmptyColumnsAndRow();
|
||||
// The instance angle is not considered when moving the instance after
|
||||
// rows/columns were added/removed because the instance position does not
|
||||
// include the rotation transformation. Otherwise, we could have used
|
||||
// tileMapToSceneTransformation to get the new position.
|
||||
selectedInstance.setX(
|
||||
selectedInstance.getX() + shiftedColumns * (tileSet.tileSize * scaleX)
|
||||
);
|
||||
selectedInstance.setY(
|
||||
selectedInstance.getY() + shiftedRows * (tileSet.tileSize * scaleY)
|
||||
);
|
||||
if (selectedInstance.hasCustomSize()) {
|
||||
selectedInstance.setCustomWidth(
|
||||
selectedInstance.getCustomWidth() -
|
||||
tileSet.tileSize * scaleX * (poppedColumns + shiftedColumns)
|
||||
);
|
||||
selectedInstance.setCustomHeight(
|
||||
selectedInstance.getCustomHeight() -
|
||||
tileSet.tileSize * scaleY * (poppedRows + shiftedRows)
|
||||
);
|
||||
}
|
||||
shouldTrimAfterOperations = true;
|
||||
|
||||
this.props.onInstancesResized([selectedInstance]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldTrimAfterOperations) {
|
||||
const trimData = editableTileMap.trimEmptyColumnsAndRowToFitLayer(0);
|
||||
if (trimData) {
|
||||
const {
|
||||
shiftedRows,
|
||||
shiftedColumns,
|
||||
poppedRows,
|
||||
poppedColumns,
|
||||
} = trimData;
|
||||
// The instance angle is not considered when moving the instance after
|
||||
// rows/columns were added/removed because the instance position does not
|
||||
// include the rotation transformation. Otherwise, we could have used
|
||||
// tileMapToSceneTransformation to get the new position.
|
||||
selectedInstance.setX(
|
||||
selectedInstance.getX() +
|
||||
shiftedColumns * (tileSet.tileSize * scaleX)
|
||||
);
|
||||
selectedInstance.setY(
|
||||
selectedInstance.getY() + shiftedRows * (tileSet.tileSize * scaleY)
|
||||
);
|
||||
if (selectedInstance.hasCustomSize()) {
|
||||
selectedInstance.setCustomWidth(
|
||||
selectedInstance.getCustomWidth() -
|
||||
tileSet.tileSize * scaleX * (poppedColumns + shiftedColumns)
|
||||
);
|
||||
selectedInstance.setCustomHeight(
|
||||
selectedInstance.getCustomHeight() -
|
||||
tileSet.tileSize * scaleY * (poppedRows + shiftedRows)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// $FlowIgnore
|
||||
renderedInstance.updatePixiTileMap();
|
||||
selectedInstance.setRawStringProperty(
|
||||
'tilemap',
|
||||
|
@@ -40,7 +40,6 @@ const defaultShortcuts: ShortcutMap = {
|
||||
OPEN_PROPERTIES_PANEL: 'KeyP',
|
||||
TOGGLE_INSTANCES_PANEL: 'KeyI',
|
||||
TOGGLE_LAYERS_PANEL: 'KeyL',
|
||||
RENAME_SCENE_OBJECT: 'F2',
|
||||
TOGGLE_WINDOW_MASK: 'KeyM',
|
||||
TOGGLE_GRID: 'Alt+KeyG',
|
||||
OPEN_SETUP_GRID: 'CmdOrCtrl+Shift+KeyG',
|
||||
|
@@ -25,12 +25,15 @@ import { Spacer } from '../UI/Grid';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
layout: ?gdLayout,
|
||||
layout: gdLayout | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
layer: gdLayer,
|
||||
initialInstances: gdInitialInstancesContainer,
|
||||
|
||||
@@ -42,14 +45,18 @@ type Props = {|
|
||||
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|
||||
|};
|
||||
|
||||
const LayerEditorDialog = (props: Props) => {
|
||||
const {
|
||||
initialTab,
|
||||
layer,
|
||||
initialInstances,
|
||||
onClose,
|
||||
hotReloadPreviewButtonProps,
|
||||
} = props;
|
||||
const LayerEditorDialog = ({
|
||||
initialTab,
|
||||
project,
|
||||
layout,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
layer,
|
||||
initialInstances,
|
||||
onClose,
|
||||
hotReloadPreviewButtonProps,
|
||||
resourceManagementProps,
|
||||
}: Props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const {
|
||||
onCancelChanges,
|
||||
@@ -235,7 +242,7 @@ const LayerEditorDialog = (props: Props) => {
|
||||
There are {instancesCount} instances of objects on this layer.
|
||||
</Trans>
|
||||
</Text>
|
||||
{!props.project.getUseDeprecatedZeroAsDefaultZOrder() && (
|
||||
{!project.getUseDeprecatedZeroAsDefaultZOrder() && (
|
||||
<Text>
|
||||
<Trans>
|
||||
Objects created using events on this layer will be given a "Z
|
||||
@@ -415,20 +422,27 @@ const LayerEditorDialog = (props: Props) => {
|
||||
<EffectsList
|
||||
target="layer"
|
||||
layerRenderingType={layer.getRenderingType()}
|
||||
project={props.project}
|
||||
resourceManagementProps={props.resourceManagementProps}
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
effectsContainer={layer.getEffects()}
|
||||
onEffectsRenamed={(oldName, newName) => {
|
||||
if (props.layout) {
|
||||
gd.WholeProjectRefactorer.renameLayerEffect(
|
||||
props.project,
|
||||
props.layout,
|
||||
props.layer,
|
||||
if (layout) {
|
||||
gd.WholeProjectRefactorer.renameLayerEffectInScene(
|
||||
project,
|
||||
layout,
|
||||
layer,
|
||||
oldName,
|
||||
newName
|
||||
);
|
||||
} else if (eventsFunctionsExtension && eventsBasedObject) {
|
||||
gd.WholeProjectRefactorer.renameLayerEffectInEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
layer,
|
||||
oldName,
|
||||
newName
|
||||
);
|
||||
} else {
|
||||
// TODO EBO: refactoring for custom objects.
|
||||
}
|
||||
}}
|
||||
onEffectsUpdated={() => {
|
||||
|
@@ -27,6 +27,8 @@ const DropTarget = makeDropTarget('layers-list');
|
||||
type LayersListBodyProps = {|
|
||||
project: gdProject,
|
||||
layout: gdLayout | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
layersContainer: gdLayersContainer,
|
||||
selectedLayer: string,
|
||||
onSelectLayer: string => void,
|
||||
@@ -47,7 +49,21 @@ const getEffectsCount = (platform: gdPlatform, layer: gdLayer) => {
|
||||
: effectsContainer.getEffectsCount();
|
||||
};
|
||||
|
||||
const LayersListBody = (props: LayersListBodyProps) => {
|
||||
const LayersListBody = ({
|
||||
project,
|
||||
layout,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
layersContainer,
|
||||
onEditEffects,
|
||||
onEdit,
|
||||
width,
|
||||
onLayerRenamed,
|
||||
onRemoveLayer,
|
||||
unsavedChanges,
|
||||
selectedLayer,
|
||||
onSelectLayer,
|
||||
}: LayersListBodyProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const [nameErrors, setNameErrors] = React.useState<{
|
||||
@@ -55,18 +71,6 @@ const LayersListBody = (props: LayersListBodyProps) => {
|
||||
}>({});
|
||||
const draggedLayerIndexRef = React.useRef<number | null>(null);
|
||||
|
||||
const {
|
||||
project,
|
||||
layout,
|
||||
layersContainer,
|
||||
onEditEffects,
|
||||
onEdit,
|
||||
width,
|
||||
onLayerRenamed,
|
||||
onRemoveLayer,
|
||||
unsavedChanges,
|
||||
} = props;
|
||||
|
||||
const onLayerModified = React.useCallback(
|
||||
() => {
|
||||
if (unsavedChanges) unsavedChanges.triggerUnsavedChanges();
|
||||
@@ -102,8 +106,8 @@ const LayersListBody = (props: LayersListBodyProps) => {
|
||||
key={`layer-${layer.ptr}`}
|
||||
id={`layer-${i}`}
|
||||
layer={layer}
|
||||
isSelected={props.selectedLayer === layerName}
|
||||
onSelect={() => props.onSelectLayer(layerName)}
|
||||
isSelected={selectedLayer === layerName}
|
||||
onSelect={() => onSelectLayer(layerName)}
|
||||
nameError={nameErrors[layerName]}
|
||||
effectsCount={getEffectsCount(project.getCurrentPlatform(), layer)}
|
||||
onEditEffects={() => onEditEffects(layer)}
|
||||
@@ -129,14 +133,20 @@ const LayersListBody = (props: LayersListBodyProps) => {
|
||||
} else {
|
||||
layersContainer.getLayer(layerName).setName(newName);
|
||||
if (layout) {
|
||||
gd.WholeProjectRefactorer.renameLayer(
|
||||
gd.WholeProjectRefactorer.renameLayerInScene(
|
||||
project,
|
||||
layout,
|
||||
layerName,
|
||||
newName
|
||||
);
|
||||
} else {
|
||||
// TODO EBO: refactoring for custom objects.
|
||||
} else if (eventsFunctionsExtension && eventsBasedObject) {
|
||||
gd.WholeProjectRefactorer.renameLayerInEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
layerName,
|
||||
newName
|
||||
);
|
||||
}
|
||||
onLayerRenamed();
|
||||
onLayerModified();
|
||||
@@ -204,6 +214,8 @@ type Props = {|
|
||||
selectedLayer: string,
|
||||
onSelectLayer: string => void,
|
||||
layout: gdLayout | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
layersContainer: gdLayersContainer,
|
||||
onEditLayerEffects: (layer: ?gdLayer) => void,
|
||||
onEditLayer: (layer: ?gdLayer) => void,
|
||||
@@ -231,6 +243,7 @@ const hasLightingLayer = (layersContainer: gdLayersContainer) => {
|
||||
|
||||
const LayersList = React.forwardRef<Props, LayersListInterface>(
|
||||
(props, ref) => {
|
||||
const { eventsFunctionsExtension, eventsBasedObject } = props;
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
@@ -288,6 +301,8 @@ const LayersList = React.forwardRef<Props, LayersListInterface>(
|
||||
onSelectLayer={props.onSelectLayer}
|
||||
project={props.project}
|
||||
layout={props.layout}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
layersContainer={props.layersContainer}
|
||||
onEditEffects={props.onEditLayerEffects}
|
||||
onEdit={props.onEditLayer}
|
||||
|
@@ -157,6 +157,7 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
|
||||
project={project}
|
||||
projectScopedContainersAccessor={projectScopedContainersAccessor}
|
||||
layout={null}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
globalObjectsContainer={null}
|
||||
objectsContainer={eventsBasedObject.getObjects()}
|
||||
|
@@ -205,6 +205,7 @@ export class ExternalLayoutEditorContainer extends React.Component<
|
||||
project={project}
|
||||
projectScopedContainersAccessor={projectScopedContainersAccessor}
|
||||
layout={layout}
|
||||
eventsFunctionsExtension={null}
|
||||
eventsBasedObject={null}
|
||||
globalObjectsContainer={project.getObjects()}
|
||||
objectsContainer={layout.getObjects()}
|
||||
|
@@ -103,6 +103,7 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
|
||||
project={project}
|
||||
projectScopedContainersAccessor={projectScopedContainersAccessor}
|
||||
layout={layout}
|
||||
eventsFunctionsExtension={null}
|
||||
eventsBasedObject={null}
|
||||
globalObjectsContainer={project.getObjects()}
|
||||
objectsContainer={layout.getObjects()}
|
||||
|
@@ -6,6 +6,8 @@ import Snackbar from '@material-ui/core/Snackbar';
|
||||
import HomeIcon from '../UI/CustomSvgIcons/Home';
|
||||
import DebuggerIcon from '../UI/CustomSvgIcons/Debug';
|
||||
import ProjectResourcesIcon from '../UI/CustomSvgIcons/ProjectResources';
|
||||
import SceneIcon from '../UI/CustomSvgIcons/Scene';
|
||||
import EventsIcon from '../UI/CustomSvgIcons/Events';
|
||||
import ExternalEventsIcon from '../UI/CustomSvgIcons/ExternalEvents';
|
||||
import ExternalLayoutIcon from '../UI/CustomSvgIcons/ExternalLayout';
|
||||
import ExtensionIcon from '../UI/CustomSvgIcons/Extension';
|
||||
@@ -603,6 +605,10 @@ const MainFrame = (props: Props) => {
|
||||
<DebuggerIcon />
|
||||
) : kind === 'resources' ? (
|
||||
<ProjectResourcesIcon />
|
||||
) : kind === 'layout' ? (
|
||||
<SceneIcon />
|
||||
) : kind === 'layout events' ? (
|
||||
<EventsIcon />
|
||||
) : kind === 'external events' ? (
|
||||
<ExternalEventsIcon />
|
||||
) : kind === 'external layout' ? (
|
||||
|
@@ -54,6 +54,8 @@ const CustomObjectPropertiesEditor = (props: Props) => {
|
||||
objectConfiguration,
|
||||
project,
|
||||
layout,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
objectName,
|
||||
resourceManagementProps,
|
||||
@@ -262,6 +264,10 @@ const CustomObjectPropertiesEditor = (props: Props) => {
|
||||
}
|
||||
project={project}
|
||||
layout={layout}
|
||||
eventsFunctionsExtension={
|
||||
eventsFunctionsExtension
|
||||
}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
resourceManagementProps={
|
||||
resourceManagementProps
|
||||
}
|
||||
@@ -290,6 +296,8 @@ const CustomObjectPropertiesEditor = (props: Props) => {
|
||||
animations={animations}
|
||||
project={project}
|
||||
layout={layout}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
object={object}
|
||||
objectName={objectName}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
@@ -392,15 +400,26 @@ const CustomObjectPropertiesEditor = (props: Props) => {
|
||||
project={project}
|
||||
onPointsUpdated={onObjectUpdated}
|
||||
onRenamedPoint={(oldName, newName) => {
|
||||
// TODO EBO Refactor event-based object events when a point is renamed.
|
||||
if (layout && object) {
|
||||
gd.WholeProjectRefactorer.renameObjectPoint(
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
if (layout) {
|
||||
gd.WholeProjectRefactorer.renameObjectPointInScene(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
oldName,
|
||||
newName
|
||||
);
|
||||
} else if (eventsFunctionsExtension && eventsBasedObject) {
|
||||
gd.WholeProjectRefactorer.renameObjectPointInEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
oldName,
|
||||
newName
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@@ -18,6 +18,8 @@ export type EditorProps = {|
|
||||
* (for instance, an object animation or a layer name).
|
||||
*/
|
||||
layout: gdLayout | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
/**
|
||||
* The edited object. It can be undefined for sub-ObjectConfiguration of
|
||||
* custom object. There is no event to refactor in this case.
|
||||
|
@@ -102,6 +102,8 @@ const Model3DEditor = ({
|
||||
objectConfiguration,
|
||||
project,
|
||||
layout,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
onSizeUpdated,
|
||||
onObjectUpdated,
|
||||
@@ -429,15 +431,25 @@ const Model3DEditor = ({
|
||||
}
|
||||
|
||||
animation.setName(newName);
|
||||
// TODO EBO Refactor event-based object events when an animation is renamed.
|
||||
if (layout && object) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimation(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
if (object) {
|
||||
if (layout) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimationInScene(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
} else if (eventsFunctionsExtension && eventsBasedObject) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimationInEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
}
|
||||
}
|
||||
forceUpdate();
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
@@ -446,6 +458,8 @@ const Model3DEditor = ({
|
||||
model3DConfiguration,
|
||||
layout,
|
||||
object,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
forceUpdate,
|
||||
onObjectUpdated,
|
||||
nameErrors,
|
||||
|
@@ -3,7 +3,7 @@
|
||||
import * as React from 'react';
|
||||
import type { EditorProps } from './EditorProps.flow';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
@@ -13,9 +13,9 @@ import TileSetVisualizer, {
|
||||
getTileIdFromGridCoordinates,
|
||||
} from '../../InstancesEditor/TileSetVisualizer';
|
||||
import type { TileMapTileSelection } from '../../InstancesEditor/TileSetVisualizer';
|
||||
import { Column } from '../../UI/Grid';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import Checkbox from '../../UI/Checkbox';
|
||||
import Text from '../../UI/Text';
|
||||
|
||||
const SimpleTileMapEditor = ({
|
||||
objectConfiguration,
|
||||
@@ -34,9 +34,6 @@ const SimpleTileMapEditor = ({
|
||||
const columnCount = parseFloat(
|
||||
objectProperties.get('columnCount').getValue()
|
||||
);
|
||||
const [configureHitBoxes, setConfigureHitBoxes] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [error, setError] = React.useState<React.Node>(null);
|
||||
const atlasImage = objectProperties.get('atlasImage').getValue();
|
||||
const previousAtlasImageResourceName = React.useRef<string>(atlasImage);
|
||||
@@ -148,7 +145,7 @@ const SimpleTileMapEditor = ({
|
||||
if (!Number.isInteger(_rowCount) || !Number.isInteger(_columnCount)) {
|
||||
setError(
|
||||
<Trans>
|
||||
The new atlas image dimensions is not compatible with the tile size.
|
||||
The new atlas image size is not compatible with the tile size.
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
@@ -179,6 +176,7 @@ const SimpleTileMapEditor = ({
|
||||
<ScrollView>
|
||||
<ColumnStackLayout noMargin>
|
||||
{!!renderObjectNameField && renderObjectNameField()}
|
||||
{/* TODO: Should this be a Select field with all possible values given the atlas image size? */}
|
||||
<SemiControlledTextField
|
||||
floatingLabelFixed
|
||||
floatingLabelText={<Trans>Tile size</Trans>}
|
||||
@@ -194,27 +192,31 @@ const SimpleTileMapEditor = ({
|
||||
/>
|
||||
{error && <AlertMessage kind="error">{error}</AlertMessage>}
|
||||
{atlasImage && (
|
||||
<ResponsiveLineStackLayout>
|
||||
<Column noMargin expand>
|
||||
<Checkbox
|
||||
checked={configureHitBoxes}
|
||||
onCheck={(e, checked) => setConfigureHitBoxes(checked)}
|
||||
label={<Trans>Configure tiles with hit boxes</Trans>}
|
||||
/>
|
||||
</Column>
|
||||
<Column noMargin expand>
|
||||
<TileSetVisualizer
|
||||
project={project}
|
||||
objectConfiguration={objectConfiguration}
|
||||
tileMapTileSelection={tileMapTileSelection}
|
||||
onSelectTileMapTile={onChangeTilesWithHitBox}
|
||||
showPaintingToolbar={false}
|
||||
allowMultipleSelection
|
||||
onAtlasImageLoaded={onAtlasImageLoaded}
|
||||
interactive={configureHitBoxes}
|
||||
/>
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
<>
|
||||
<Line>
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>Configure tile’s hit boxes</Trans>
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>
|
||||
Click on the tilemap grid to activate or deactivate hit
|
||||
boxes.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
<TileSetVisualizer
|
||||
project={project}
|
||||
objectConfiguration={objectConfiguration}
|
||||
tileMapTileSelection={tileMapTileSelection}
|
||||
onSelectTileMapTile={onChangeTilesWithHitBox}
|
||||
showPaintingToolbar={false}
|
||||
allowMultipleSelection
|
||||
onAtlasImageLoaded={onAtlasImageLoaded}
|
||||
interactive={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
|
@@ -58,6 +58,8 @@ const SpineEditor = ({
|
||||
objectConfiguration,
|
||||
project,
|
||||
layout,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
onSizeUpdated,
|
||||
onObjectUpdated,
|
||||
@@ -283,15 +285,25 @@ const SpineEditor = ({
|
||||
}
|
||||
|
||||
animation.setName(newName);
|
||||
// TODO EBO Refactor event-based object events when an animation is renamed.
|
||||
if (layout && object) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimation(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
if (object) {
|
||||
if (layout) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimationInScene(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
} else if (eventsFunctionsExtension && eventsBasedObject) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimationInEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
}
|
||||
}
|
||||
forceUpdate();
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
@@ -300,6 +312,8 @@ const SpineEditor = ({
|
||||
spineConfiguration,
|
||||
layout,
|
||||
object,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
forceUpdate,
|
||||
onObjectUpdated,
|
||||
nameErrors,
|
||||
|
@@ -80,14 +80,17 @@ export type AnimationListInterface = {|
|
||||
|
||||
type AnimationListProps = {|
|
||||
project: gdProject,
|
||||
// TODO EBO : Layout and EventBasedObject should have a common interface to
|
||||
// browse their events. It would allow to refactor the events when an
|
||||
// animation is renamed for instance.
|
||||
/**
|
||||
* The layout is used to adapt events when an identifier is renamed
|
||||
* (for instance, an object animation or a layer name).
|
||||
*/
|
||||
layout: gdLayout | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
/**
|
||||
* The event-based object is used to adapt events when an identifier is
|
||||
* renamed (for instance, an object animation or a layer name).
|
||||
*/
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
/**
|
||||
* The edited object. It can be undefined for sub-ObjectConfiguration of
|
||||
* custom object. There is no event to refactor in this case.
|
||||
@@ -117,6 +120,8 @@ const AnimationList = React.forwardRef<
|
||||
animations,
|
||||
project,
|
||||
layout,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
objectName,
|
||||
resourceManagementProps,
|
||||
@@ -355,27 +360,39 @@ const AnimationList = React.forwardRef<
|
||||
}
|
||||
|
||||
animation.setName(newName);
|
||||
// TODO EBO Refactor event-based object events when an animation is renamed.
|
||||
if (layout && object) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimation(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
if (object) {
|
||||
if (layout) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimationInScene(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
} else if (eventsFunctionsExtension && eventsBasedObject) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimationInEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
}
|
||||
}
|
||||
forceUpdate();
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
},
|
||||
[
|
||||
forceUpdate,
|
||||
layout,
|
||||
nameErrors,
|
||||
object,
|
||||
onObjectUpdated,
|
||||
project,
|
||||
animations,
|
||||
layout,
|
||||
object,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
forceUpdate,
|
||||
onObjectUpdated,
|
||||
nameErrors,
|
||||
project,
|
||||
]
|
||||
);
|
||||
|
||||
|
@@ -41,6 +41,8 @@ export default function SpriteEditor({
|
||||
objectConfiguration,
|
||||
project,
|
||||
layout,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
objectName,
|
||||
resourceManagementProps,
|
||||
@@ -128,6 +130,8 @@ export default function SpriteEditor({
|
||||
animations={animations}
|
||||
project={project}
|
||||
layout={layout}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
object={object}
|
||||
objectName={objectName}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
@@ -267,15 +271,26 @@ export default function SpriteEditor({
|
||||
project={project}
|
||||
onPointsUpdated={onObjectUpdated}
|
||||
onRenamedPoint={(oldName, newName) => {
|
||||
// TODO EBO Refactor event-based object events when a point is renamed.
|
||||
if (layout && object) {
|
||||
gd.WholeProjectRefactorer.renameObjectPoint(
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
if (layout) {
|
||||
gd.WholeProjectRefactorer.renameObjectPointInScene(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
oldName,
|
||||
newName
|
||||
);
|
||||
} else if (eventsFunctionsExtension && eventsBasedObject) {
|
||||
gd.WholeProjectRefactorer.renameObjectPointInEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
oldName,
|
||||
newName
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@@ -47,7 +47,9 @@ type Props = {|
|
||||
|
||||
// Passed down to object editors:
|
||||
project: gdProject,
|
||||
layout?: ?gdLayout,
|
||||
layout: gdLayout | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
eventsBasedObject: gdEventsBasedObject | null,
|
||||
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
|
||||
onComputeAllVariableNames: () => Array<string>,
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
@@ -55,9 +57,6 @@ type Props = {|
|
||||
onUpdateBehaviorsSharedData: () => void,
|
||||
initialTab: ?ObjectEditorTab,
|
||||
|
||||
// Passed down to the behaviors editor:
|
||||
eventsFunctionsExtension?: gdEventsFunctionsExtension,
|
||||
|
||||
// Preview:
|
||||
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|
||||
openBehaviorEvents: (extensionName: string, behaviorName: string) => void,
|
||||
@@ -73,9 +72,25 @@ type InnerDialogProps = {|
|
||||
|
||||
const InnerDialog = (props: InnerDialogProps) => {
|
||||
const { showConfirmation } = useAlertDialog();
|
||||
const { openBehaviorEvents } = props;
|
||||
const {
|
||||
openBehaviorEvents,
|
||||
object,
|
||||
project,
|
||||
layout,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
helpPagePath,
|
||||
resourceManagementProps,
|
||||
getValidatedObjectOrGroupName,
|
||||
onCancel,
|
||||
onRename,
|
||||
initialTab,
|
||||
projectScopedContainersAccessor,
|
||||
onUpdateBehaviorsSharedData,
|
||||
onComputeAllVariableNames,
|
||||
} = props;
|
||||
const [currentTab, setCurrentTab] = React.useState<ObjectEditorTab>(
|
||||
props.initialTab || 'properties'
|
||||
initialTab || 'properties'
|
||||
);
|
||||
const [objectName, setObjectName] = React.useState(props.objectName);
|
||||
const forceUpdate = useForceUpdate();
|
||||
@@ -85,9 +100,9 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
hasUnsavedChanges,
|
||||
getOriginalContentSerializedElement,
|
||||
} = useSerializableObjectCancelableEditor({
|
||||
serializableObject: props.object,
|
||||
useProjectToUnserialize: props.project,
|
||||
onCancel: props.onCancel,
|
||||
serializableObject: object,
|
||||
useProjectToUnserialize: project,
|
||||
onCancel: onCancel,
|
||||
resetThenClearPersistentUuid: true,
|
||||
});
|
||||
|
||||
@@ -95,8 +110,8 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
// from event-based object when extensions are refreshed after an extension
|
||||
// installation.
|
||||
const objectMetadata = gd.MetadataProvider.getObjectMetadata(
|
||||
props.project.getCurrentPlatform(),
|
||||
props.object.getType()
|
||||
project.getCurrentPlatform(),
|
||||
object.getType()
|
||||
);
|
||||
|
||||
const EditorComponent: ?React.ComponentType<EditorProps> =
|
||||
@@ -110,7 +125,7 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
);
|
||||
const changeset = gd.WholeProjectRefactorer.computeChangesetForVariablesContainer(
|
||||
originalSerializedVariables,
|
||||
props.object.getVariables()
|
||||
object.getVariables()
|
||||
);
|
||||
if (changeset.hasRemovedVariables()) {
|
||||
// While we support refactoring that would remove all references (actions, conditions...)
|
||||
@@ -121,17 +136,17 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
}
|
||||
|
||||
gd.WholeProjectRefactorer.applyRefactoringForVariablesContainer(
|
||||
props.project,
|
||||
props.object.getVariables(),
|
||||
project,
|
||||
object.getVariables(),
|
||||
changeset,
|
||||
originalSerializedVariables
|
||||
);
|
||||
props.object.clearPersistentUuid();
|
||||
object.clearPersistentUuid();
|
||||
|
||||
// Do the renaming *after* applying changes, as "withSerializableObject"
|
||||
// HOC will unserialize the object to apply modifications, which will
|
||||
// override the name.
|
||||
props.onRename(objectName);
|
||||
onRename(objectName);
|
||||
};
|
||||
|
||||
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
|
||||
@@ -167,7 +182,7 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>Edit {objectName}</Trans>}
|
||||
key={props.object && props.object.ptr}
|
||||
key={object && object.ptr}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
@@ -183,7 +198,7 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<HelpButton key="help-button" helpPagePath={props.helpPagePath} />,
|
||||
<HelpButton key="help-button" helpPagePath={helpPagePath} />,
|
||||
<HotReloadPreviewButton
|
||||
key="hot-reload-preview-button"
|
||||
{...props.hotReloadPreviewButtonProps}
|
||||
@@ -237,11 +252,13 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
}
|
||||
>
|
||||
<EditorComponent
|
||||
objectConfiguration={props.object.getConfiguration()}
|
||||
project={props.project}
|
||||
layout={props.layout || null}
|
||||
object={props.object}
|
||||
resourceManagementProps={props.resourceManagementProps}
|
||||
objectConfiguration={object.getConfiguration()}
|
||||
project={project}
|
||||
layout={layout}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
object={object}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
onSizeUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positioned*/
|
||||
}
|
||||
@@ -259,9 +276,7 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
onChange={newObjectName => {
|
||||
if (newObjectName === objectName) return;
|
||||
|
||||
setObjectName(
|
||||
props.getValidatedObjectOrGroupName(newObjectName)
|
||||
);
|
||||
setObjectName(getValidatedObjectOrGroupName(newObjectName));
|
||||
notifyOfChange();
|
||||
}}
|
||||
autoFocus="desktop"
|
||||
@@ -272,33 +287,30 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
) : null}
|
||||
{currentTab === 'behaviors' && (
|
||||
<BehaviorsEditor
|
||||
object={props.object}
|
||||
project={props.project}
|
||||
eventsFunctionsExtension={props.eventsFunctionsExtension}
|
||||
resourceManagementProps={props.resourceManagementProps}
|
||||
object={object}
|
||||
project={project}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
onSizeUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positioned*/
|
||||
}
|
||||
onUpdateBehaviorsSharedData={props.onUpdateBehaviorsSharedData}
|
||||
onUpdateBehaviorsSharedData={onUpdateBehaviorsSharedData}
|
||||
onBehaviorsUpdated={notifyOfChange}
|
||||
openBehaviorEvents={askConfirmationAndOpenBehaviorEvents}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'variables' && (
|
||||
<Column expand noMargin>
|
||||
{props.object.getVariables().count() > 0 &&
|
||||
DismissableTutorialMessage && (
|
||||
<Line>
|
||||
<Column noMargin expand>
|
||||
{DismissableTutorialMessage}
|
||||
</Column>
|
||||
</Line>
|
||||
)}
|
||||
{object.getVariables().count() > 0 && DismissableTutorialMessage && (
|
||||
<Line>
|
||||
<Column noMargin expand>
|
||||
{DismissableTutorialMessage}
|
||||
</Column>
|
||||
</Line>
|
||||
)}
|
||||
<VariablesList
|
||||
projectScopedContainersAccessor={
|
||||
props.projectScopedContainersAccessor
|
||||
}
|
||||
variablesContainer={props.object.getVariables()}
|
||||
projectScopedContainersAccessor={projectScopedContainersAccessor}
|
||||
variablesContainer={object.getVariables()}
|
||||
areObjectVariables
|
||||
emptyPlaceholderTitle={
|
||||
<Trans>Add your first object variable</Trans>
|
||||
@@ -309,7 +321,7 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
</Trans>
|
||||
}
|
||||
helpPagePath={'/all-features/variables/object-variables'}
|
||||
onComputeAllVariableNames={props.onComputeAllVariableNames}
|
||||
onComputeAllVariableNames={onComputeAllVariableNames}
|
||||
onVariablesUpdated={notifyOfChange}
|
||||
/>
|
||||
</Column>
|
||||
@@ -319,16 +331,24 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
target="object"
|
||||
// TODO (3D): declare the renderer type in object metadata.
|
||||
layerRenderingType="2d"
|
||||
project={props.project}
|
||||
resourceManagementProps={props.resourceManagementProps}
|
||||
effectsContainer={props.object.getEffects()}
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
effectsContainer={object.getEffects()}
|
||||
onEffectsRenamed={(oldName, newName) => {
|
||||
// TODO EBO Refactor event-based object events when an effect is renamed.
|
||||
if (props.layout) {
|
||||
gd.WholeProjectRefactorer.renameObjectEffect(
|
||||
props.project,
|
||||
props.layout,
|
||||
props.object,
|
||||
if (layout) {
|
||||
gd.WholeProjectRefactorer.renameObjectEffectInScene(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
oldName,
|
||||
newName
|
||||
);
|
||||
} else if (eventsFunctionsExtension && eventsBasedObject) {
|
||||
gd.WholeProjectRefactorer.renameObjectEffectInEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
object,
|
||||
oldName,
|
||||
newName
|
||||
);
|
||||
|
@@ -176,7 +176,7 @@ const ObjectsEditorService = {
|
||||
objectConfiguration: gdObjectConfiguration
|
||||
): gdObjectJsImplementation =>
|
||||
gd.asObjectJsImplementation(objectConfiguration),
|
||||
helpPagePath: '/objects/tilemap', // TODO: create wiki page.
|
||||
helpPagePath: '/objects/simple_tilemap',
|
||||
},
|
||||
'TiledSpriteObject::TiledSprite': {
|
||||
component: TiledSpriteEditor,
|
||||
|
@@ -26,6 +26,7 @@ import TreeView, { type TreeViewInterface } from '../UI/TreeView';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import useAlertDialog from '../UI/Alert/useAlertDialog';
|
||||
import ErrorBoundary from '../UI/ErrorBoundary';
|
||||
import KeyboardShortcuts from '../UI/KeyboardShortcuts';
|
||||
|
||||
export const groupWithContextReactDndType = 'GD_GROUP_WITH_CONTEXT';
|
||||
|
||||
@@ -130,6 +131,16 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
|
||||
|
||||
React.useImperativeHandle(ref, () => ({ forceUpdate }));
|
||||
|
||||
// Initialize keyboard shortcuts as empty.
|
||||
// onDelete, onDuplicate and onRename callbacks are set in an effect because it applies to
|
||||
// the selected item (that is a state variable). As it is stored in a ref, the keyboard shortcut
|
||||
// instance does not update with selectedGroupWithContext changes.
|
||||
const keyboardShortcutsRef = React.useRef<KeyboardShortcuts>(
|
||||
new KeyboardShortcuts({
|
||||
shortcutCallbacks: {},
|
||||
})
|
||||
);
|
||||
|
||||
const scrollToItem = React.useCallback(
|
||||
(groupWithContext: GroupWithContext) => {
|
||||
if (treeViewRef.current) {
|
||||
@@ -271,10 +282,11 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
|
||||
'unserializeFrom'
|
||||
);
|
||||
newGroup.setName(newName); // Unserialization has overwritten the name.
|
||||
|
||||
setSelectedGroupWithContext({ group: newGroup, global });
|
||||
onEditName({ group: newGroup, global });
|
||||
onObjectGroupModified();
|
||||
},
|
||||
[globalObjectGroups, objectGroups, onObjectGroupModified]
|
||||
[globalObjectGroups, objectGroups, onObjectGroupModified, onEditName]
|
||||
);
|
||||
|
||||
const onRename = React.useCallback(
|
||||
@@ -551,6 +563,29 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
|
||||
[globalObjectGroups, objectGroups]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (keyboardShortcutsRef.current) {
|
||||
keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => {
|
||||
if (!selectedGroupWithContext) return;
|
||||
onDelete(selectedGroupWithContext);
|
||||
});
|
||||
keyboardShortcutsRef.current.setShortcutCallback(
|
||||
'onDuplicate',
|
||||
() => {
|
||||
if (!selectedGroupWithContext) return;
|
||||
onDuplicate(selectedGroupWithContext);
|
||||
}
|
||||
);
|
||||
keyboardShortcutsRef.current.setShortcutCallback('onRename', () => {
|
||||
if (!selectedGroupWithContext) return;
|
||||
onEditName(selectedGroupWithContext);
|
||||
});
|
||||
}
|
||||
},
|
||||
[selectedGroupWithContext, onDelete, onDuplicate, onEditName]
|
||||
);
|
||||
|
||||
// Force List component to be mounted again if globalObjectGroups or objectGroups
|
||||
// has been changed. Avoid accessing to invalid objects that could
|
||||
// crash the app.
|
||||
@@ -571,7 +606,12 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
<div style={styles.listContainer}>
|
||||
<div
|
||||
style={styles.listContainer}
|
||||
onKeyDown={keyboardShortcutsRef.current.onKeyDown}
|
||||
onKeyUp={keyboardShortcutsRef.current.onKeyUp}
|
||||
id="objects-groups-list"
|
||||
>
|
||||
<I18n>
|
||||
{({ i18n }) => {
|
||||
const treeViewData = getTreeViewData(i18n);
|
||||
|
@@ -27,9 +27,6 @@ import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewB
|
||||
import { getInstanceCountInLayoutForObject } from '../Utils/Layout';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||
import { getShortcutDisplayName } from '../KeyboardShortcuts';
|
||||
import defaultShortcuts from '../KeyboardShortcuts/DefaultShortcuts';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import ResponsiveRaisedButton from '../UI/ResponsiveRaisedButton';
|
||||
import Add from '../UI/CustomSvgIcons/Add';
|
||||
@@ -189,7 +186,6 @@ export type ObjectsListInterface = {|
|
||||
forceUpdateList: () => void,
|
||||
openNewObjectDialog: () => void,
|
||||
closeNewObjectDialog: () => void,
|
||||
renameObjectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext => void,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
@@ -267,10 +263,10 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const { currentlyRunningInAppTutorial } = React.useContext(
|
||||
InAppTutorialContext
|
||||
);
|
||||
const [searchText, setSearchText] = React.useState('');
|
||||
const { showDeleteConfirmation } = useAlertDialog();
|
||||
const treeViewRef = React.useRef<?TreeViewInterface<TreeViewItem>>(null);
|
||||
const forceUpdate = useForceUpdate();
|
||||
@@ -299,13 +295,17 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
closeNewObjectDialog: () => {
|
||||
setNewObjectDialogOpen(null);
|
||||
},
|
||||
renameObjectFolderOrObjectWithContext: objectFolderOrObjectWithContext => {
|
||||
if (treeViewRef.current)
|
||||
treeViewRef.current.renameItem(objectFolderOrObjectWithContext);
|
||||
},
|
||||
}));
|
||||
|
||||
const [searchText, setSearchText] = React.useState('');
|
||||
// Initialize keyboard shortcuts as empty.
|
||||
// onDelete, onDuplicate and onRename callbacks are set in an effect because it applies
|
||||
// to the selected item (that is a props). As it is stored in a ref, the keyboard shortcut
|
||||
// instance does not update with selectedObjectFolderOrObjectsWithContext changes.
|
||||
const keyboardShortcutsRef = React.useRef<KeyboardShortcuts>(
|
||||
new KeyboardShortcuts({
|
||||
shortcutCallbacks: {},
|
||||
})
|
||||
);
|
||||
|
||||
const addObject = React.useCallback(
|
||||
(objectType: string) => {
|
||||
@@ -546,31 +546,6 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
]
|
||||
);
|
||||
|
||||
// Initialize keyboard shortcuts as empty.
|
||||
// onDelete callback is set outside because it deletes the selected
|
||||
// item (that is a props). As it is stored in a ref, the keyboard shortcut
|
||||
// instance does not update with selectedObjectFolderOrObjectsWithContext changes.
|
||||
const keyboardShortcutsRef = React.useRef<KeyboardShortcuts>(
|
||||
new KeyboardShortcuts({
|
||||
shortcutCallbacks: {},
|
||||
})
|
||||
);
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (keyboardShortcutsRef.current) {
|
||||
keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => {
|
||||
deleteObjectFolderOrObjectWithContext(
|
||||
selectedObjectFolderOrObjectsWithContext[0]
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedObjectFolderOrObjectsWithContext,
|
||||
deleteObjectFolderOrObjectWithContext,
|
||||
]
|
||||
);
|
||||
|
||||
const copyObjectFolderOrObjectWithContext = React.useCallback(
|
||||
(objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext) => {
|
||||
const { objectFolderOrObject } = objectFolderOrObjectWithContext;
|
||||
@@ -721,7 +696,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
[isMobile]
|
||||
);
|
||||
|
||||
const duplicateObject = React.useCallback(
|
||||
const duplicateObjectFolderOrObjectWithContext = React.useCallback(
|
||||
(
|
||||
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
|
||||
duplicateInScene?: boolean
|
||||
@@ -765,6 +740,35 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (keyboardShortcutsRef.current) {
|
||||
keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => {
|
||||
deleteObjectFolderOrObjectWithContext(
|
||||
selectedObjectFolderOrObjectsWithContext[0]
|
||||
);
|
||||
});
|
||||
keyboardShortcutsRef.current.setShortcutCallback(
|
||||
'onDuplicate',
|
||||
() => {
|
||||
duplicateObjectFolderOrObjectWithContext(
|
||||
selectedObjectFolderOrObjectsWithContext[0]
|
||||
);
|
||||
}
|
||||
);
|
||||
keyboardShortcutsRef.current.setShortcutCallback('onRename', () => {
|
||||
editName(selectedObjectFolderOrObjectsWithContext[0]);
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedObjectFolderOrObjectsWithContext,
|
||||
deleteObjectFolderOrObjectWithContext,
|
||||
duplicateObjectFolderOrObjectWithContext,
|
||||
editName,
|
||||
]
|
||||
);
|
||||
|
||||
const rename = React.useCallback(
|
||||
(item: TreeViewItem, newName: string) => {
|
||||
if (item.isRoot || item.isPlaceholder) return;
|
||||
@@ -1356,10 +1360,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
{
|
||||
label: i18n._(t`Rename`),
|
||||
click: () => editName(item),
|
||||
accelerator: getShortcutDisplayName(
|
||||
preferences.values.userShortcutMap['RENAME_SCENE_OBJECT'] ||
|
||||
defaultShortcuts.RENAME_SCENE_OBJECT
|
||||
),
|
||||
accelerator: 'F2',
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
@@ -1445,15 +1446,13 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Duplicate`),
|
||||
click: () => duplicateObject(item),
|
||||
click: () => duplicateObjectFolderOrObjectWithContext(item),
|
||||
accelerator: 'CmdOrCtrl+D',
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Rename`),
|
||||
click: () => editName(item),
|
||||
accelerator: getShortcutDisplayName(
|
||||
preferences.values.userShortcutMap['RENAME_SCENE_OBJECT'] ||
|
||||
defaultShortcuts.RENAME_SCENE_OBJECT
|
||||
),
|
||||
accelerator: 'F2',
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
@@ -1528,7 +1527,6 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
objectsContainer,
|
||||
initialInstances,
|
||||
project,
|
||||
preferences.values.userShortcutMap,
|
||||
canSetAsGlobalObject,
|
||||
onSelectAllInstancesOfObjectInLayout,
|
||||
onExportAssets,
|
||||
@@ -1541,7 +1539,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
|
||||
selectedObjectFolderOrObjectsWithContext,
|
||||
copyObjectFolderOrObjectWithContext,
|
||||
cutObjectFolderOrObjectWithContext,
|
||||
duplicateObject,
|
||||
duplicateObjectFolderOrObjectWithContext,
|
||||
onEditObject,
|
||||
selectObjectFolderOrObjectWithContext,
|
||||
setAsGlobalObject,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user