Compare commits

..

19 Commits

Author SHA1 Message Date
Clément Pasteau
0a182346fa wip sync thweens 2024-07-25 11:54:49 +02:00
Clément Pasteau
bb6eb01153 New condition for multiplayer games to know if a player is connected (#6790) 2024-07-25 09:49:07 +02:00
github-actions[bot]
9fb086dcdf Update translations [skip ci] (#6787)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-07-24 18:48:20 +02:00
D8H
7e60a0246e Fix custom object getDrawableX/Y (#6789)
- Don't show in changelog
2024-07-24 17:48:00 +02:00
D8H
3cb2da3de5 Add autocompletion for parameter fields in events-based object events (#6770)
- Don't show in changelog
2024-07-24 17:23:20 +02:00
AlexandreS
ef23470a00 Add new object: (Integrated) TileMap (#6782)
This PR adds a new TileMap object that can be configured directly in GDevelop.

Import an atlas image, configure the tile size and start painting in the editor!
You can also configure which tiles should have a hit box and use them as platforms.
2024-07-24 17:06:13 +02:00
Clément Pasteau
196ea5e480 Fix multiplayer notifications (#6788)
Do not show in changelog
2024-07-24 15:14:53 +02:00
Clément Pasteau
e732f1952c Fix typo (#6786)
Do not show in changelog
2024-07-23 17:23:19 +02:00
github-actions[bot]
f5f024cc42 Update translations [skip ci] (#6784)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-07-23 17:16:20 +02:00
Clément Pasteau
6a3df62598 Improve lobbies connections (#6762)
* Lobby start is now more reliable, and wait for everyone to be connected to the host to start the game, rather than wait on a fixed countdown. This can speed up the start (as well as slow it down on slower connections.)
  * A timeout is still in place to start the game without the missing players if they couldn't connect

* Lobbies can now be joined after the game is started, if defined as such in the lobbies section of the game dashboard
  * new actions & conditions are available

* Slightly improve disconnection time in preview
2024-07-23 16:13:50 +02:00
D8H
75f049d911 Show the top of the 1st page when the asset store search changes (#6765) 2024-07-22 16:17:50 +02:00
D8H
4d0ac6f355 Add icons for scenes and scene events (#6785)
- Don't show in changelog
2024-07-22 15:22:36 +02:00
D8H
00a5c93b35 Automatically rename identifiers in event-based objects (#6769)
- Don't show in changelog
2024-07-22 15:14:03 +02:00
D8H
a90cc83967 Add a dialog to choose between 2D and 3D when creating a custom object (#6776) 2024-07-22 14:43:50 +02:00
Aurélien Vivet
87a5934df3 Update the AdMob extension to support Play Services v23 (#6780) 2024-07-22 13:42:09 +02:00
github-actions[bot]
d0245b8f1a Update translations [skip ci] (#6774)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-07-22 12:54:05 +02:00
D8H
45d73df6fb Fix initialization of local array variables (#6777) 2024-07-22 11:07:47 +02:00
AlexandreS
7ac600e92d Fix svg attrs (#6783)
Don't show in changelog
2024-07-22 10:31:50 +02:00
Aurélien Vivet
7ba8d0133e Declare shortcut CTRL+D to duplicate in object list (#6273)
Add duplicate shortcuts to objects list.
- Also applies the same shortcuts to the objects groups list.

---------

Co-authored-by: osmaneTKT <72160458+osmaneTKT@users.noreply.github.com>
2024-07-18 09:53:06 +02:00
188 changed files with 4503 additions and 1952 deletions

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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',

View File

@@ -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.');

View File

@@ -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',

View File

@@ -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,
};
};

View File

@@ -208,7 +208,10 @@ namespace gdjs {
};
const handleChangeVariableOwnerMessagesToSend = function () {
if (!gdjs.multiplayer.isLobbyGameRunning()) {
if (
!gdjs.multiplayer.isLobbyGameRunning() ||
!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()
) {
return;
}

View File

@@ -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);
};

View File

@@ -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;
}

View File

@@ -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();
};

View File

@@ -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

View File

@@ -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() {

View File

@@ -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;

View File

@@ -80,7 +80,7 @@ namespace gdjs {
}
getOrLoadSimpleTileMap(
tileMapAsJsObject: object,
tileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject,
objectName: string,
tileSize: number,
columnCount: number,

View File

@@ -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

View File

@@ -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

View File

@@ -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"}

View File

@@ -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

View File

@@ -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,

View File

@@ -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"}

View File

@@ -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"}

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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();
}
}
/**

View File

@@ -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();
}

View File

@@ -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";
}
}
}

View File

@@ -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>

View File

@@ -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();

View File

@@ -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;
}
}
}

View File

@@ -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.
*/

View File

@@ -521,6 +521,7 @@ namespace gdjs {
return [];
},
pushVariableCopy: () => {},
_pushVariable: () => {},
pushValue: () => {},
removeAtIndex: function () {
return;

View File

@@ -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,

View File

@@ -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

View File

@@ -252,6 +252,13 @@ class Variable {
this._childrenArray.push(variable.clone());
return this;
}
/**
* @param {Variable} variable
*/
_pushVariable(variable) {
this._childrenArray.push(variable);
}
/**
*

View File

@@ -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',

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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[];
};

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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"

View File

@@ -14,7 +14,7 @@ type Props = {|
value: string,
onChange: string => void,
disabled?: boolean,
eventsFunctionsExtension?: gdEventsFunctionsExtension,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|};
type State = {|
behaviorMetadata: Array<EnumeratedBehaviorMetadata>,

View File

@@ -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();

View File

@@ -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,

View File

@@ -269,7 +269,7 @@ const BehaviorConfigurationEditor = React.forwardRef<
type Props = {|
project: gdProject,
eventsFunctionsExtension?: gdEventsFunctionsExtension,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
object: gdObject,
onUpdateBehaviorsSharedData: () => void,
onSizeUpdated?: ?() => void,

View File

@@ -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`,

View File

@@ -731,6 +731,7 @@ export default function EventsBasedBehaviorPropertiesEditor({
{property.getType() === 'Behavior' && (
<BehaviorTypeSelector
project={project}
eventsFunctionsExtension={extension}
objectType={
behaviorObjectType || ''
}

View File

@@ -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
}

View File

@@ -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>
);
}

View File

@@ -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)}

View File

@@ -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={() => {

View File

@@ -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) => {

View File

@@ -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}

View File

@@ -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>
);
}

View File

@@ -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,
|};

View File

@@ -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,

View File

@@ -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();

View File

@@ -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)}

View File

@@ -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
}
/>
);

View File

@@ -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
}
/>
);

View File

@@ -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
}
/>
);

View File

@@ -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
}
/>
);

View File

@@ -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
}
/>
);

View File

@@ -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
}
/>
);

View File

@@ -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

View File

@@ -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>

View File

@@ -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') {

View File

@@ -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>

View File

@@ -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',

View File

@@ -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',

View File

@@ -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={() => {

View File

@@ -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}

View File

@@ -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()}

View File

@@ -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()}

View File

@@ -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()}

View File

@@ -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' ? (

View File

@@ -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
);
}
}}
/>

View File

@@ -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.

View File

@@ -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,

View File

@@ -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 tiles 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>

View File

@@ -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,

View File

@@ -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,
]
);

View File

@@ -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
);
}
}}
/>

View File

@@ -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
);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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