mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
64 Commits
experiment
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
![]() |
058b73db66 | ||
![]() |
75566c9f38 | ||
![]() |
8db8cbe3c2 | ||
![]() |
dd8c5dce2e | ||
![]() |
2810056626 | ||
![]() |
d082d4f5a4 | ||
![]() |
eaa0f4077f | ||
![]() |
7d73f58bc4 | ||
![]() |
9171b40429 | ||
![]() |
242fc49342 | ||
![]() |
7081fd570c | ||
![]() |
34ecdce7fa | ||
![]() |
3a0903a19f | ||
![]() |
e1a4f88ba6 | ||
![]() |
ed59498835 | ||
![]() |
4b70653a0b | ||
![]() |
48dd91043a | ||
![]() |
30e7ef5865 | ||
![]() |
289c555fe4 | ||
![]() |
803a55869c | ||
![]() |
282f4c184f | ||
![]() |
875237cc8b | ||
![]() |
7aeb44d05e | ||
![]() |
e445ff710e | ||
![]() |
e016babf71 | ||
![]() |
6a34e3c1db | ||
![]() |
82e819c033 | ||
![]() |
72e5deeef3 | ||
![]() |
a91cc4557f | ||
![]() |
b2c953a31d | ||
![]() |
b29fbaa1f8 | ||
![]() |
e8faa17e12 | ||
![]() |
2585ad5dca | ||
![]() |
a9108fa87b | ||
![]() |
cd3186d2d8 | ||
![]() |
6a737f7d51 | ||
![]() |
9211aa7a3b | ||
![]() |
864ba181e9 | ||
![]() |
10fa3296a9 | ||
![]() |
2fb39b9dbe | ||
![]() |
6f43e896d6 | ||
![]() |
122df05f99 | ||
![]() |
b7a5122b07 | ||
![]() |
eb9d680d95 | ||
![]() |
354da42a9e | ||
![]() |
aca37fdade | ||
![]() |
bb6eb01153 | ||
![]() |
9fb086dcdf | ||
![]() |
7e60a0246e | ||
![]() |
3cb2da3de5 | ||
![]() |
ef23470a00 | ||
![]() |
196ea5e480 | ||
![]() |
e732f1952c | ||
![]() |
f5f024cc42 | ||
![]() |
6a3df62598 | ||
![]() |
75f049d911 | ||
![]() |
4d0ac6f355 | ||
![]() |
00a5c93b35 | ||
![]() |
a90cc83967 | ||
![]() |
87a5934df3 | ||
![]() |
d0245b8f1a | ||
![]() |
45d73df6fb | ||
![]() |
7ac600e92d | ||
![]() |
7ba8d0133e |
@@ -1179,8 +1179,13 @@ gd::String EventsCodeGenerator::GenerateFreeAction(
|
||||
// Generate call
|
||||
gd::String call;
|
||||
if (instrInfos.codeExtraInformation.type == "number" ||
|
||||
instrInfos.codeExtraInformation.type == "string" ||
|
||||
instrInfos.codeExtraInformation.type == "boolean") {
|
||||
instrInfos.codeExtraInformation.type == "string" ||
|
||||
// Boolean actions declared with addExpressionAndConditionAndAction uses
|
||||
// MutatorAndOrAccessor even though they don't declare an operator parameter.
|
||||
// Boolean operators are only used with SetMutators or SetCustomCodeGenerator.
|
||||
(instrInfos.codeExtraInformation.type == "boolean" &&
|
||||
instrInfos.codeExtraInformation.accessType ==
|
||||
gd::InstructionMetadata::ExtraInformation::AccessType::Mutators)) {
|
||||
if (instrInfos.codeExtraInformation.accessType ==
|
||||
gd::InstructionMetadata::ExtraInformation::MutatorAndOrAccessor)
|
||||
call = GenerateOperatorCall(
|
||||
|
@@ -116,6 +116,13 @@ void SpriteAnimationList::ExposeResources(gd::ArbitraryResourceWorker& worker) {
|
||||
}
|
||||
}
|
||||
|
||||
bool SpriteAnimationList::HasAnimationNamed(const gd::String &name) const {
|
||||
return !name.empty() && (find_if(animations.begin(), animations.end(),
|
||||
[&name](const Animation &animation) {
|
||||
return animation.GetName() == name;
|
||||
}) != animations.end());
|
||||
}
|
||||
|
||||
const Animation& SpriteAnimationList::GetAnimation(std::size_t nb) const {
|
||||
if (nb >= animations.size()) return badAnimation;
|
||||
|
||||
|
@@ -51,6 +51,11 @@ class GD_CORE_API SpriteAnimationList {
|
||||
*/
|
||||
std::size_t GetAnimationsCount() const { return animations.size(); };
|
||||
|
||||
/**
|
||||
* \brief Return true if an animation exists for a given name.
|
||||
*/
|
||||
bool HasAnimationNamed(const gd::String &name) const;
|
||||
|
||||
/**
|
||||
* \brief Add an animation at the end of the existing ones.
|
||||
*/
|
||||
|
@@ -87,6 +87,19 @@ bool SpriteObject::UpdateInitialInstanceProperty(
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t SpriteObject::GetAnimationsCount() const {
|
||||
return animations.GetAnimationsCount();
|
||||
}
|
||||
|
||||
const gd::String &SpriteObject::GetAnimationName(size_t index) const {
|
||||
return animations.GetAnimation(index).GetName();
|
||||
}
|
||||
|
||||
bool SpriteObject::HasAnimationNamed(
|
||||
const gd::String &name) const {
|
||||
return animations.HasAnimationNamed(name);
|
||||
}
|
||||
|
||||
const SpriteAnimationList& SpriteObject::GetAnimations() const {
|
||||
return animations;
|
||||
}
|
||||
|
@@ -52,6 +52,12 @@ class GD_CORE_API SpriteObject : public gd::ObjectConfiguration {
|
||||
const gd::String& name,
|
||||
const gd::String& value) override;
|
||||
|
||||
size_t GetAnimationsCount() const override;
|
||||
|
||||
const gd::String &GetAnimationName(size_t index) const override;
|
||||
|
||||
bool HasAnimationNamed(const gd::String &animationName) const override;
|
||||
|
||||
/**
|
||||
* \brief Return the animation configuration.
|
||||
*/
|
||||
|
@@ -31,6 +31,8 @@
|
||||
|
||||
namespace gd {
|
||||
|
||||
VariablesContainer EventsVariableInstructionTypeSwitcher::nullVariablesContainer;
|
||||
|
||||
bool EventsVariableInstructionTypeSwitcher::DoVisitInstruction(gd::Instruction& instruction,
|
||||
bool isCondition) {
|
||||
const auto& metadata = isCondition
|
||||
@@ -82,7 +84,8 @@ bool EventsVariableInstructionTypeSwitcher::DoVisitInstruction(gd::Instruction&
|
||||
// Every occurrence of the variable or its children are checked.
|
||||
// Ensuring that a child is actually the one with a type change would
|
||||
// take more time.
|
||||
if (variablesContainer == &targetVariablesContainer) {
|
||||
if (variablesContainer == &targetVariablesContainer ||
|
||||
lastObjectName == groupName) {
|
||||
if (typeChangedVariableNames.find(variableName) !=
|
||||
typeChangedVariableNames.end()) {
|
||||
gd::VariableInstructionSwitcher::
|
||||
|
@@ -17,7 +17,7 @@
|
||||
namespace gd {
|
||||
class VariablesContainer;
|
||||
class Platform;
|
||||
} // namespace gd
|
||||
} // namespace gd
|
||||
|
||||
namespace gd {
|
||||
/**
|
||||
@@ -33,21 +33,36 @@ class GD_CORE_API EventsVariableInstructionTypeSwitcher
|
||||
public:
|
||||
EventsVariableInstructionTypeSwitcher(
|
||||
const gd::Platform &platform_,
|
||||
const gd::VariablesContainer &targetVariablesContainer_,
|
||||
const std::unordered_set<gd::String> &typeChangedVariableNames_)
|
||||
const std::unordered_set<gd::String> &typeChangedVariableNames_,
|
||||
const gd::VariablesContainer &targetVariablesContainer_)
|
||||
: platform(platform_),
|
||||
targetVariablesContainer(targetVariablesContainer_),
|
||||
typeChangedVariableNames(typeChangedVariableNames_){};
|
||||
typeChangedVariableNames(typeChangedVariableNames_),
|
||||
targetVariablesContainer(targetVariablesContainer_), groupName(""){};
|
||||
EventsVariableInstructionTypeSwitcher(
|
||||
const gd::Platform &platform_,
|
||||
const std::unordered_set<gd::String> &typeChangedVariableNames_,
|
||||
const gd::String &groupName_)
|
||||
: platform(platform_),
|
||||
typeChangedVariableNames(typeChangedVariableNames_),
|
||||
targetVariablesContainer(nullVariablesContainer),
|
||||
groupName(groupName_){};
|
||||
virtual ~EventsVariableInstructionTypeSwitcher();
|
||||
|
||||
private:
|
||||
private:
|
||||
bool DoVisitInstruction(gd::Instruction &instruction,
|
||||
bool isCondition) override;
|
||||
|
||||
const gd::Platform &platform;
|
||||
const gd::VariablesContainer &targetVariablesContainer;
|
||||
gd::String objectName;
|
||||
/**
|
||||
* Groups don't have VariablesContainer, so `targetVariablesContainer` will be
|
||||
* pointing to `nullVariablesContainer` and the group name is use instead to
|
||||
* check which instruction to modify.
|
||||
*/
|
||||
const gd::String groupName;
|
||||
const std::unordered_set<gd::String> &typeChangedVariableNames;
|
||||
|
||||
static VariablesContainer nullVariablesContainer;
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -32,6 +32,8 @@
|
||||
|
||||
namespace gd {
|
||||
|
||||
VariablesContainer EventsVariableReplacer::nullVariablesContainer;
|
||||
|
||||
/**
|
||||
* \brief Go through the nodes and rename variables,
|
||||
* or signal if the instruction must be renamed if a removed variable is used.
|
||||
@@ -44,22 +46,26 @@ class GD_CORE_API ExpressionVariableReplacer
|
||||
ExpressionVariableReplacer(
|
||||
const gd::Platform& platform_,
|
||||
const gd::ProjectScopedContainers& projectScopedContainers_,
|
||||
const gd::VariablesContainer& targetVariablesContainer_,
|
||||
const VariablesRenamingChangesetNode& variablesRenamingChangesetRoot_,
|
||||
const std::unordered_set<gd::String>& removedVariableNames_)
|
||||
const std::unordered_set<gd::String>& removedVariableNames_,
|
||||
const gd::VariablesContainer& targetVariablesContainer_,
|
||||
const gd::String &groupName_,
|
||||
const gd::String &forcedInitialObjectName)
|
||||
: hasDoneRenaming(false),
|
||||
removedVariableUsed(false),
|
||||
platform(platform_),
|
||||
projectScopedContainers(projectScopedContainers_),
|
||||
forcedInitialVariablesContainer(nullptr),
|
||||
targetVariablesContainer(targetVariablesContainer_),
|
||||
forcedVariablesContainer(nullptr),
|
||||
forcedObjectName(forcedInitialObjectName),
|
||||
variablesRenamingChangesetRoot(variablesRenamingChangesetRoot_),
|
||||
removedVariableNames(removedVariableNames_){};
|
||||
removedVariableNames(removedVariableNames_),
|
||||
targetVariablesContainer(targetVariablesContainer_),
|
||||
targetGroupName(groupName_){};
|
||||
virtual ~ExpressionVariableReplacer(){};
|
||||
|
||||
void SetForcedInitialVariablesContainer(
|
||||
const gd::VariablesContainer* forcedInitialVariablesContainer_) {
|
||||
forcedInitialVariablesContainer = forcedInitialVariablesContainer_;
|
||||
forcedVariablesContainer = forcedInitialVariablesContainer_;
|
||||
}
|
||||
|
||||
bool HasDoneRenaming() const { return hasDoneRenaming; }
|
||||
@@ -82,12 +88,13 @@ class GD_CORE_API ExpressionVariableReplacer
|
||||
// The node represents a variable or an object name on which a variable
|
||||
// will be accessed.
|
||||
|
||||
if (forcedInitialVariablesContainer) {
|
||||
if (forcedVariablesContainer) {
|
||||
const gd::String oldVariableName = node.name;
|
||||
PushVariablesRenamingChangesetRoot();
|
||||
// A scope was forced. Honor it: it means this node represents a variable
|
||||
// of the forced variables container.
|
||||
if (forcedInitialVariablesContainer == &targetVariablesContainer) {
|
||||
if (forcedVariablesContainer == &targetVariablesContainer ||
|
||||
IsTargetingObjectGroup(forcedObjectName)) {
|
||||
RenameOrRemoveVariableOfTargetVariableContainer(node.name);
|
||||
}
|
||||
|
||||
@@ -150,7 +157,8 @@ class GD_CORE_API ExpressionVariableReplacer
|
||||
// This is always true because MatchIdentifierWithName is used to get
|
||||
// objectNameToUseForVariableAccessor.
|
||||
if (objectsContainersList.HasObjectOrGroupVariablesContainer(
|
||||
objectNameToUseForVariableAccessor, targetVariablesContainer)) {
|
||||
objectNameToUseForVariableAccessor, targetVariablesContainer) ||
|
||||
IsTargetingObjectGroup(objectNameToUseForVariableAccessor)) {
|
||||
objectNameToUseForVariableAccessor = "";
|
||||
// The node represents an object variable, and this object variables are
|
||||
// the target. Do the replacement or removals:
|
||||
@@ -197,10 +205,11 @@ class GD_CORE_API ExpressionVariableReplacer
|
||||
// (and if it's a variable reference or a value does not have any importance
|
||||
// here).
|
||||
|
||||
if (forcedInitialVariablesContainer) {
|
||||
if (forcedVariablesContainer) {
|
||||
// A scope was forced. Honor it: it means this node represents a variable
|
||||
// of the forced variables container.
|
||||
if (forcedInitialVariablesContainer == &targetVariablesContainer) {
|
||||
if (forcedVariablesContainer == &targetVariablesContainer ||
|
||||
IsTargetingObjectGroup(forcedObjectName)) {
|
||||
renameVariableAndChild();
|
||||
}
|
||||
return;
|
||||
@@ -213,7 +222,8 @@ class GD_CORE_API ExpressionVariableReplacer
|
||||
[&]() {
|
||||
// This represents an object.
|
||||
if (objectsContainersList.HasObjectOrGroupVariablesContainer(
|
||||
node.identifierName, targetVariablesContainer)) {
|
||||
node.identifierName, targetVariablesContainer) ||
|
||||
IsTargetingObjectGroup(node.identifierName)) {
|
||||
// The node represents an object variable, and this object variables
|
||||
// are the target. Do the replacement or removals:
|
||||
PushVariablesRenamingChangesetRoot();
|
||||
@@ -261,31 +271,33 @@ class GD_CORE_API ExpressionVariableReplacer
|
||||
// force the "scope" at which starts the evalution of variables.
|
||||
if (parameterMetadata && parameterMetadata->GetValueTypeMetadata()
|
||||
.IsLegacyPreScopedVariable()) {
|
||||
const gd::VariablesContainer* oldForcedInitialVariablesContainer =
|
||||
forcedInitialVariablesContainer;
|
||||
const gd::VariablesContainer *oldForcedVariablesContainer =
|
||||
forcedVariablesContainer;
|
||||
const gd::String &oldForcedObjectName = forcedObjectName;
|
||||
|
||||
forcedInitialVariablesContainer = nullptr;
|
||||
forcedVariablesContainer = nullptr;
|
||||
forcedObjectName = "";
|
||||
if (parameterMetadata->GetType() == "globalvar") {
|
||||
forcedInitialVariablesContainer =
|
||||
forcedVariablesContainer =
|
||||
projectScopedContainers.GetVariablesContainersList()
|
||||
.GetTopMostVariablesContainer();
|
||||
} else if (parameterMetadata->GetType() == "scenevar") {
|
||||
forcedInitialVariablesContainer =
|
||||
forcedVariablesContainer =
|
||||
projectScopedContainers.GetVariablesContainersList()
|
||||
.GetBottomMostVariablesContainer();
|
||||
} else if (parameterMetadata->GetType() == "objectvar") {
|
||||
auto objectName = gd::ExpressionVariableOwnerFinder::GetObjectName(
|
||||
platform,
|
||||
projectScopedContainers.GetObjectsContainersList(),
|
||||
node.objectName,
|
||||
*node.parameters[parameterIndex].get());
|
||||
forcedInitialVariablesContainer =
|
||||
platform, projectScopedContainers.GetObjectsContainersList(),
|
||||
node.objectName, *node.parameters[parameterIndex].get());
|
||||
forcedVariablesContainer =
|
||||
projectScopedContainers.GetObjectsContainersList()
|
||||
.GetObjectOrGroupVariablesContainer(objectName);
|
||||
forcedObjectName = objectName;
|
||||
}
|
||||
|
||||
node.parameters[parameterIndex]->Visit(*this);
|
||||
forcedInitialVariablesContainer = oldForcedInitialVariablesContainer;
|
||||
forcedVariablesContainer = oldForcedVariablesContainer;
|
||||
forcedObjectName = oldForcedObjectName;
|
||||
} else {
|
||||
// For any other parameter, there is no special treatment being needed.
|
||||
node.parameters[parameterIndex]->Visit(*this);
|
||||
@@ -298,6 +310,10 @@ class GD_CORE_API ExpressionVariableReplacer
|
||||
bool hasDoneRenaming;
|
||||
bool removedVariableUsed;
|
||||
|
||||
bool IsTargetingObjectGroup(const gd::String &objectGroupName) {
|
||||
return !targetGroupName.empty() && objectGroupName == targetGroupName;
|
||||
}
|
||||
|
||||
bool RenameOrRemoveVariableOfTargetVariableContainer(
|
||||
gd::String& variableName) {
|
||||
const auto *currentVariablesRenamingChangesetNode =
|
||||
@@ -382,10 +398,17 @@ class GD_CORE_API ExpressionVariableReplacer
|
||||
// Scope:
|
||||
const gd::Platform& platform;
|
||||
const gd::ProjectScopedContainers& projectScopedContainers;
|
||||
const gd::VariablesContainer* forcedInitialVariablesContainer;
|
||||
const gd::VariablesContainer* forcedVariablesContainer;
|
||||
gd::String forcedObjectName;
|
||||
|
||||
// Renaming or removing to do:
|
||||
const gd::VariablesContainer& targetVariablesContainer;
|
||||
/**
|
||||
* Groups don't have VariablesContainer, so `targetVariablesContainer` will be
|
||||
* pointing to `nullVariablesContainer` and the group name is use instead to
|
||||
* check which variable accesses to modify in expressions.
|
||||
*/
|
||||
const gd::String& targetGroupName;
|
||||
const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot;
|
||||
const std::unordered_set<gd::String>& removedVariableNames;
|
||||
|
||||
@@ -397,7 +420,7 @@ const gd::VariablesContainer*
|
||||
EventsVariableReplacer::FindForcedVariablesContainerIfAny(
|
||||
const gd::String& type, const gd::String& lastObjectName) {
|
||||
// Handle legacy pre-scoped variable parameters: in this case, we
|
||||
// force the "scope" at which starts the evalution of variables.
|
||||
// force the "scope" at which starts the evaluation of variables.
|
||||
if (type == "objectvar") {
|
||||
return GetProjectScopedContainers()
|
||||
.GetObjectsContainersList()
|
||||
@@ -442,9 +465,11 @@ bool EventsVariableReplacer::DoVisitInstruction(gd::Instruction& instruction,
|
||||
if (node) {
|
||||
ExpressionVariableReplacer renamer(platform,
|
||||
GetProjectScopedContainers(),
|
||||
targetVariablesContainer,
|
||||
variablesRenamingChangesetRoot,
|
||||
removedVariableNames);
|
||||
removedVariableNames,
|
||||
targetVariablesContainer,
|
||||
targetGroupName,
|
||||
type == "objectvar" ? lastObjectName : "");
|
||||
renamer.SetForcedInitialVariablesContainer(
|
||||
FindForcedVariablesContainerIfAny(type, lastObjectName));
|
||||
node->Visit(renamer);
|
||||
@@ -474,9 +499,11 @@ bool EventsVariableReplacer::DoVisitEventExpression(
|
||||
if (node) {
|
||||
ExpressionVariableReplacer renamer(platform,
|
||||
GetProjectScopedContainers(),
|
||||
targetVariablesContainer,
|
||||
variablesRenamingChangesetRoot,
|
||||
removedVariableNames);
|
||||
removedVariableNames,
|
||||
targetVariablesContainer,
|
||||
targetGroupName,
|
||||
"");
|
||||
renamer.SetForcedInitialVariablesContainer(
|
||||
FindForcedVariablesContainerIfAny(type, ""));
|
||||
node->Visit(renamer);
|
||||
|
@@ -35,13 +35,24 @@ class GD_CORE_API EventsVariableReplacer
|
||||
public:
|
||||
EventsVariableReplacer(
|
||||
const gd::Platform &platform_,
|
||||
const gd::VariablesContainer &targetVariablesContainer_,
|
||||
const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot_,
|
||||
const std::unordered_set<gd::String> &removedVariableNames_)
|
||||
const std::unordered_set<gd::String> &removedVariableNames_,
|
||||
const gd::VariablesContainer &targetVariablesContainer_)
|
||||
: platform(platform_),
|
||||
targetVariablesContainer(targetVariablesContainer_),
|
||||
variablesRenamingChangesetRoot(variablesRenamingChangesetRoot_),
|
||||
removedVariableNames(removedVariableNames_) {};
|
||||
removedVariableNames(removedVariableNames_),
|
||||
targetVariablesContainer(targetVariablesContainer_),
|
||||
targetGroupName("") {};
|
||||
EventsVariableReplacer(
|
||||
const gd::Platform &platform_,
|
||||
const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot_,
|
||||
const std::unordered_set<gd::String> &removedVariableNames_,
|
||||
const gd::String &targetGroupName_)
|
||||
: platform(platform_),
|
||||
variablesRenamingChangesetRoot(variablesRenamingChangesetRoot_),
|
||||
removedVariableNames(removedVariableNames_),
|
||||
targetVariablesContainer(nullVariablesContainer),
|
||||
targetGroupName(targetGroupName_) {};
|
||||
virtual ~EventsVariableReplacer();
|
||||
|
||||
private:
|
||||
@@ -55,9 +66,16 @@ class GD_CORE_API EventsVariableReplacer
|
||||
|
||||
const gd::Platform &platform;
|
||||
const gd::VariablesContainer &targetVariablesContainer;
|
||||
gd::String objectName;
|
||||
/**
|
||||
* Groups don't have VariablesContainer, so `targetVariablesContainer` will be
|
||||
* pointing to `nullVariablesContainer` and the group name is use instead to
|
||||
* check which variable accesses to modify in expressions.
|
||||
*/
|
||||
const gd::String targetGroupName;
|
||||
const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot;
|
||||
const std::unordered_set<gd::String> &removedVariableNames;
|
||||
|
||||
static VariablesContainer nullVariablesContainer;
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
196
Core/GDCore/IDE/GroupVariableHelper.cpp
Normal file
196
Core/GDCore/IDE/GroupVariableHelper.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#include "GroupVariableHelper.h"
|
||||
|
||||
#include "GDCore/IDE/WholeProjectRefactorer.h"
|
||||
#include "GDCore/Project/Object.h"
|
||||
#include "GDCore/Project/ObjectGroup.h"
|
||||
#include "GDCore/Project/ObjectsContainer.h"
|
||||
#include "GDCore/Project/ObjectsContainersList.h"
|
||||
#include "GDCore/Project/Variable.h"
|
||||
#include "GDCore/Project/VariablesContainer.h"
|
||||
#include "GDCore/String.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
void GroupVariableHelper::FillAnyVariableBetweenObjects(
|
||||
gd::ObjectsContainer &globalObjectsContainer,
|
||||
gd::ObjectsContainer &objectsContainer,
|
||||
const gd::ObjectGroup &objectGroup) {
|
||||
const auto &objectNames = objectGroup.GetAllObjectsNames();
|
||||
for (const gd::String &sourceObjectName : objectNames) {
|
||||
const bool hasObject = objectsContainer.HasObjectNamed(sourceObjectName);
|
||||
if (!hasObject &&
|
||||
!globalObjectsContainer.HasObjectNamed(sourceObjectName)) {
|
||||
continue;
|
||||
}
|
||||
const auto &sourceObject =
|
||||
hasObject ? objectsContainer.GetObject(sourceObjectName)
|
||||
: globalObjectsContainer.GetObject(sourceObjectName);
|
||||
const auto &sourceVariablesContainer = sourceObject.GetVariables();
|
||||
|
||||
for (const gd::String &destinationObjectName : objectNames) {
|
||||
if (sourceObjectName == destinationObjectName) {
|
||||
continue;
|
||||
}
|
||||
const bool hasObject =
|
||||
objectsContainer.HasObjectNamed(destinationObjectName);
|
||||
if (!hasObject &&
|
||||
!globalObjectsContainer.HasObjectNamed(destinationObjectName)) {
|
||||
continue;
|
||||
}
|
||||
auto &destinationObject =
|
||||
hasObject ? objectsContainer.GetObject(destinationObjectName)
|
||||
: globalObjectsContainer.GetObject(destinationObjectName);
|
||||
auto &destinationVariablesContainer = destinationObject.GetVariables();
|
||||
|
||||
for (std::size_t sourceVariableIndex = 0;
|
||||
sourceVariableIndex < sourceVariablesContainer.Count();
|
||||
++sourceVariableIndex) {
|
||||
auto &sourceVariable =
|
||||
sourceVariablesContainer.Get(sourceVariableIndex);
|
||||
const auto &variableName =
|
||||
sourceVariablesContainer.GetNameAt(sourceVariableIndex);
|
||||
|
||||
if (!destinationVariablesContainer.Has(variableName)) {
|
||||
destinationVariablesContainer.Insert(
|
||||
variableName, sourceVariable,
|
||||
destinationVariablesContainer.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gd::VariablesContainer GroupVariableHelper::MergeVariableContainers(
|
||||
const gd::ObjectsContainersList &objectsContainersList,
|
||||
const gd::ObjectGroup &objectGroup) {
|
||||
gd::VariablesContainer mergedVariablesContainer;
|
||||
|
||||
const auto &objectNames = objectGroup.GetAllObjectsNames();
|
||||
std::size_t objectIndex = 0;
|
||||
bool isFirstObjectFound = false;
|
||||
for (; objectIndex < objectNames.size() && !isFirstObjectFound;
|
||||
objectIndex++) {
|
||||
const gd::String &objectName = objectNames[objectIndex];
|
||||
if (!objectsContainersList.HasObjectOrGroupNamed(objectName)) {
|
||||
continue;
|
||||
}
|
||||
isFirstObjectFound = true;
|
||||
mergedVariablesContainer =
|
||||
*objectsContainersList.GetObjectOrGroupVariablesContainer(objectName);
|
||||
}
|
||||
for (; objectIndex < objectNames.size(); objectIndex++) {
|
||||
const gd::String &objectName = objectNames[objectIndex];
|
||||
if (!objectsContainersList.HasObjectOrGroupNamed(objectName)) {
|
||||
continue;
|
||||
}
|
||||
const auto &variablesContainer =
|
||||
*objectsContainersList.GetObjectOrGroupVariablesContainer(objectName);
|
||||
|
||||
for (std::size_t variableIndex = 0;
|
||||
variableIndex < mergedVariablesContainer.Count(); ++variableIndex) {
|
||||
auto &mergedVariable = mergedVariablesContainer.Get(variableIndex);
|
||||
const auto &variableName =
|
||||
mergedVariablesContainer.GetNameAt(variableIndex);
|
||||
|
||||
if (variablesContainer.Has(variableName)) {
|
||||
auto &variable = variablesContainer.Get(variableName);
|
||||
if (mergedVariable.GetType() != variable.GetType()) {
|
||||
mergedVariable.CastTo(gd::Variable::Type::MixedTypes);
|
||||
} else if (mergedVariable != variable) {
|
||||
mergedVariable.MarkAsMixedValues();
|
||||
}
|
||||
} else {
|
||||
mergedVariablesContainer.Remove(variableName);
|
||||
variableIndex--;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mergedVariablesContainer;
|
||||
}
|
||||
|
||||
void GroupVariableHelper::FillMissingGroupVariablesToObjects(
|
||||
gd::ObjectsContainer &globalObjectsContainer,
|
||||
gd::ObjectsContainer &objectsContainer, const gd::ObjectGroup &objectGroup,
|
||||
const gd::SerializerElement &originalSerializedVariables) {
|
||||
gd::VariablesContainer groupVariablesContainer;
|
||||
groupVariablesContainer.UnserializeFrom(originalSerializedVariables);
|
||||
// Add missing variables to objects added in the group.
|
||||
for (const gd::String &objectName : objectGroup.GetAllObjectsNames()) {
|
||||
const bool hasObject = objectsContainer.HasObjectNamed(objectName);
|
||||
if (!hasObject && !globalObjectsContainer.HasObjectNamed(objectName)) {
|
||||
continue;
|
||||
}
|
||||
auto &object = hasObject ? objectsContainer.GetObject(objectName)
|
||||
: globalObjectsContainer.GetObject(objectName);
|
||||
auto &variablesContainer = object.GetVariables();
|
||||
for (std::size_t variableIndex = 0;
|
||||
variableIndex < groupVariablesContainer.Count(); ++variableIndex) {
|
||||
auto &groupVariable = groupVariablesContainer.Get(variableIndex);
|
||||
const auto &variableName =
|
||||
groupVariablesContainer.GetNameAt(variableIndex);
|
||||
|
||||
if (!variablesContainer.Has(variableName)) {
|
||||
variablesContainer.Insert(variableName, groupVariable,
|
||||
variablesContainer.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO Handle position changes for group variables.
|
||||
// We could try to change the order of object variables in a way that the next
|
||||
// call to MergeVariableContainers rebuild them in the same order.
|
||||
void GroupVariableHelper::ApplyChangesToObjects(
|
||||
gd::ObjectsContainer &globalObjectsContainer,
|
||||
gd::ObjectsContainer &objectsContainer,
|
||||
const gd::VariablesContainer &groupVariablesContainer,
|
||||
const gd::ObjectGroup &objectGroup,
|
||||
const gd::VariablesChangeset &changeset) {
|
||||
for (const gd::String &objectName : objectGroup.GetAllObjectsNames()) {
|
||||
const bool hasObject = objectsContainer.HasObjectNamed(objectName);
|
||||
if (!hasObject && !globalObjectsContainer.HasObjectNamed(objectName)) {
|
||||
continue;
|
||||
}
|
||||
auto &object = hasObject ? objectsContainer.GetObject(objectName)
|
||||
: globalObjectsContainer.GetObject(objectName);
|
||||
auto &variablesContainer = object.GetVariables();
|
||||
for (const gd::String &variableName : changeset.removedVariableNames) {
|
||||
variablesContainer.Remove(variableName);
|
||||
}
|
||||
for (const gd::String &variableName : changeset.addedVariableNames) {
|
||||
if (variablesContainer.Has(variableName)) {
|
||||
// It can happens if an object already had the variable but it was not
|
||||
// shared by other object of the group.
|
||||
continue;
|
||||
}
|
||||
variablesContainer.Insert(variableName,
|
||||
groupVariablesContainer.Get(variableName),
|
||||
variablesContainer.Count());
|
||||
}
|
||||
for (const auto &pair : changeset.oldToNewVariableNames) {
|
||||
const gd::String &oldVariableName = pair.first;
|
||||
const gd::String &newVariableName = pair.second;
|
||||
if (variablesContainer.Has(newVariableName)) {
|
||||
// It can happens if an object already had the variable but it was not
|
||||
// shared by other object of the group.
|
||||
variablesContainer.Remove(oldVariableName);
|
||||
} else {
|
||||
variablesContainer.Rename(oldVariableName, newVariableName);
|
||||
}
|
||||
}
|
||||
// Apply type and value changes
|
||||
for (const gd::String &variableName : changeset.valueChangedVariableNames) {
|
||||
size_t index = variablesContainer.GetPosition(variableName);
|
||||
variablesContainer.Remove(variableName);
|
||||
variablesContainer.Insert(
|
||||
variableName, groupVariablesContainer.Get(variableName), index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gd
|
75
Core/GDCore/IDE/GroupVariableHelper.h
Normal file
75
Core/GDCore/IDE/GroupVariableHelper.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "GDCore/Project/VariablesContainer.h"
|
||||
|
||||
namespace gd {
|
||||
class ObjectsContainersList;
|
||||
class ObjectsContainer;
|
||||
class ObjectGroup;
|
||||
class VariablesContainer;
|
||||
struct VariablesChangeset;
|
||||
} // namespace gd
|
||||
|
||||
namespace gd {
|
||||
|
||||
/**
|
||||
* Help handling variables of group objects as a whole.
|
||||
*
|
||||
* This is used by the object group variable editor.
|
||||
*/
|
||||
class GD_CORE_API GroupVariableHelper {
|
||||
public:
|
||||
/**
|
||||
* Copy every variable from every object of the group to the other objects
|
||||
* if they don't have it already.
|
||||
*
|
||||
* In the editor, when an object group is created, users can choose between:
|
||||
* - doing no change and only see variables that are already shared by any
|
||||
* objects of the group
|
||||
* - applying this function and see every variable
|
||||
*/
|
||||
static void
|
||||
FillAnyVariableBetweenObjects(gd::ObjectsContainer &globalObjectsContainer,
|
||||
gd::ObjectsContainer &objectsContainer,
|
||||
const gd::ObjectGroup &objectGroup);
|
||||
|
||||
/**
|
||||
* Build a variable container with the intersection of variables from the
|
||||
* every objects of the given group.
|
||||
*/
|
||||
static gd::VariablesContainer MergeVariableContainers(
|
||||
const gd::ObjectsContainersList &objectsContainersList,
|
||||
const gd::ObjectGroup &objectGroup);
|
||||
|
||||
/**
|
||||
* @brief Copy the variables of the group to all objects.
|
||||
*
|
||||
* Objects can be added during the group edition and may not necessarily have
|
||||
* all the variables initially shared by the group.
|
||||
*
|
||||
* \see gd::GroupVariableHelper::MergeVariableContainers
|
||||
*/
|
||||
static void FillMissingGroupVariablesToObjects(
|
||||
gd::ObjectsContainer &globalObjectsContainer,
|
||||
gd::ObjectsContainer &objectsContainer,
|
||||
const gd::ObjectGroup &objectGroup,
|
||||
const gd::SerializerElement &originalSerializedVariables);
|
||||
|
||||
/**
|
||||
* @brief Apply the changes done with the variables editor to the objects of
|
||||
* the group.
|
||||
*/
|
||||
static void
|
||||
ApplyChangesToObjects(gd::ObjectsContainer &globalObjectsContainers,
|
||||
gd::ObjectsContainer &objectsContainers,
|
||||
const gd::VariablesContainer &groupVariablesContainer,
|
||||
const gd::ObjectGroup &objectGroup,
|
||||
const gd::VariablesChangeset &changeset);
|
||||
};
|
||||
|
||||
} // namespace gd
|
@@ -11,22 +11,23 @@
|
||||
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/IDE/DependenciesAnalyzer.h"
|
||||
#include "GDCore/IDE/GroupVariableHelper.h"
|
||||
#include "GDCore/IDE/EventBasedBehaviorBrowser.h"
|
||||
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
|
||||
#include "GDCore/IDE/Events/BehaviorParametersFiller.h"
|
||||
#include "GDCore/IDE/Events/BehaviorTypeRenamer.h"
|
||||
#include "GDCore/IDE/Events/CustomObjectTypeRenamer.h"
|
||||
#include "GDCore/IDE/Events/EventsBehaviorRenamer.h"
|
||||
#include "GDCore/IDE/Events/EventsPropertyReplacer.h"
|
||||
#include "GDCore/IDE/Events/EventsRefactorer.h"
|
||||
#include "GDCore/IDE/Events/EventsVariableReplacer.h"
|
||||
#include "GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.h"
|
||||
#include "GDCore/IDE/Events/EventsVariableReplacer.h"
|
||||
#include "GDCore/IDE/Events/ExpressionsParameterMover.h"
|
||||
#include "GDCore/IDE/Events/ExpressionsRenamer.h"
|
||||
#include "GDCore/IDE/Events/InstructionsParameterMover.h"
|
||||
#include "GDCore/IDE/Events/InstructionsTypeRenamer.h"
|
||||
#include "GDCore/IDE/Events/LinkEventTargetRenamer.h"
|
||||
#include "GDCore/IDE/Events/ProjectElementRenamer.h"
|
||||
#include "GDCore/IDE/Events/BehaviorParametersFiller.h"
|
||||
#include "GDCore/IDE/EventsFunctionTools.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryBehaviorSharedDataWorker.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryEventBasedBehaviorsWorker.h"
|
||||
@@ -173,6 +174,7 @@ WholeProjectRefactorer::ComputeChangesetForVariablesContainer(
|
||||
removedUuidAndNames.find(variable.GetPersistentUuid());
|
||||
if (existingOldVariableUuidAndName == removedUuidAndNames.end()) {
|
||||
// This is a new variable.
|
||||
changeset.addedVariableNames.insert(variableName);
|
||||
} else {
|
||||
const gd::String &oldName = existingOldVariableUuidAndName->second;
|
||||
|
||||
@@ -182,9 +184,15 @@ WholeProjectRefactorer::ComputeChangesetForVariablesContainer(
|
||||
}
|
||||
|
||||
const auto &oldVariable = oldVariablesContainer.Get(oldName);
|
||||
if (gd::WholeProjectRefactorer::HasAnyVariableTypeChanged(oldVariable, variable)) {
|
||||
if (gd::WholeProjectRefactorer::HasAnyVariableTypeChanged(oldVariable,
|
||||
variable)) {
|
||||
changeset.typeChangedVariableNames.insert(variableName);
|
||||
}
|
||||
if (oldVariable != variable
|
||||
// Mixed values are never equals, but they must not override anything.
|
||||
&& !variable.HasMixedValues()) {
|
||||
changeset.valueChangedVariableNames.insert(variableName);
|
||||
}
|
||||
|
||||
const auto &variablesRenamingChangesetNode =
|
||||
gd::WholeProjectRefactorer::ComputeChangesetForVariable(oldVariable,
|
||||
@@ -310,8 +318,8 @@ void WholeProjectRefactorer::ApplyRefactoringForVariablesContainer(
|
||||
|
||||
// Rename and remove variables
|
||||
gd::EventsVariableReplacer eventsVariableReplacer(
|
||||
project.GetCurrentPlatform(), variablesContainer,
|
||||
changeset, changeset.removedVariableNames);
|
||||
project.GetCurrentPlatform(), changeset, changeset.removedVariableNames,
|
||||
variablesContainer);
|
||||
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
|
||||
eventsVariableReplacer);
|
||||
|
||||
@@ -321,8 +329,83 @@ void WholeProjectRefactorer::ApplyRefactoringForVariablesContainer(
|
||||
// Switch types of instructions
|
||||
gd::EventsVariableInstructionTypeSwitcher
|
||||
eventsVariableInstructionTypeSwitcher(project.GetCurrentPlatform(),
|
||||
variablesContainer,
|
||||
changeset.typeChangedVariableNames);
|
||||
changeset.typeChangedVariableNames,
|
||||
variablesContainer);
|
||||
gd::ProjectBrowserHelper::ExposeProjectEvents(
|
||||
project, eventsVariableInstructionTypeSwitcher);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::ApplyRefactoringForGroupVariablesContainer(
|
||||
gd::Project &project, gd::ObjectsContainer &globalObjectsContainer,
|
||||
gd::ObjectsContainer &objectsContainer,
|
||||
const gd::VariablesContainer &groupVariablesContainer,
|
||||
const gd::ObjectGroup &objectGroup,
|
||||
const gd::VariablesChangeset &changeset,
|
||||
const gd::SerializerElement &originalSerializedVariables) {
|
||||
|
||||
// While we support refactoring that would remove all references (actions, conditions...)
|
||||
// it's both a bit dangerous for the user and we would need to show the user what
|
||||
// will be removed before doing so. For now, just clear the removed variables so they don't
|
||||
// trigger any refactoring.
|
||||
std::unordered_set<gd::String> removedVariableNames;
|
||||
|
||||
// Rename variables in events for the objects of the group.
|
||||
for (const gd::String &objectName : objectGroup.GetAllObjectsNames()) {
|
||||
const bool hasObject = objectsContainer.HasObjectNamed(objectName);
|
||||
if (!hasObject && !globalObjectsContainer.HasObjectNamed(objectName)) {
|
||||
continue;
|
||||
}
|
||||
auto &object = hasObject ? objectsContainer.GetObject(objectName)
|
||||
: globalObjectsContainer.GetObject(objectName);
|
||||
auto &variablesContainer = object.GetVariables();
|
||||
|
||||
gd::EventsVariableReplacer eventsVariableReplacer(
|
||||
project.GetCurrentPlatform(), changeset,
|
||||
removedVariableNames, variablesContainer);
|
||||
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
|
||||
eventsVariableReplacer);
|
||||
}
|
||||
|
||||
// Rename variables in events for the group.
|
||||
gd::EventsVariableReplacer eventsVariableReplacer(
|
||||
project.GetCurrentPlatform(), changeset, removedVariableNames,
|
||||
objectGroup.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
|
||||
eventsVariableReplacer);
|
||||
|
||||
// Apply changes to objects.
|
||||
gd::GroupVariableHelper::FillMissingGroupVariablesToObjects(
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
objectGroup,
|
||||
originalSerializedVariables);
|
||||
gd::GroupVariableHelper::ApplyChangesToObjects(
|
||||
globalObjectsContainer, objectsContainer, groupVariablesContainer,
|
||||
objectGroup, changeset);
|
||||
|
||||
// Switch types of instructions for the group objects.
|
||||
for (const gd::String &objectName : objectGroup.GetAllObjectsNames()) {
|
||||
const bool hasObject = objectsContainer.HasObjectNamed(objectName);
|
||||
if (!hasObject && !globalObjectsContainer.HasObjectNamed(objectName)) {
|
||||
continue;
|
||||
}
|
||||
auto &object = hasObject ? objectsContainer.GetObject(objectName)
|
||||
: globalObjectsContainer.GetObject(objectName);
|
||||
auto &variablesContainer = object.GetVariables();
|
||||
|
||||
gd::EventsVariableInstructionTypeSwitcher
|
||||
eventsVariableInstructionTypeSwitcher(
|
||||
project.GetCurrentPlatform(), changeset.typeChangedVariableNames,
|
||||
variablesContainer);
|
||||
gd::ProjectBrowserHelper::ExposeProjectEvents(
|
||||
project, eventsVariableInstructionTypeSwitcher);
|
||||
}
|
||||
|
||||
// Switch types of instructions for the group.
|
||||
gd::EventsVariableInstructionTypeSwitcher
|
||||
eventsVariableInstructionTypeSwitcher(project.GetCurrentPlatform(),
|
||||
changeset.typeChangedVariableNames,
|
||||
objectGroup.GetName());
|
||||
gd::ProjectBrowserHelper::ExposeProjectEvents(
|
||||
project, eventsVariableInstructionTypeSwitcher);
|
||||
}
|
||||
@@ -849,7 +932,8 @@ void WholeProjectRefactorer::RenameEventsBasedBehaviorProperty(
|
||||
oldPropertyName, newPropertyName);
|
||||
|
||||
gd::ProjectBrowserHelper::ExposeEventsBasedBehaviorEvents(
|
||||
project, eventsFunctionsExtension, eventsBasedBehavior, behaviorRenamer);
|
||||
project, eventsFunctionsExtension, eventsBasedBehavior,
|
||||
behaviorRenamer);
|
||||
} else {
|
||||
// Properties that represent primitive values will be used through
|
||||
// their related actions/conditions/expressions. Rename these.
|
||||
@@ -919,7 +1003,8 @@ void WholeProjectRefactorer::RenameEventsBasedBehaviorSharedProperty(
|
||||
oldPropertyName, newPropertyName);
|
||||
|
||||
gd::ProjectBrowserHelper::ExposeEventsBasedBehaviorEvents(
|
||||
project, eventsFunctionsExtension, eventsBasedBehavior, behaviorRenamer);
|
||||
project, eventsFunctionsExtension, eventsBasedBehavior,
|
||||
behaviorRenamer);
|
||||
} else {
|
||||
// Properties that represent primitive values will be used through
|
||||
// their related actions/conditions/expressions. Rename these.
|
||||
@@ -1518,7 +1603,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 +1625,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 +1635,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 +1726,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(
|
||||
@@ -1793,10 +1947,9 @@ void WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
|
||||
bool isObjectGroup) {
|
||||
// Object groups can't be in other groups
|
||||
if (!isObjectGroup) {
|
||||
for (std::size_t g = 0;
|
||||
g < project.GetObjects().GetObjectGroups().size(); ++g) {
|
||||
project.GetObjects().GetObjectGroups()[g].RenameObject(oldName,
|
||||
newName);
|
||||
for (std::size_t g = 0; g < project.GetObjects().GetObjectGroups().size();
|
||||
++g) {
|
||||
project.GetObjects().GetObjectGroups()[g].RenameObject(oldName, newName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1805,13 +1958,13 @@ void WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
|
||||
if (layout.GetObjects().HasObjectNamed(oldName))
|
||||
continue;
|
||||
|
||||
ObjectOrGroupRenamedInLayout(project, layout, oldName, newName,
|
||||
ObjectOrGroupRenamedInScene(project, layout, oldName, newName,
|
||||
isObjectGroup);
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::GlobalObjectRemoved(
|
||||
gd::Project &project, const gd::String &objectName) {
|
||||
void WholeProjectRefactorer::GlobalObjectRemoved(gd::Project &project,
|
||||
const gd::String &objectName) {
|
||||
auto &globalGroups = project.GetObjects().GetObjectGroups();
|
||||
for (std::size_t g = 0; g < globalGroups.size(); ++g) {
|
||||
globalGroups[g].RemoveObject(objectName);
|
||||
@@ -1822,7 +1975,7 @@ void WholeProjectRefactorer::GlobalObjectRemoved(
|
||||
if (layout.GetObjects().HasObjectNamed(objectName))
|
||||
continue;
|
||||
|
||||
ObjectRemovedInLayout(project, layout, objectName);
|
||||
ObjectRemovedInScene(project, layout, objectName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1833,13 +1986,13 @@ void WholeProjectRefactorer::BehaviorsAddedToGlobalObject(
|
||||
if (layout.GetObjects().HasObjectNamed(objectName))
|
||||
continue;
|
||||
|
||||
BehaviorsAddedToObjectInLayout(project, layout, objectName);
|
||||
BehaviorsAddedToObjectInScene(project, layout, objectName);
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RemoveLayerInScene(gd::Project &project,
|
||||
gd::Layout &scene,
|
||||
const gd::String &layerName) {
|
||||
gd::Layout &scene,
|
||||
const gd::String &layerName) {
|
||||
if (layerName.empty())
|
||||
return;
|
||||
|
||||
@@ -1853,15 +2006,14 @@ void WholeProjectRefactorer::RemoveLayerInScene(gd::Project &project,
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::MergeLayersInScene(gd::Project &project,
|
||||
gd::Layout &scene,
|
||||
const gd::String &originLayerName,
|
||||
const gd::String &targetLayerName) {
|
||||
void WholeProjectRefactorer::MergeLayersInScene(
|
||||
gd::Project &project, gd::Layout &scene, const gd::String &originLayerName,
|
||||
const gd::String &targetLayerName) {
|
||||
if (originLayerName == targetLayerName || originLayerName.empty())
|
||||
return;
|
||||
|
||||
scene.GetInitialInstances().MoveInstancesToLayer(originLayerName,
|
||||
targetLayerName);
|
||||
targetLayerName);
|
||||
|
||||
std::vector<gd::String> externalLayoutsNames =
|
||||
GetAssociatedExternalLayouts(project, scene);
|
||||
|
@@ -19,6 +19,7 @@ class Project;
|
||||
class Layout;
|
||||
class Layer;
|
||||
class Object;
|
||||
class ObjectGroup;
|
||||
class String;
|
||||
class EventsFunctionsExtension;
|
||||
class EventsFunction;
|
||||
@@ -58,6 +59,8 @@ struct VariablesChangeset : VariablesRenamingChangesetNode {
|
||||
* would take more time than checking the instruction type is rightly set.
|
||||
*/
|
||||
std::unordered_set<gd::String> typeChangedVariableNames;
|
||||
std::unordered_set<gd::String> valueChangedVariableNames;
|
||||
std::unordered_set<gd::String> addedVariableNames;
|
||||
|
||||
bool HasRemovedVariables() { return !removedVariableNames.empty(); }
|
||||
|
||||
@@ -68,7 +71,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.
|
||||
*/
|
||||
@@ -91,6 +94,18 @@ class GD_CORE_API WholeProjectRefactorer {
|
||||
const gd::VariablesChangeset &changeset,
|
||||
const gd::SerializerElement &originalSerializedVariables);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project according to the changes (renaming or deletion)
|
||||
* made to variables of a group.
|
||||
*/
|
||||
static void ApplyRefactoringForGroupVariablesContainer(
|
||||
gd::Project &project, gd::ObjectsContainer &globalObjectsContainer,
|
||||
gd::ObjectsContainer &objectsContainer,
|
||||
const gd::VariablesContainer &groupVariablesContainer,
|
||||
const gd::ObjectGroup &objectGroup,
|
||||
const gd::VariablesChangeset &changeset,
|
||||
const gd::SerializerElement &originalSerializedVariables);
|
||||
|
||||
/**
|
||||
* \brief Refactor the project **before** an events function extension is
|
||||
* renamed.
|
||||
@@ -339,40 +354,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 +446,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 +458,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 +469,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
|
||||
|
@@ -224,6 +224,20 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t CustomObjectConfiguration::GetAnimationsCount() const {
|
||||
return animations.GetAnimationsCount();
|
||||
}
|
||||
|
||||
const gd::String &
|
||||
CustomObjectConfiguration::GetAnimationName(size_t index) const {
|
||||
return animations.GetAnimation(index).GetName();
|
||||
}
|
||||
|
||||
bool CustomObjectConfiguration::HasAnimationNamed(
|
||||
const gd::String &name) const {
|
||||
return animations.HasAnimationNamed(name);
|
||||
}
|
||||
|
||||
const SpriteAnimationList& CustomObjectConfiguration::GetAnimations() const {
|
||||
return animations;
|
||||
}
|
||||
|
@@ -67,6 +67,12 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
|
||||
|
||||
gd::ObjectConfiguration &GetChildObjectConfiguration(const gd::String& objectName);
|
||||
|
||||
std::size_t GetAnimationsCount() const override;
|
||||
|
||||
const gd::String &GetAnimationName(size_t index) const override;
|
||||
|
||||
bool HasAnimationNamed(const gd::String &animationName) const override;
|
||||
|
||||
/**
|
||||
* \brief Return the animation configuration for Animatable custom objects.
|
||||
*/
|
||||
|
@@ -14,6 +14,7 @@
|
||||
#include "GDCore/Tools/Log.h"
|
||||
|
||||
namespace gd {
|
||||
gd::String ObjectConfiguration::badAnimationName;
|
||||
|
||||
ObjectConfiguration::~ObjectConfiguration() {}
|
||||
|
||||
|
@@ -165,7 +165,36 @@ class GD_CORE_API ObjectConfiguration {
|
||||
void UnserializeFrom(gd::Project& project, const SerializerElement& element);
|
||||
///@}
|
||||
|
||||
protected:
|
||||
/** \name Animations
|
||||
* Members functions related to object animations
|
||||
*/
|
||||
///@{
|
||||
/**
|
||||
* \brief Return the number of animations declared in this object
|
||||
* configuration.
|
||||
*/
|
||||
virtual size_t GetAnimationsCount() const {
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Return the name of an animation declared in this object
|
||||
* configuration.
|
||||
*/
|
||||
virtual const gd::String &GetAnimationName(size_t index) const {
|
||||
return badAnimationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Return true if an animation is declared in this object
|
||||
* configuration for a given name.
|
||||
*/
|
||||
virtual bool HasAnimationNamed(const gd::String &animationName) const {
|
||||
return false;
|
||||
}
|
||||
///@}
|
||||
|
||||
protected:
|
||||
gd::String type; ///< Which type of object is represented by this
|
||||
///< configuration.
|
||||
|
||||
@@ -181,6 +210,9 @@ class GD_CORE_API ObjectConfiguration {
|
||||
* custom attributes.
|
||||
*/
|
||||
virtual void DoSerializeTo(SerializerElement& element) const {};
|
||||
|
||||
private:
|
||||
static gd::String badAnimationName;
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -73,6 +73,15 @@ bool ObjectsContainersList::HasObjectNamed(const gd::String& name) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
const gd::Object* ObjectsContainersList::GetObject(const gd::String& name) const {
|
||||
for (auto it = objectsContainers.rbegin(); it != objectsContainers.rend();
|
||||
++it) {
|
||||
if ((*it)->HasObjectNamed(name)) return &(*it)->GetObject(name);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ObjectsContainersList::VariableExistence
|
||||
ObjectsContainersList::HasObjectOrGroupWithVariableNamed(
|
||||
const gd::String& objectOrGroupName, const gd::String& variableName) const {
|
||||
@@ -368,7 +377,7 @@ std::vector<gd::String> ObjectsContainersList::ExpandObjectName(
|
||||
}
|
||||
|
||||
// Ensure that all returned objects actually exists (i.e: if some groups have
|
||||
// names refering to non existing objects, don't return them).
|
||||
// names referring to non existing objects, don't return them).
|
||||
for (std::size_t i = 0; i < realObjects.size();) {
|
||||
if (!HasObjectNamed(realObjects[i]))
|
||||
realObjects.erase(realObjects.begin() + i);
|
||||
@@ -521,4 +530,63 @@ std::vector<gd::String> ObjectsContainersList::GetBehaviorsOfObject(
|
||||
*objectsContainers[0], *objectsContainers[1], objectName, searchInGroups);
|
||||
}
|
||||
|
||||
std::vector<gd::String> ObjectsContainersList::GetAnimationNamesOfObject(
|
||||
const gd::String &objectOrGroupName) const {
|
||||
std::vector<gd::String> animationNames;
|
||||
|
||||
for (auto it = objectsContainers.rbegin(); it != objectsContainers.rend();
|
||||
++it) {
|
||||
if ((*it)->HasObjectNamed(objectOrGroupName)) {
|
||||
const auto &objectConfiguration =
|
||||
(*it)->GetObject(objectOrGroupName).GetConfiguration();
|
||||
|
||||
for (size_t index = 0; index < objectConfiguration.GetAnimationsCount();
|
||||
index++) {
|
||||
animationNames.push_back(objectConfiguration.GetAnimationName(index));
|
||||
}
|
||||
return animationNames;
|
||||
}
|
||||
if ((*it)->GetObjectGroups().Has(objectOrGroupName)) {
|
||||
const auto &objectGroup = (*it)->GetObjectGroups().Get(objectOrGroupName);
|
||||
const auto &objectNames = objectGroup.GetAllObjectsNames();
|
||||
|
||||
std::size_t objectIndex = 0;
|
||||
bool isFirstObjectFound = false;
|
||||
for (; objectIndex < objectNames.size() && !isFirstObjectFound;
|
||||
objectIndex++) {
|
||||
const gd::String &objectName = objectNames[objectIndex];
|
||||
if (!HasObjectNamed(objectName)) {
|
||||
continue;
|
||||
}
|
||||
isFirstObjectFound = true;
|
||||
const auto &objectConfiguration =
|
||||
GetObject(objectName)->GetConfiguration();
|
||||
for (size_t index = 0; index < objectConfiguration.GetAnimationsCount();
|
||||
index++) {
|
||||
animationNames.push_back(objectConfiguration.GetAnimationName(index));
|
||||
}
|
||||
}
|
||||
for (; objectIndex < objectNames.size(); objectIndex++) {
|
||||
const gd::String &objectName = objectNames[objectIndex];
|
||||
if (!HasObjectNamed(objectName)) {
|
||||
continue;
|
||||
}
|
||||
const auto &objectConfiguration =
|
||||
GetObject(objectName)->GetConfiguration();
|
||||
|
||||
for (size_t animationIndex = 0; animationIndex < animationNames.size();
|
||||
animationIndex++) {
|
||||
if (!objectConfiguration.HasAnimationNamed(
|
||||
animationNames[animationIndex])) {
|
||||
animationNames.erase(animationNames.begin() + animationIndex);
|
||||
animationIndex--;
|
||||
}
|
||||
}
|
||||
}
|
||||
return animationNames;
|
||||
}
|
||||
}
|
||||
return animationNames;
|
||||
}
|
||||
|
||||
} // namespace gd
|
@@ -129,7 +129,17 @@ class GD_CORE_API ObjectsContainersList {
|
||||
const gd::String& objectName, bool searchInGroups = true) const;
|
||||
|
||||
/**
|
||||
* \brief Return a list containing all objects refered to by the group.
|
||||
* \brief Get the animation names of an object/group.
|
||||
* \note The animation names of a group are the animation names common to
|
||||
* every object of the group.
|
||||
*
|
||||
* @return The names of animations
|
||||
*/
|
||||
std::vector<gd::String>
|
||||
GetAnimationNamesOfObject(const gd::String &objectOrGroupName) const;
|
||||
|
||||
/**
|
||||
* \brief Return a list containing all objects referred to by the group.
|
||||
* If an object name is passed, then only this object name is returned.
|
||||
*
|
||||
* If \a onlyObjectToSelectIfPresent is set and present in the group(s),
|
||||
@@ -170,6 +180,8 @@ class GD_CORE_API ObjectsContainersList {
|
||||
private:
|
||||
bool HasObjectNamed(const gd::String& name) const;
|
||||
|
||||
const gd::Object* GetObject(const gd::String& name) const;
|
||||
|
||||
bool HasObjectWithVariableNamed(const gd::String& objectName,
|
||||
const gd::String& variableName) const;
|
||||
|
||||
|
@@ -30,6 +30,8 @@ gd::String Variable::TypeAsString(Type t) {
|
||||
return "structure";
|
||||
case Type::Array:
|
||||
return "array";
|
||||
case Type::MixedTypes:
|
||||
return "mixed";
|
||||
default:
|
||||
return "error-type";
|
||||
}
|
||||
@@ -46,6 +48,8 @@ Variable::Type Variable::StringAsType(const gd::String& str) {
|
||||
return Type::Structure;
|
||||
else if (str == "array")
|
||||
return Type::Array;
|
||||
else if (str == "mixed")
|
||||
return Type::MixedTypes;
|
||||
|
||||
// Default to number
|
||||
return Type::Number;
|
||||
@@ -56,6 +60,7 @@ bool Variable::IsPrimitive(const Type type) {
|
||||
}
|
||||
|
||||
void Variable::CastTo(const Type newType) {
|
||||
hasMixedValues = false;
|
||||
if (newType == Type::Number)
|
||||
SetValue(GetValue());
|
||||
else if (newType == Type::String)
|
||||
@@ -85,6 +90,9 @@ void Variable::CastTo(const Type newType) {
|
||||
type = Type::Array;
|
||||
// Free now unused memory
|
||||
children.clear();
|
||||
} else if (newType == Type::MixedTypes) {
|
||||
type = Type::MixedTypes;
|
||||
hasMixedValues = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +150,7 @@ Variable& Variable::GetChild(const gd::String& name) {
|
||||
if (it != children.end()) return *it->second;
|
||||
|
||||
type = Type::Structure;
|
||||
hasMixedValues = false;
|
||||
children[name] = std::make_shared<gd::Variable>();
|
||||
return *children[name];
|
||||
}
|
||||
@@ -202,6 +211,7 @@ Variable& Variable::PushNew() {
|
||||
const size_t count = GetChildrenCount();
|
||||
auto& variable = GetAtIndex(count);
|
||||
if (type == Type::Array && count > 0) {
|
||||
hasMixedValues = false;
|
||||
const auto childType = GetAtIndex(count - 1).type;
|
||||
variable.type = childType;
|
||||
if (childType == Type::Number) {
|
||||
@@ -224,6 +234,7 @@ void Variable::RemoveAtIndex(const size_t index) {
|
||||
|
||||
bool Variable::InsertAtIndex(const gd::Variable& variable, const size_t index) {
|
||||
if (type != Type::Array) return false;
|
||||
hasMixedValues = false;
|
||||
auto newVariable = std::make_shared<gd::Variable>(variable);
|
||||
if (index < childrenArray.size()) {
|
||||
childrenArray.insert(childrenArray.begin() + index, newVariable);
|
||||
@@ -238,6 +249,7 @@ bool Variable::InsertChild(const gd::String& name,
|
||||
if (type != Type::Structure || HasChild(name)) {
|
||||
return false;
|
||||
}
|
||||
hasMixedValues = false;
|
||||
children[name] = std::make_shared<gd::Variable>(variable);
|
||||
return true;
|
||||
};
|
||||
@@ -270,6 +282,9 @@ void Variable::SerializeTo(SerializerElement& element) const {
|
||||
child->SerializeTo(childrenElement.AddChild("variable"));
|
||||
}
|
||||
}
|
||||
if (hasMixedValues) {
|
||||
element.SetBoolAttribute("hasMixedValues", true);
|
||||
}
|
||||
}
|
||||
|
||||
void Variable::UnserializeFrom(const SerializerElement& element) {
|
||||
@@ -313,6 +328,9 @@ void Variable::UnserializeFrom(const SerializerElement& element) {
|
||||
PushNew().UnserializeFrom(childElement);
|
||||
}
|
||||
}
|
||||
if (element.GetBoolAttribute("hasMixedValues", false)) {
|
||||
MarkAsMixedValues();
|
||||
}
|
||||
}
|
||||
|
||||
Variable& Variable::ResetPersistentUuid() {
|
||||
@@ -384,7 +402,8 @@ Variable::Variable(const Variable& other)
|
||||
folded(other.folded),
|
||||
boolVal(other.boolVal),
|
||||
type(other.type),
|
||||
persistentUuid(other.persistentUuid) {
|
||||
persistentUuid(other.persistentUuid),
|
||||
hasMixedValues(other.hasMixedValues) {
|
||||
CopyChildren(other);
|
||||
}
|
||||
|
||||
@@ -396,6 +415,7 @@ Variable& Variable::operator=(const Variable& other) {
|
||||
boolVal = other.boolVal;
|
||||
type = other.type;
|
||||
persistentUuid = other.persistentUuid;
|
||||
hasMixedValues = other.hasMixedValues;
|
||||
CopyChildren(other);
|
||||
}
|
||||
|
||||
@@ -411,4 +431,61 @@ void Variable::CopyChildren(const gd::Variable& other) {
|
||||
childrenArray.push_back(std::make_shared<gd::Variable>(*child.get()));
|
||||
}
|
||||
}
|
||||
|
||||
bool Variable::operator==(const gd::Variable &variable) const {
|
||||
if (type != variable.type || hasMixedValues || variable.hasMixedValues) {
|
||||
return false;
|
||||
}
|
||||
if (type == Variable::Type::Number) {
|
||||
return value == variable.value;
|
||||
}
|
||||
if (type == Variable::Type::String) {
|
||||
return str == variable.str;
|
||||
}
|
||||
if (type == Variable::Type::Boolean) {
|
||||
return boolVal == variable.boolVal;
|
||||
}
|
||||
if (type == Variable::Type::Structure) {
|
||||
if (children.size() != variable.children.size()) {
|
||||
return false;
|
||||
}
|
||||
for (auto &pair : children) {
|
||||
const gd::String &name = pair.first;
|
||||
const auto &child = pair.second;
|
||||
|
||||
auto it = variable.children.find(name);
|
||||
if (it == variable.children.end()) {
|
||||
return false;
|
||||
}
|
||||
auto &otherChild = it->second;
|
||||
if (*child != *otherChild) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (type == Variable::Type::Array) {
|
||||
if (childrenArray.size() != variable.childrenArray.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < childrenArray.size(); ++i) {
|
||||
if (*childrenArray[i] != *variable.childrenArray[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// MixedTypes variables can't equal another variable.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Variable::operator!=(const gd::Variable &variable) const {
|
||||
return !(*this == variable);
|
||||
}
|
||||
|
||||
void Variable::MarkAsMixedValues() {
|
||||
hasMixedValues = true;
|
||||
ClearChildren();
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -4,8 +4,8 @@
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#ifndef GDCORE_VARIABLE_H
|
||||
#define GDCORE_VARIABLE_H
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
@@ -31,6 +31,8 @@ class GD_CORE_API Variable {
|
||||
static gd::Variable badVariable;
|
||||
enum Type {
|
||||
Unknown,
|
||||
/** Used when objects of a group have different types for a variable. */
|
||||
MixedTypes,
|
||||
|
||||
// Primitive types
|
||||
String,
|
||||
@@ -50,7 +52,7 @@ class GD_CORE_API Variable {
|
||||
/**
|
||||
* \brief Default constructor creating a variable with 0 as value.
|
||||
*/
|
||||
Variable() : value(0), type(Type::Number){};
|
||||
Variable() : value(0), type(Type::Number), hasMixedValues(false) {};
|
||||
Variable(const Variable&);
|
||||
virtual ~Variable(){};
|
||||
|
||||
@@ -87,6 +89,7 @@ class GD_CORE_API Variable {
|
||||
void SetString(const gd::String& newStr) {
|
||||
str = newStr;
|
||||
type = Type::String;
|
||||
hasMixedValues = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,6 +105,7 @@ class GD_CORE_API Variable {
|
||||
// NaN values are not supported by GDevelop nor the serializer.
|
||||
if (std::isnan(value)) value = 0.0;
|
||||
type = Type::Number;
|
||||
hasMixedValues = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,8 +119,23 @@ class GD_CORE_API Variable {
|
||||
void SetBool(bool val) {
|
||||
boolVal = val;
|
||||
type = Type::Boolean;
|
||||
hasMixedValues = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Return true when objects of a group have different values for a
|
||||
* variable.
|
||||
*/
|
||||
bool HasMixedValues() const {
|
||||
return hasMixedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Return true when objects of a group have different values for a
|
||||
* variable.
|
||||
*/
|
||||
void MarkAsMixedValues();
|
||||
|
||||
// Operators are overloaded to allow accessing to variable using a simple
|
||||
// int-like semantic.
|
||||
void operator=(double val) { SetValue(val); };
|
||||
@@ -168,6 +187,9 @@ class GD_CORE_API Variable {
|
||||
bool operator==(const bool val) const { return GetBool() == val; };
|
||||
bool operator!=(const bool val) const { return GetBool() != val; };
|
||||
|
||||
bool operator==(const gd::Variable& variable) const;
|
||||
bool operator!=(const gd::Variable& variable) const;
|
||||
|
||||
///@}
|
||||
|
||||
/** \name Collection types
|
||||
@@ -376,6 +398,7 @@ class GD_CORE_API Variable {
|
||||
mutable gd::String str;
|
||||
mutable double value;
|
||||
mutable bool boolVal;
|
||||
mutable bool hasMixedValues;
|
||||
mutable std::map<gd::String, std::shared_ptr<Variable>>
|
||||
children; ///< Children, when the variable is considered as a structure.
|
||||
mutable std::vector<std::shared_ptr<Variable>>
|
||||
@@ -392,5 +415,3 @@ class GD_CORE_API Variable {
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
||||
#endif // GDCORE_VARIABLE_H
|
||||
|
@@ -55,8 +55,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Object or expression completions when type is string") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("string", "My", 0, 2).ToString()
|
||||
};
|
||||
// clang-format on
|
||||
@@ -67,8 +67,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Object or expression completions when type is number") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 0, number, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
"{ 0, number, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("number", "My", 0, 2).ToString()
|
||||
};
|
||||
// clang-format on
|
||||
@@ -79,8 +79,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Object or expression completions when type is number|string") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 0, number|string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
"{ 0, number|string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("number|string", "My", 0, 2).ToString()
|
||||
};
|
||||
// clang-format on
|
||||
@@ -94,8 +94,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Object or expression completions in a variable name") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("string", "My", 0, 2).ToString()
|
||||
};
|
||||
// clang-format on
|
||||
@@ -115,8 +115,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Object or expression completions in a variable index") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 0, number, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
"{ 0, number, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("number", "My", 0, 2).ToString()
|
||||
};
|
||||
// clang-format on
|
||||
@@ -136,7 +136,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Object when type is an object") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 0, object, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 0, object, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
};
|
||||
// clang-format on
|
||||
REQUIRE(getCompletionsFor("object", "My", 0) == expectedCompletions);
|
||||
@@ -149,7 +149,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
// result in different code generation):
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 0, objectPtr, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 0, objectPtr, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
};
|
||||
// clang-format on
|
||||
REQUIRE(getCompletionsFor("objectPtr", "My", 0) == expectedCompletions);
|
||||
@@ -204,8 +204,8 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 0, unknown, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
"{ 0, unknown, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }",
|
||||
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
gd::ExpressionCompletionDescription::ForExpressionWithPrefix("unknown", "My", 9, 10).ToString()
|
||||
};
|
||||
// clang-format on
|
||||
@@ -215,7 +215,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Function with a Variable as argument") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 3, no type, 2, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
"{ 3, no type, 3, no prefix, myVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
};
|
||||
// clang-format on
|
||||
REQUIRE(getCompletionsFor("number",
|
||||
@@ -225,7 +225,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Object function with a Variable as argument") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedCompletions{
|
||||
"{ 3, no type, 2, no prefix, myObjectVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
"{ 3, no type, 3, no prefix, myObjectVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }",
|
||||
};
|
||||
// clang-format on
|
||||
REQUIRE(getCompletionsFor("number",
|
||||
@@ -254,7 +254,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Test with string type") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedObjectCompletions{
|
||||
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
};
|
||||
std::vector<gd::String> expectedBehaviorOrFunctionCompletions{
|
||||
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("Func", 9, 13, "MyObject").ToString(),
|
||||
@@ -277,7 +277,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Test with 'number|string' type") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedObjectCompletions{
|
||||
"{ 0, number|string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
"{ 0, number|string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
};
|
||||
std::vector<gd::String> expectedBehaviorOrFunctionCompletions{
|
||||
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("Func", 9, 13, "MyObject").ToString(),
|
||||
@@ -303,7 +303,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Test 1") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedObjectCompletions{
|
||||
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
};
|
||||
std::vector<gd::String> expectedBehaviorOrFunctionCompletions{
|
||||
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("Func", 9, 13, "MyObject").ToString(),
|
||||
@@ -336,7 +336,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Test 1") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedObjectCompletions{
|
||||
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
};
|
||||
std::vector<gd::String> expectedBehaviorCompletions{
|
||||
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("MyBehavior", 9, 19, "MyObject").ToString()};
|
||||
@@ -366,7 +366,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Test 2") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedObjectCompletions{
|
||||
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
};
|
||||
std::vector<gd::String> expectedBehaviorCompletions{
|
||||
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("MyBehavior", 9, 19, "MyObject").ToString()
|
||||
@@ -396,7 +396,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
|
||||
SECTION("Test 1") {
|
||||
// clang-format off
|
||||
std::vector<gd::String> expectedObjectCompletions{
|
||||
"{ 0, string, 2, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
"{ 0, string, 3, no prefix, MyObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }"
|
||||
};
|
||||
std::vector<gd::String> expectedBehaviorCompletions{
|
||||
gd::ExpressionCompletionDescription::ForBehaviorWithPrefix("MyBehavior", 9, 19, "MyObject").ToString()
|
||||
|
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
/**
|
||||
* @file Tests covering layout content helper methods.
|
||||
*/
|
||||
#include "GDCore/Project/Layout.h"
|
||||
#include "DummyPlatform.h"
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/Extensions/Platform.h"
|
||||
#include "GDCore/Project/Object.h"
|
||||
#include "GDCore/Project/ObjectsContainer.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "catch.hpp"
|
||||
|
||||
using namespace gd;
|
||||
|
||||
TEST_CASE("Layout", "[common]") {
|
||||
|
||||
SECTION("Find the type of a behavior in a object") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
|
||||
project.GetObjects(), layout.GetObjects(),
|
||||
"MyObject", "MyBehavior", true) == "MyExtension::MyBehavior");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for an object that doesn't have the behavior") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
|
||||
project.GetObjects(), layout.GetObjects(),
|
||||
"MyObject", "MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Find the type of a behavior in a group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto &group =
|
||||
layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
|
||||
project.GetObjects(), layout.GetObjects(),
|
||||
"MyGroup", "MyBehavior", true) == "MyExtension::MyBehavior");
|
||||
}
|
||||
|
||||
SECTION(
|
||||
"Give an empty type for a group with an object missing the behavior") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
// object2 doesn't have the behavior.
|
||||
|
||||
auto &group =
|
||||
layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
|
||||
project.GetObjects(), layout.GetObjects(),
|
||||
"MyGroup", "MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for a group with behaviors of same name but "
|
||||
"different types") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object2.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
|
||||
"MyBehavior");
|
||||
|
||||
auto &group =
|
||||
layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
|
||||
project.GetObjects(), layout.GetObjects(),
|
||||
"MyGroup", "MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for an empty group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
auto &group =
|
||||
layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(
|
||||
project.GetObjects(), layout.GetObjects(),
|
||||
"MyGroup", "MyBehavior", true) == "");
|
||||
}
|
||||
}
|
488
Core/tests/ObjectsContainersList.cpp
Normal file
488
Core/tests/ObjectsContainersList.cpp
Normal file
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
/**
|
||||
* @file Tests covering layout content helper methods.
|
||||
*/
|
||||
#include "GDCore/Project/ObjectsContainersList.h"
|
||||
#include "DummyPlatform.h"
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteObject.h"
|
||||
#include "GDCore/Extensions/Platform.h"
|
||||
#include "GDCore/Project/Layout.h"
|
||||
#include "GDCore/Project/Object.h"
|
||||
#include "GDCore/Project/ObjectsContainer.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Tools/MakeUnique.h"
|
||||
#include "catch.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
using namespace gd;
|
||||
|
||||
TEST_CASE("ObjectContainersList (HasObjectOrGroupNamed)", "[common]") {
|
||||
|
||||
SECTION("Can check an object exists") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.HasObjectOrGroupNamed("MyObject"));
|
||||
REQUIRE(!objectsContainersList.HasObjectOrGroupNamed("MyWrongObject"));
|
||||
}
|
||||
|
||||
SECTION("Can check a group exists") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.HasObjectOrGroupNamed("MyGroup"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ObjectContainersList (HasObjectOrGroupWithVariableNamed)", "[common]") {
|
||||
|
||||
SECTION("Can check a variable exists in an object") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
object.GetVariables().InsertNew("MyVariable", 0);
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.HasObjectOrGroupWithVariableNamed("MyObject", "MyVariable"));
|
||||
REQUIRE(!objectsContainersList.HasObjectOrGroupWithVariableNamed("MyObject", "MyWrongVariable"));
|
||||
}
|
||||
|
||||
SECTION("Can check a variable exists in a group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.GetVariables().InsertNew("MyVariable", 0);
|
||||
// This variable is only in one of the 2 objects.
|
||||
object1.GetVariables().InsertNew("MyOtherVariable", 0);
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object1.GetVariables().InsertNew("MyVariable", 0);
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.HasObjectOrGroupWithVariableNamed("MyGroup", "MyVariable"));
|
||||
REQUIRE(!objectsContainersList.HasObjectOrGroupWithVariableNamed("MyGroup", "MyWrongVariable"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ObjectContainersList (GetTypeOfObject)", "[common]") {
|
||||
|
||||
SECTION("Find the type of an object") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfObject("MyObject") == "MyExtension::Sprite");
|
||||
}
|
||||
|
||||
SECTION("Find the object type of a group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfObject("MyGroup") == "MyExtension::Sprite");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for groups with mixed object types") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "FakeObjectWithDefaultBehavior", "MyObject2", 0);
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfObject("MyGroup") == "");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for an empty group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfObject(
|
||||
"MyGroup") == "");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ObjectContainersList (GetTypeOfBehaviorInObjectOrGroup)",
|
||||
"[common]") {
|
||||
|
||||
SECTION("Find the type of a behavior in an object") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
|
||||
"MyObject", "MyBehavior", true) == "MyExtension::MyBehavior");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for an object that doesn't have the behavior") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
|
||||
"MyObject", "MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Find the type of a behavior in a group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
|
||||
"MyGroup", "MyBehavior", true) == "MyExtension::MyBehavior");
|
||||
}
|
||||
|
||||
SECTION(
|
||||
"Give an empty type for a group with an object missing the behavior") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
// object2 doesn't have the behavior.
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
|
||||
"MyGroup", "MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for a group with behaviors of same name but different types") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object2.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
|
||||
"MyBehavior");
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
|
||||
"MyGroup", "MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for an empty group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
|
||||
"MyGroup", "MyBehavior", true) == "");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ObjectContainersList (HasBehaviorInObjectOrGroup)", "[common]") {
|
||||
|
||||
SECTION("Can check a behavior exists in an object") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.HasBehaviorInObjectOrGroup("MyObject", "MyBehavior"));
|
||||
REQUIRE(!objectsContainersList.HasBehaviorInObjectOrGroup("MyObject", "MyWrongBehavior"));
|
||||
}
|
||||
|
||||
SECTION("Can check a behavior exists in a group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
// This behavior is only in one of the 2 objects.
|
||||
object1.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
|
||||
"MyOtherBehavior");
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
REQUIRE(objectsContainersList.HasBehaviorInObjectOrGroup("MyGroup", "MyBehavior"));
|
||||
REQUIRE(!objectsContainersList.HasBehaviorInObjectOrGroup("MyGroup", "MyOtherBehavior"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ObjectContainersList (GetBehaviorsOfObject)", "[common]") {
|
||||
|
||||
SECTION("Find the behaviors in an object") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject", 0);
|
||||
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
const auto behaviors =
|
||||
objectsContainersList.GetBehaviorsOfObject("MyObject", true);
|
||||
REQUIRE(behaviors.size() == 1);
|
||||
REQUIRE(behaviors[0] == "MyBehavior");
|
||||
}
|
||||
|
||||
SECTION("Find the behaviors in a group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
// This behavior is only in one of the 2 objects.
|
||||
object1.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
|
||||
"MyOtherBehavior");
|
||||
gd::Object &object2 = layout.GetObjects().InsertNewObject(
|
||||
project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
const auto behaviors =
|
||||
objectsContainersList.GetBehaviorsOfObject("MyGroup", true);
|
||||
REQUIRE(behaviors.size() == 1);
|
||||
REQUIRE(behaviors[0] == "MyBehavior");
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
gd::SpriteObject BuildSpriteWithAnimations(gd::String animationName1 = "!",
|
||||
gd::String animationName2 = "!",
|
||||
gd::String animationName3 = "!") {
|
||||
gd::SpriteObject configuration;
|
||||
gd::SpriteAnimationList &animations = configuration.GetAnimations();
|
||||
if (animationName1 != "!") {
|
||||
gd::Animation animation;
|
||||
animation.SetName(animationName1);
|
||||
animations.AddAnimation(animation);
|
||||
if (animationName2 != "!") {
|
||||
gd::Animation animation;
|
||||
animation.SetName(animationName2);
|
||||
animations.AddAnimation(animation);
|
||||
}
|
||||
if (animationName3 != "!") {
|
||||
gd::Animation animation;
|
||||
animation.SetName(animationName3);
|
||||
animations.AddAnimation(animation);
|
||||
}
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
|
||||
bool Contains(const std::vector<gd::String> &vector, const gd::String &value) {
|
||||
return std::find(vector.begin(), vector.end(), value) !=
|
||||
vector.end();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("ObjectContainersList (GetAnimationNamesOfObject)", "[common]") {
|
||||
|
||||
SECTION("Find the animation names in a sprite") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object object("MyObject", "Sprite",
|
||||
gd::make_unique<gd::SpriteObject>(
|
||||
BuildSpriteWithAnimations("Idle", "Run")));
|
||||
layout.GetObjects().InsertObject(object, 0);
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
const auto animationNames =
|
||||
objectsContainersList.GetAnimationNamesOfObject("MyObject");
|
||||
REQUIRE(Contains(animationNames, "Idle"));
|
||||
REQUIRE(Contains(animationNames, "Run"));
|
||||
REQUIRE(animationNames.size() == 2);
|
||||
}
|
||||
|
||||
SECTION("Find the animation names in a group of sprite") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object object1("MyObject1", "Sprite",
|
||||
gd::make_unique<gd::SpriteObject>(
|
||||
BuildSpriteWithAnimations("Idle", "Jump", "Run")));
|
||||
layout.GetObjects().InsertObject(object1, 0);
|
||||
gd::Object object2("MyObject2", "Sprite",
|
||||
gd::make_unique<gd::SpriteObject>(
|
||||
BuildSpriteWithAnimations("Run", "Idle", "Climb")));
|
||||
layout.GetObjects().InsertObject(object2, 0);
|
||||
|
||||
auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
auto objectsContainersList = gd::ObjectsContainersList::
|
||||
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
|
||||
|
||||
const auto animationNames =
|
||||
objectsContainersList.GetAnimationNamesOfObject("MyGroup");
|
||||
REQUIRE(Contains(animationNames, "Idle"));
|
||||
REQUIRE(Contains(animationNames, "Run"));
|
||||
REQUIRE(animationNames.size() == 2);
|
||||
}
|
||||
}
|
@@ -91,4 +91,49 @@ TEST_CASE("Variable", "[common][variables]") {
|
||||
"Hello second copied World");
|
||||
REQUIRE(variable3.GetChild("Child2").GetValue() == 44);
|
||||
}
|
||||
SECTION("Can find identical number variables") {
|
||||
gd::Variable variable;
|
||||
variable.SetValue(123);
|
||||
|
||||
gd::Variable otherVariable;
|
||||
otherVariable.SetValue(123);
|
||||
|
||||
REQUIRE(variable == otherVariable);
|
||||
}
|
||||
SECTION("Can find different number variables") {
|
||||
gd::Variable variable;
|
||||
variable.SetValue(123);
|
||||
|
||||
gd::Variable otherVariable;
|
||||
otherVariable.SetValue(456);
|
||||
|
||||
REQUIRE(variable != otherVariable);
|
||||
}
|
||||
SECTION("Can find identical structure variables") {
|
||||
gd::Variable variable;
|
||||
variable.GetChild("MyChild").SetValue(123);
|
||||
|
||||
gd::Variable otherVariable;
|
||||
otherVariable.GetChild("MyChild").SetValue(123);
|
||||
|
||||
REQUIRE(variable == otherVariable);
|
||||
}
|
||||
SECTION("Can find structure with different child value") {
|
||||
gd::Variable variable;
|
||||
variable.GetChild("MyChild").SetValue(123);
|
||||
|
||||
gd::Variable otherVariable;
|
||||
otherVariable.GetChild("MyChild").SetValue(456);
|
||||
|
||||
REQUIRE(variable != otherVariable);
|
||||
}
|
||||
SECTION("Can find structure with different child name") {
|
||||
gd::Variable variable;
|
||||
variable.GetChild("MyChild").SetValue(123);
|
||||
|
||||
gd::Variable otherVariable;
|
||||
otherVariable.GetChild("MyOtherChild").SetValue(123);
|
||||
|
||||
REQUIRE(variable != otherVariable);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
@@ -235,6 +235,19 @@ void Model3DObjectConfiguration::ExposeResources(
|
||||
worker.ExposeModel3D(modelResourceName);
|
||||
}
|
||||
|
||||
const gd::String &
|
||||
Model3DObjectConfiguration::GetAnimationName(size_t index) const {
|
||||
return GetAnimation(index).GetName();
|
||||
}
|
||||
|
||||
bool Model3DObjectConfiguration::HasAnimationNamed(
|
||||
const gd::String &name) const {
|
||||
return !name.empty() && (find_if(animations.begin(), animations.end(),
|
||||
[&name](const Model3DAnimation &animation) {
|
||||
return animation.GetName() == name;
|
||||
}) != animations.end());
|
||||
}
|
||||
|
||||
Model3DAnimation Model3DObjectConfiguration::badAnimation;
|
||||
|
||||
const Model3DAnimation &
|
||||
@@ -252,14 +265,6 @@ Model3DAnimation &Model3DObjectConfiguration::GetAnimation(std::size_t nb) {
|
||||
return animations[nb];
|
||||
}
|
||||
|
||||
bool Model3DObjectConfiguration::HasAnimationNamed(
|
||||
const gd::String &name) const {
|
||||
return !name.empty() && (find_if(animations.begin(), animations.end(),
|
||||
[&name](const Model3DAnimation &animation) {
|
||||
return animation.GetName() == name;
|
||||
}) != animations.end());
|
||||
}
|
||||
|
||||
void Model3DObjectConfiguration::AddAnimation(
|
||||
const Model3DAnimation &animation) {
|
||||
animations.push_back(animation);
|
||||
|
@@ -85,6 +85,12 @@ public:
|
||||
* Methods related to animations management
|
||||
*/
|
||||
///@{
|
||||
std::size_t GetAnimationsCount() const override { return animations.size(); };
|
||||
|
||||
const gd::String &GetAnimationName(size_t index) const override;
|
||||
|
||||
bool HasAnimationNamed(const gd::String &animationName) const override;
|
||||
|
||||
/**
|
||||
* \brief Return the animation at the specified index.
|
||||
* If the index is out of bound, a "bad animation" object is returned.
|
||||
@@ -97,16 +103,6 @@ public:
|
||||
*/
|
||||
Model3DAnimation &GetAnimation(std::size_t nb);
|
||||
|
||||
/**
|
||||
* \brief Return the number of animations this object has.
|
||||
*/
|
||||
std::size_t GetAnimationsCount() const { return animations.size(); };
|
||||
|
||||
/**
|
||||
* \brief Return true if the animation called "name" exists.
|
||||
*/
|
||||
bool HasAnimationNamed(const gd::String& name) const;
|
||||
|
||||
/**
|
||||
* \brief Add an animation at the end of the existing ones.
|
||||
*/
|
||||
|
@@ -118,6 +118,19 @@ namespace gdjs {
|
||||
this._materialType = this._convertMaterialType(
|
||||
objectData.content.materialType
|
||||
);
|
||||
|
||||
this.onModelChanged(objectData);
|
||||
|
||||
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called after the renderer loaded a Model resource:
|
||||
* - After the renderer was instantiated
|
||||
* - After reloading the model
|
||||
*/
|
||||
private onModelChanged(objectData) {
|
||||
this._updateModel(objectData);
|
||||
if (this._animations.length > 0) {
|
||||
this._renderer.playAnimation(
|
||||
@@ -125,9 +138,6 @@ namespace gdjs {
|
||||
this._animations[0].loop
|
||||
);
|
||||
}
|
||||
|
||||
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
updateFromObjectData(
|
||||
@@ -135,18 +145,7 @@ namespace gdjs {
|
||||
newObjectData: Model3DObjectData
|
||||
): boolean {
|
||||
super.updateFromObjectData(oldObjectData, newObjectData);
|
||||
if (
|
||||
oldObjectData.content.width !== newObjectData.content.width ||
|
||||
oldObjectData.content.height !== newObjectData.content.height ||
|
||||
oldObjectData.content.depth !== newObjectData.content.depth ||
|
||||
oldObjectData.content.rotationX !== newObjectData.content.rotationX ||
|
||||
oldObjectData.content.rotationY !== newObjectData.content.rotationY ||
|
||||
oldObjectData.content.rotationZ !== newObjectData.content.rotationZ ||
|
||||
oldObjectData.content.keepAspectRatio !==
|
||||
newObjectData.content.keepAspectRatio
|
||||
) {
|
||||
this._updateModel(newObjectData);
|
||||
}
|
||||
|
||||
if (
|
||||
oldObjectData.content.materialType !==
|
||||
newObjectData.content.materialType
|
||||
@@ -154,6 +153,24 @@ namespace gdjs {
|
||||
this._materialType = this._convertMaterialType(
|
||||
newObjectData.content.materialType
|
||||
);
|
||||
}
|
||||
if (
|
||||
oldObjectData.content.modelResourceName !==
|
||||
newObjectData.content.modelResourceName
|
||||
) {
|
||||
this._reloadModel(newObjectData);
|
||||
} else if (
|
||||
oldObjectData.content.width !== newObjectData.content.width ||
|
||||
oldObjectData.content.height !== newObjectData.content.height ||
|
||||
oldObjectData.content.depth !== newObjectData.content.depth ||
|
||||
oldObjectData.content.rotationX !== newObjectData.content.rotationX ||
|
||||
oldObjectData.content.rotationY !== newObjectData.content.rotationY ||
|
||||
oldObjectData.content.rotationZ !== newObjectData.content.rotationZ ||
|
||||
oldObjectData.content.keepAspectRatio !==
|
||||
newObjectData.content.keepAspectRatio ||
|
||||
oldObjectData.content.materialType !==
|
||||
newObjectData.content.materialType
|
||||
) {
|
||||
this._updateModel(newObjectData);
|
||||
}
|
||||
if (
|
||||
@@ -218,6 +235,12 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
_reloadModel(objectData: Model3DObjectData) {
|
||||
this._modelResourceName = objectData.content.modelResourceName;
|
||||
this._renderer._reloadModel(this, this._runtimeScene);
|
||||
this.onModelChanged(objectData);
|
||||
}
|
||||
|
||||
_updateModel(objectData: Model3DObjectData) {
|
||||
const rotationX = objectData.content.rotationX || 0;
|
||||
const rotationY = objectData.content.rotationY || 0;
|
||||
|
@@ -236,6 +236,20 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `_updateModel` should always be called after this method.
|
||||
* Ideally, use `Model3DRuntimeObject#_reloadModel` instead.
|
||||
*/
|
||||
_reloadModel(
|
||||
runtimeObject: Model3DRuntimeObject,
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
) {
|
||||
this._originalModel = instanceContainer
|
||||
.getGame()
|
||||
.getModel3DManager()
|
||||
.getModel(runtimeObject._modelResourceName);
|
||||
}
|
||||
|
||||
_updateModel(
|
||||
rotationX: float,
|
||||
rotationY: float,
|
||||
|
@@ -38,7 +38,7 @@ module.exports = {
|
||||
.setName('Consent Cordova plugin')
|
||||
.setDependencyType('cordova')
|
||||
.setExportName('cordova-plugin-consent')
|
||||
.setVersion('2.4.0')
|
||||
.setVersion('3.0.0-alpha.8')
|
||||
.onlyIfOtherDependencyIsExported('AdMob Cordova plugin');
|
||||
|
||||
extension
|
||||
@@ -58,7 +58,7 @@ module.exports = {
|
||||
.setName('AdMob Cordova plugin')
|
||||
.setDependencyType('cordova')
|
||||
.setExportName('admob-plus-cordova')
|
||||
.setVersion('1.28.0')
|
||||
.setVersion('2.0.0-alpha.18')
|
||||
.setExtraSetting(
|
||||
'APP_ID_ANDROID',
|
||||
new gd.PropertyDescriptor('AdMobAppIdAndroid').setType(
|
||||
@@ -166,11 +166,11 @@ module.exports = {
|
||||
)
|
||||
.addParameter('string', _('Android app open ID'), '', false)
|
||||
.setParameterLongDescription(
|
||||
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/3419835294"` for loading a test app open.'
|
||||
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/9257395921"` for loading a test app open.'
|
||||
)
|
||||
.addParameter('string', _('iOS app open ID'), '', false)
|
||||
.setParameterLongDescription(
|
||||
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/5662855259"` for loading a test app open.'
|
||||
'Get it from your AdMob account. You can use `"ca-app-pub-3940256099942544/5575463023"` for loading a test app open.'
|
||||
)
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
|
@@ -7,8 +7,8 @@ namespace gdjs {
|
||||
|
||||
const testAdIds = {
|
||||
appOpen: {
|
||||
android: 'ca-app-pub-3940256099942544/3419835294',
|
||||
ios: 'ca-app-pub-3940256099942544/5662855259',
|
||||
android: 'ca-app-pub-3940256099942544/9257395921',
|
||||
ios: 'ca-app-pub-3940256099942544/5575463023',
|
||||
},
|
||||
banner: {
|
||||
android: 'ca-app-pub-3940256099942544/6300978111',
|
||||
@@ -67,6 +67,7 @@ namespace gdjs {
|
||||
|
||||
// Admob does not initialize automatically, so we store a flag to know if it's initialized.
|
||||
let admobStarted = false;
|
||||
let isStarting = false;
|
||||
let isUsingTestAds = false;
|
||||
|
||||
// Banner
|
||||
@@ -115,9 +116,13 @@ namespace gdjs {
|
||||
async () => {
|
||||
// Obtain user consent ?
|
||||
|
||||
logger.info('Starting AdMob.');
|
||||
isStarting = true;
|
||||
|
||||
await admob.start();
|
||||
|
||||
logger.info('AdMob successfully started.');
|
||||
isStarting = false;
|
||||
admobStarted = true;
|
||||
},
|
||||
false
|
||||
@@ -126,15 +131,32 @@ namespace gdjs {
|
||||
/**
|
||||
* Helper to know if we are on mobile and admob is correctly initialized.
|
||||
*/
|
||||
const checkIfAdMobIsAvailable = () => {
|
||||
const checkIfAdMobIsAvailable = async () => {
|
||||
if (typeof cordova === 'undefined') {
|
||||
logger.warn('We are not on mobile, AdMob will not be available.');
|
||||
return false;
|
||||
}
|
||||
if (typeof admob === 'undefined' || !admobStarted) {
|
||||
logger.warn('AdMob has not been configured or started properly.');
|
||||
if (typeof admob === 'undefined') {
|
||||
logger.warn('AdMob has not been configured properly.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!admobStarted) {
|
||||
if (isStarting) {
|
||||
// Delay the call until AdMob is started, up to 5 seconds.
|
||||
let time = 0;
|
||||
while (!admobStarted && time < 5000) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
time += 100;
|
||||
}
|
||||
}
|
||||
|
||||
if (!admobStarted) {
|
||||
logger.warn('AdMob is not started.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -164,8 +186,10 @@ namespace gdjs {
|
||||
* charging advertisers. If you click on too many ads without being in test mode, you risk your
|
||||
* account being flagged for invalid activity.
|
||||
*/
|
||||
export const setTestMode = (enable: boolean) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
export const setTestMode = async (enable: boolean) => {
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
logger.info('Setting AdMob test mode to:', enable);
|
||||
|
||||
isUsingTestAds = enable;
|
||||
};
|
||||
@@ -185,7 +209,7 @@ namespace gdjs {
|
||||
displayLandscape,
|
||||
displayWhenLoaded
|
||||
) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
// If an appOpen is already loading or showing, we don't stop it.
|
||||
if (appOpenLoading || appOpenShowing) {
|
||||
return;
|
||||
@@ -242,7 +266,7 @@ namespace gdjs {
|
||||
|
||||
/** Show the loaded appOpen. */
|
||||
export const showAppOpen = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!appOpen) {
|
||||
logger.warn('App Open has not been set up, call loadAppOpen first.');
|
||||
@@ -293,7 +317,7 @@ namespace gdjs {
|
||||
* If a banner is already set up, it will be hidden and replaced by the new one.
|
||||
*/
|
||||
export const setupBanner = async (androidAdUnitId, iosAdUnitId, atTop) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
const adUnitId = getAdUnitId(androidAdUnitId, iosAdUnitId, 'banner');
|
||||
if (!adUnitId) return;
|
||||
|
||||
@@ -354,7 +378,7 @@ namespace gdjs {
|
||||
|
||||
/** Hide the banner shown on screen. */
|
||||
export const hideBanner = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!banner || !bannerShowing) {
|
||||
logger.warn('No banner is being shown.');
|
||||
@@ -381,7 +405,7 @@ namespace gdjs {
|
||||
iosAdUnitId,
|
||||
displayWhenLoaded
|
||||
) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
// If an interstitial is already loading or showing, we don't stop it.
|
||||
if (interstitialLoading || interstitialShowing) {
|
||||
return;
|
||||
@@ -440,7 +464,7 @@ namespace gdjs {
|
||||
|
||||
/** Show the loaded interstitial. */
|
||||
export const showInterstitial = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!interstitial) {
|
||||
logger.warn(
|
||||
@@ -495,7 +519,7 @@ namespace gdjs {
|
||||
iosAdUnitID,
|
||||
displayWhenLoaded
|
||||
) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
if (rewardedInterstitialLoading || rewardedInterstitialShowing) {
|
||||
return;
|
||||
}
|
||||
@@ -557,7 +581,7 @@ namespace gdjs {
|
||||
|
||||
/** Show the loaded reward interstitial. */
|
||||
export const showRewardedInterstitial = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!rewardedInterstitial) {
|
||||
logger.warn(
|
||||
@@ -614,7 +638,7 @@ namespace gdjs {
|
||||
iosAdUnitID,
|
||||
displayWhenLoaded
|
||||
) => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
if (rewardedVideoLoading || rewardedVideoShowing) {
|
||||
return;
|
||||
}
|
||||
@@ -672,7 +696,7 @@ namespace gdjs {
|
||||
|
||||
/** Show the loaded reward video. */
|
||||
export const showRewardedVideo = async () => {
|
||||
if (!checkIfAdMobIsAvailable()) return;
|
||||
if (!(await checkIfAdMobIsAvailable())) return;
|
||||
|
||||
if (!rewardedVideo) {
|
||||
logger.warn('Video has not been set up, call loadRewardedVideo first.');
|
||||
|
@@ -417,7 +417,7 @@ module.exports = {
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerLeft');
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerJustLeft');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
@@ -443,7 +443,108 @@ module.exports = {
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerLeft');
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerJustLeft');
|
||||
|
||||
extension
|
||||
.addExpression(
|
||||
'LastLeftPlayerNumber',
|
||||
_('Last left player number'),
|
||||
_('Returns the number of the player that has just left the lobby.'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName(
|
||||
'gdjs.multiplayerMessageManager.getLatestPlayerWhoJustLeft'
|
||||
);
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'HasAnyPlayerJoined',
|
||||
_('Any player has joined'),
|
||||
_('Check if any player has joined the lobby.'),
|
||||
_('Any player has joined'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerJustJoined');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'HasPlayerJoined',
|
||||
_('Player has joined'),
|
||||
_('Check if the player has joined the lobby.'),
|
||||
_('Player _PARAM0_ has joined'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.addParameter('number', _('Player number'), '', false)
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerJustJoined');
|
||||
|
||||
extension
|
||||
.addExpression(
|
||||
'LastJoinedPlayerNumber',
|
||||
_('Last joined player number'),
|
||||
_('Returns the number of the player that has just joined the lobby.'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName(
|
||||
'gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined'
|
||||
);
|
||||
|
||||
extension
|
||||
.addStrExpression(
|
||||
@@ -520,6 +621,37 @@ module.exports = {
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('gdjs.multiplayer.getPlayersInLobbyCount');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'IsPlayerConnected',
|
||||
_('Player is connected'),
|
||||
_('Check if the specified player is connected to the lobby.'),
|
||||
_('Player _PARAM0_ is connected'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.addParameter(
|
||||
'number',
|
||||
_('The position of the player in the lobby (1, 2, ...)'),
|
||||
'',
|
||||
false
|
||||
)
|
||||
.setHelpPath('/all-features/multiplayer')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.isPlayerConnected');
|
||||
|
||||
extension
|
||||
.addExpressionAndCondition(
|
||||
'number',
|
||||
|
@@ -34,8 +34,66 @@ namespace gdjs {
|
||||
this.cache.add(key);
|
||||
this.keys.push(key);
|
||||
}
|
||||
|
||||
clear = () => {
|
||||
this.cache.clear();
|
||||
this.keys = [];
|
||||
};
|
||||
}
|
||||
|
||||
class SavedSyncDataUpdates<T> {
|
||||
private _updates: T[] = [];
|
||||
|
||||
store(update: T) {
|
||||
this._updates.push(update);
|
||||
if (this._updates.length > 10) {
|
||||
this._updates.shift();
|
||||
}
|
||||
}
|
||||
|
||||
getUpdates() {
|
||||
return this._updates;
|
||||
}
|
||||
|
||||
remove(update: T) {
|
||||
const index = this._updates.indexOf(update);
|
||||
if (index !== -1) {
|
||||
this._updates.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._updates = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to clone an object without reassigning the target object.
|
||||
* It's mainly helpful for tests, where multiple instances of the MultiplayerMessageManager are created,
|
||||
* and prevents keeping references to the same object.
|
||||
*/
|
||||
const cloneObjectWithoutOverwriting = ({
|
||||
target,
|
||||
source,
|
||||
}: {
|
||||
target: Object;
|
||||
source: Object;
|
||||
}) => {
|
||||
// Add the new properties.
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key) && !target.hasOwnProperty(key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the properties that are not in the source.
|
||||
for (const key in target) {
|
||||
if (target.hasOwnProperty(key) && !source.hasOwnProperty(key)) {
|
||||
delete target[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export type MultiplayerMessageManager = ReturnType<
|
||||
typeof makeMultiplayerMessageManager
|
||||
>;
|
||||
@@ -91,24 +149,39 @@ namespace gdjs {
|
||||
let lastSceneSyncTimestamp = 0;
|
||||
let lastSentSceneSyncData: LayoutNetworkSyncData | null = null;
|
||||
let numberOfForcedSceneUpdates = 0;
|
||||
let lastReceivedSceneSyncDataUpdates = new SavedSyncDataUpdates<
|
||||
LayoutNetworkSyncData
|
||||
>();
|
||||
|
||||
// The number of times per second the game data should be synchronized.
|
||||
const gameSyncDataTickRate = 1;
|
||||
let lastGameSyncTimestamp = 0;
|
||||
let lastSentGameSyncData: GameNetworkSyncData | null = null;
|
||||
let numberOfForcedGameUpdates = 0;
|
||||
let lastReceivedGameSyncDataUpdates = new SavedSyncDataUpdates<
|
||||
GameNetworkSyncData
|
||||
>();
|
||||
|
||||
// Send heartbeat messages to host to ensure the connection is still alive.
|
||||
// Send heartbeat messages from host to players, ensuring their connection is still alive,
|
||||
// measure the ping, and send other useful info.
|
||||
const heartbeatTickRate = 1;
|
||||
let lastHeartbeatTimestamp = 0;
|
||||
let _playersLastHeartbeatInfo: {
|
||||
[playerNumber: number]: {
|
||||
lastRoundTripTimes: number[];
|
||||
};
|
||||
let _playersLastRoundTripTimes: {
|
||||
[playerNumber: number]: number[];
|
||||
} = {};
|
||||
let _peerIdToPlayerNumber: { [peerId: string]: number } = {};
|
||||
let _playersPings: { [playerNumber: number]: number } = { 1: 0 };
|
||||
let _playersInfo: {
|
||||
[playerNumber: number]: {
|
||||
ping: number;
|
||||
playerId: string;
|
||||
username: string;
|
||||
};
|
||||
} = {};
|
||||
let _playerNumbersWhoJustLeft: number[] = [];
|
||||
let _playerNumbersWhoJustJoined: number[] = [];
|
||||
let _temporaryPlayerNumberToUsername: {
|
||||
[playerNumber: number]: string;
|
||||
} = {};
|
||||
|
||||
const addExpectedMessageAcknowledgement = ({
|
||||
originalMessageName,
|
||||
@@ -157,11 +230,6 @@ namespace gdjs {
|
||||
});
|
||||
};
|
||||
|
||||
const clearExpectedMessageAcknowledgements = () => {
|
||||
expectedMessageAcknowledgements = {};
|
||||
_lastClockReceivedByInstanceByScene = {};
|
||||
};
|
||||
|
||||
const getLastClockReceivedForInstanceOnScene = ({
|
||||
sceneNetworkId,
|
||||
instanceNetworkId,
|
||||
@@ -290,27 +358,6 @@ namespace gdjs {
|
||||
(instance) => instance.networkId === instanceNetworkId
|
||||
) || null;
|
||||
|
||||
if (!instance) {
|
||||
debugLogger.info(
|
||||
`instance ${objectName} ${instanceNetworkId} not found with network ID, trying to find it with persistent UUID.`
|
||||
);
|
||||
instance =
|
||||
instances.find(
|
||||
(instance) =>
|
||||
// For objects created from the start, the network ID is not set yet.
|
||||
instance.persistentUuid &&
|
||||
instance.persistentUuid.substring(0, 8) === instanceNetworkId
|
||||
) || null;
|
||||
|
||||
if (instance) {
|
||||
debugLogger.info(
|
||||
`instance ${objectName} ${instanceNetworkId} found with persistent UUID. Assigning network ID.`
|
||||
);
|
||||
// Set the network ID, as it was not set yet.
|
||||
instance.networkId = instanceNetworkId;
|
||||
}
|
||||
}
|
||||
|
||||
// If we know the position of the object, we can try to find the closest instance not synchronized yet.
|
||||
if (!instance && instanceX !== undefined && instanceY !== undefined) {
|
||||
debugLogger.info(
|
||||
@@ -411,6 +458,12 @@ namespace gdjs {
|
||||
const handleChangeInstanceOwnerMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Change owner messages do not need to be saved for later use, as the game will automatically change the owner of
|
||||
// the instance when receiving an update message with a different owner.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
|
||||
@@ -563,6 +616,12 @@ namespace gdjs {
|
||||
const handleUpdateInstanceMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Update instance messages do not need to be saved for later use, as the updates are sent pretty often,
|
||||
// a new one will be received very quickly.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
|
||||
@@ -732,6 +791,12 @@ namespace gdjs {
|
||||
const handleChangeVariableOwnerMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Change owner messages do not need to be saved for later use, as the game will automatically change the owner of
|
||||
// the variable when receiving an update message with a different owner.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
|
||||
@@ -884,6 +949,12 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const handleAcknowledgeMessagesReceived = () => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,
|
||||
// which are not sent when the game is not ready.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
// When we receive acknowledgement messages, save it in the extension, to avoid sending the message again.
|
||||
@@ -958,6 +1029,12 @@ namespace gdjs {
|
||||
const resendClearOrCancelAcknowledgedMessages = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,
|
||||
// which are not sent when the game is not ready.
|
||||
return;
|
||||
}
|
||||
|
||||
// When all acknowledgments are received for an message, we can clear the message from our
|
||||
// list of expected acknowledgments.
|
||||
const expectedMessageNames = Object.keys(expectedMessageAcknowledgements);
|
||||
@@ -1179,6 +1256,12 @@ namespace gdjs {
|
||||
const handleDestroyInstanceMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Destroy messages do not need to be saved for later use, as the game will automatically destroy
|
||||
// the instance if it does not receive an update message from it. So we return early.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
const destroyInstanceMessageNames = messageNamesArray.filter(
|
||||
@@ -1453,6 +1536,11 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const handleCustomMessagesReceived = (): void => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Assume that the custom messages are not worth saving for later use.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const messageNamesArray = Array.from(p2pMessagesMap.keys());
|
||||
const customMessageNames = messageNamesArray.filter((messageName) =>
|
||||
@@ -1569,6 +1657,11 @@ namespace gdjs {
|
||||
const handleUpdateSceneMessagesToSend = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): void => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Don't send messages if the multiplayer is not ready.
|
||||
return;
|
||||
}
|
||||
|
||||
const sceneNetworkSyncData = runtimeScene.getNetworkSyncData({
|
||||
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
|
||||
});
|
||||
@@ -1622,16 +1715,26 @@ namespace gdjs {
|
||||
const messageSender = message.getSender();
|
||||
const sceneNetworkId = messageData.id;
|
||||
|
||||
if (sceneNetworkId !== runtimeScene.networkId) {
|
||||
if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
if (sceneNetworkId !== runtimeScene.networkId) {
|
||||
debugLogger.info(
|
||||
`Received update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
|
||||
);
|
||||
// The scene is not the current scene.
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeScene.updateFromNetworkSyncData(messageData);
|
||||
} else {
|
||||
// If the game is not ready to receive game update messages, we need to save the data for later use.
|
||||
// This can happen when joining a game that is already running.
|
||||
debugLogger.info(
|
||||
`Received update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
|
||||
`Saving scene ${sceneNetworkId} update message for later use.`
|
||||
);
|
||||
// The scene is not the current scene.
|
||||
lastReceivedSceneSyncDataUpdates.store(messageData);
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeScene.updateFromNetworkSyncData(messageData);
|
||||
|
||||
// If we are are the host,
|
||||
// we need to relay the scene update to others except the player who sent the update message.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
@@ -1717,6 +1820,11 @@ namespace gdjs {
|
||||
const handleUpdateGameMessagesToSend = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): void => {
|
||||
if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
// Don't send messages if the multiplayer is not ready.
|
||||
return;
|
||||
}
|
||||
|
||||
const gameNetworkSyncData = runtimeScene.getGame().getNetworkSyncData({
|
||||
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
|
||||
});
|
||||
@@ -1768,7 +1876,15 @@ namespace gdjs {
|
||||
messages.forEach((message) => {
|
||||
const messageData = message.getData();
|
||||
const messageSender = message.getSender();
|
||||
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
|
||||
if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
|
||||
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
|
||||
} else {
|
||||
// If the game is not ready to receive game update messages, we need to save the data for later use.
|
||||
// This can happen when joining a game that is already running.
|
||||
debugLogger.info(`Saving game update message for later use.`);
|
||||
lastReceivedGameSyncDataUpdates.store(messageData);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are are the host,
|
||||
// we need to relay the game update to others except the player who sent the update message.
|
||||
@@ -1785,23 +1901,59 @@ namespace gdjs {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSavedUpdateMessages = (runtimeScene: gdjs.RuntimeScene) => {
|
||||
// Reapply the game saved updates.
|
||||
lastReceivedGameSyncDataUpdates.getUpdates().forEach((messageData) => {
|
||||
debugLogger.info(`Reapplying saved update of game.`);
|
||||
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
|
||||
});
|
||||
// Game updates are always applied properly, so we can clear them.
|
||||
lastReceivedGameSyncDataUpdates.clear();
|
||||
|
||||
// Then reapply the scene saved updates.
|
||||
lastReceivedSceneSyncDataUpdates.getUpdates().forEach((messageData) => {
|
||||
const sceneNetworkId = messageData.id;
|
||||
|
||||
if (sceneNetworkId !== runtimeScene.networkId) {
|
||||
debugLogger.info(
|
||||
`Trying to apply saved update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`
|
||||
);
|
||||
// The scene is not the current scene.
|
||||
return;
|
||||
}
|
||||
|
||||
debugLogger.info(`Reapplying saved update of scene ${sceneNetworkId}.`);
|
||||
|
||||
runtimeScene.updateFromNetworkSyncData(messageData);
|
||||
// We only remove the message if it was successfully applied, so it can be reapplied later,
|
||||
// in case we were not on the right scene.
|
||||
lastReceivedSceneSyncDataUpdates.remove(messageData);
|
||||
});
|
||||
};
|
||||
|
||||
const heartbeatMessageNamePrefix = '#heartbeat';
|
||||
const heartbeastMessageRegex = /#heartbeat#(.+)/;
|
||||
const createHeartbeatMessage = (): {
|
||||
messageName: string;
|
||||
messageData: any;
|
||||
} => {
|
||||
const playersPings = {
|
||||
1: 0, // Player 1 is the host, so we don't need to compute the ping.
|
||||
// Ensure player 1 is correctly set when the first heartbeat is sent.
|
||||
_playersInfo[1] = {
|
||||
ping: 0, // Player 1 is the host, so we don't need to compute the ping.
|
||||
playerId: gdjs.playerAuthentication.getUserId(),
|
||||
username: gdjs.playerAuthentication.getUsername(),
|
||||
};
|
||||
for (const playerNumber in _playersLastHeartbeatInfo) {
|
||||
playersPings[playerNumber] = getPlayerPing(parseInt(playerNumber, 10));
|
||||
for (const playerNumber in _playersInfo) {
|
||||
_playersInfo[playerNumber] = {
|
||||
..._playersInfo[playerNumber],
|
||||
ping: getPlayerPing(parseInt(playerNumber, 10)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,
|
||||
messageData: {
|
||||
now: getTimeNow(), // we send the current time to compute the ping.
|
||||
playersPings,
|
||||
playersInfo: _playersInfo,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1817,11 +1969,16 @@ namespace gdjs {
|
||||
messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,
|
||||
messageData: {
|
||||
sentAt: heartbeatSentAt,
|
||||
playerId: gdjs.playerAuthentication.getUserId(),
|
||||
username: gdjs.playerAuthentication.getUsername(),
|
||||
},
|
||||
};
|
||||
};
|
||||
const hasSentHeartbeatRecently = () => {
|
||||
return getTimeNow() - lastHeartbeatTimestamp < 1000 / heartbeatTickRate;
|
||||
return (
|
||||
!!lastHeartbeatTimestamp &&
|
||||
getTimeNow() - lastHeartbeatTimestamp < 1000 / heartbeatTickRate
|
||||
);
|
||||
};
|
||||
const handleHeartbeatsToSend = () => {
|
||||
// Only host sends heartbeats to all players regularly:
|
||||
@@ -1865,10 +2022,53 @@ namespace gdjs {
|
||||
// Ensure we know who is who.
|
||||
_peerIdToPlayerNumber[messageSender] = playerNumber;
|
||||
|
||||
// If we are not the host, save what the host told us about the pings and respond
|
||||
// with a heartbeat immediately.
|
||||
// If we are not the host, save what the host told us about the other players info
|
||||
// and respond with a heartbeat immediately, informing the host of our playerId and username.
|
||||
if (!gdjs.multiplayer.isPlayerHost()) {
|
||||
_playersPings = messageData.playersPings;
|
||||
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
|
||||
const currentlyKnownPlayerNumbers = Object.keys(
|
||||
_playersInfo
|
||||
).map((playerNumber) => parseInt(playerNumber, 10));
|
||||
const receivedPlayerNumbers = Object.keys(
|
||||
messageData.playersInfo
|
||||
).map((playerNumber) => parseInt(playerNumber, 10));
|
||||
const currentlyKnownPingForCurrentUser =
|
||||
_playersInfo[currentPlayerNumber] &&
|
||||
_playersInfo[currentPlayerNumber].ping;
|
||||
// If there are no players info yet, we're probably just connecting.
|
||||
// This can happen when joining a game that is already running.
|
||||
// Do not handle this case to avoid displaying too many notifications.
|
||||
if (!!currentlyKnownPlayerNumbers.length) {
|
||||
// Look at the players info received to know if there are new players who just connected.
|
||||
const newPlayerNumbers = receivedPlayerNumbers.filter(
|
||||
(playerNumber) =>
|
||||
!currentlyKnownPlayerNumbers.includes(playerNumber) &&
|
||||
playerNumber !== currentPlayerNumber // Do not consider ourselves as a new player.
|
||||
);
|
||||
_playerNumbersWhoJustJoined.push(...newPlayerNumbers);
|
||||
// Or players who have disconnected.
|
||||
const playerNumbersWhoHaveDisconnected = currentlyKnownPlayerNumbers.filter(
|
||||
(playerNumber) => !receivedPlayerNumbers.includes(playerNumber)
|
||||
);
|
||||
_playerNumbersWhoJustLeft.push(
|
||||
...playerNumbersWhoHaveDisconnected
|
||||
);
|
||||
for (const playerNumber of playerNumbersWhoHaveDisconnected) {
|
||||
// Temporarily save the username in another variable to be used for the notification,
|
||||
// as we're deleting its playerInfo just after.
|
||||
_temporaryPlayerNumberToUsername[
|
||||
playerNumber
|
||||
] = getPlayerUsername(playerNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the players info received from the host.
|
||||
// Avoid overwriting the whole object as it can mess up tests that rely on the object reference.
|
||||
cloneObjectWithoutOverwriting({
|
||||
source: messageData.playersInfo,
|
||||
target: _playersInfo,
|
||||
});
|
||||
|
||||
const {
|
||||
messageName: answerMessageName,
|
||||
messageData: answerMessageData,
|
||||
@@ -1876,27 +2076,44 @@ namespace gdjs {
|
||||
heartbeatSentAt: messageData.now, // We send back the time we received, so that the host can compute the ping.
|
||||
});
|
||||
sendDataTo([messageSender], answerMessageName, answerMessageData);
|
||||
// We have received a heartbeat from the host, informing us of our ping,
|
||||
// so we can consider the connection as working.
|
||||
if (
|
||||
_playersInfo[currentPlayerNumber] !== undefined &&
|
||||
_playersInfo[currentPlayerNumber].ping !== undefined
|
||||
) {
|
||||
gdjs.multiplayer.markConnectionAsConnected();
|
||||
if (currentlyKnownPingForCurrentUser === undefined) {
|
||||
// We just connected, let's add ourselves to the list of players who just connected,
|
||||
// for the notification and the events.
|
||||
_playerNumbersWhoJustJoined.push(currentPlayerNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are the host, compute the pings based on:
|
||||
// If we are the host.
|
||||
|
||||
// If this is a new player, we're about to send them their ping, so we can consider them connected.
|
||||
if (!_playersInfo[playerNumber]) {
|
||||
_playerNumbersWhoJustJoined.push(playerNumber);
|
||||
}
|
||||
|
||||
// compute the pings based on:
|
||||
// - the time we received the heartbeat.
|
||||
// - the time the heartbeat was sent.
|
||||
const now = getTimeNow();
|
||||
const heartbeatSentAt = messageData.sentAt;
|
||||
const roundTripTime = Math.round(now - heartbeatSentAt);
|
||||
const playerLastHeartbeatInfo =
|
||||
_playersLastHeartbeatInfo[playerNumber] || {};
|
||||
const playerLastRoundTripTimes =
|
||||
playerLastHeartbeatInfo.lastRoundTripTimes || [];
|
||||
_playersLastRoundTripTimes[playerNumber] || [];
|
||||
playerLastRoundTripTimes.push(roundTripTime);
|
||||
if (playerLastRoundTripTimes.length > 5) {
|
||||
// Keep only the last 5 RTT to compute the average.
|
||||
playerLastRoundTripTimes.shift();
|
||||
}
|
||||
_playersLastHeartbeatInfo[playerNumber] = {
|
||||
lastRoundTripTimes: playerLastRoundTripTimes,
|
||||
};
|
||||
_playersLastRoundTripTimes[playerNumber] = playerLastRoundTripTimes;
|
||||
|
||||
let sum = 0;
|
||||
for (const lastRoundTripTime of playerLastRoundTripTimes) {
|
||||
@@ -1905,19 +2122,30 @@ namespace gdjs {
|
||||
const averagePing = Math.round(
|
||||
sum / playerLastRoundTripTimes.length / 2 // Divide by 2 to get the one way ping.
|
||||
);
|
||||
_playersPings[playerNumber] = averagePing;
|
||||
_playersInfo[playerNumber] = {
|
||||
ping: averagePing,
|
||||
playerId: messageData.playerId,
|
||||
username: messageData.username,
|
||||
};
|
||||
|
||||
// If there are new players, let's resend a heartbeat right away so that everyone is aware of them
|
||||
// on approximately the same frame.
|
||||
if (_playerNumbersWhoJustJoined.length) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
const { messageName, messageData } = createHeartbeatMessage();
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
lastHeartbeatTimestamp = getTimeNow();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getPlayerPing = (playerNumber: number) => {
|
||||
if (playerNumber < 1) {
|
||||
// Player 1 is the host, so we don't need to compute the ping.
|
||||
// Any negative number is invalid.
|
||||
const playerInfo = _playersInfo[playerNumber];
|
||||
if (!playerInfo) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _playersPings[playerNumber] || 0;
|
||||
return playerInfo.ping || 0;
|
||||
};
|
||||
|
||||
const getCurrentPlayerPing = () => {
|
||||
@@ -1928,36 +2156,55 @@ namespace gdjs {
|
||||
const markPlayerAsDisconnected = (playerNumber: number) => {
|
||||
logger.info(`Marking player ${playerNumber} as disconnected.`);
|
||||
_playerNumbersWhoJustLeft.push(playerNumber);
|
||||
// Temporarily save the username in another variable to be used for the notification,
|
||||
// as we're deleting its playerInfo just after.
|
||||
_temporaryPlayerNumberToUsername[playerNumber] = getPlayerUsername(
|
||||
playerNumber
|
||||
);
|
||||
|
||||
// If Player 1 has disconnected, just end the game.
|
||||
if (playerNumber === 1) {
|
||||
logger.info('Host has disconnected, ending the game.');
|
||||
_playersLastHeartbeatInfo = {};
|
||||
_playersPings = {};
|
||||
clearAllMessagesTempData();
|
||||
gdjs.multiplayer.handleLobbyGameEnded();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the player from the list of players.
|
||||
// This will cause the next hearbeat to not include this player
|
||||
// and the others will consider them as disconnected.
|
||||
delete _playersLastHeartbeatInfo[playerNumber];
|
||||
delete _playersPings[playerNumber];
|
||||
clearPlayerTempData(playerNumber);
|
||||
// If we are the host, send a heartbeat right away so that everyone is aware of the disconnection
|
||||
// on approximately the same frame.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
const { messageName, messageData } = createHeartbeatMessage();
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
lastHeartbeatTimestamp = getTimeNow();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnectedPeers = (runtimeScene: RuntimeScene) => {
|
||||
const getPlayerUsername = (playerNumber: number) => {
|
||||
return (
|
||||
(_playersInfo[playerNumber] || {}).username ||
|
||||
_temporaryPlayerNumberToUsername[playerNumber] ||
|
||||
`Player ${playerNumber}`
|
||||
);
|
||||
};
|
||||
|
||||
const getPlayerId = (playerNumber: number) => {
|
||||
return (_playersInfo[playerNumber] || {}).playerId || '';
|
||||
};
|
||||
|
||||
const handleJustDisconnectedPeers = (runtimeScene: RuntimeScene) => {
|
||||
// If the game is not running, we don't need to handle disconnected peers.
|
||||
if (!gdjs.multiplayer.isLobbyGameRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Players can disconnect if the P2P connection disconnects
|
||||
// or if we don't receive heartbeats for a while.
|
||||
const disconnectedPlayerNumbers: number[] = [];
|
||||
// We rely on the p2p helper to know who has disconnected.
|
||||
const justDisconnectedPlayerNumbers: number[] = [];
|
||||
|
||||
const disconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();
|
||||
if (disconnectedPeers.length) {
|
||||
for (const disconnectedPeer of disconnectedPeers) {
|
||||
const justDisconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();
|
||||
if (justDisconnectedPeers.length) {
|
||||
for (const disconnectedPeer of justDisconnectedPeers) {
|
||||
const disconnectedPlayerNumber =
|
||||
_peerIdToPlayerNumber[disconnectedPeer];
|
||||
if (!disconnectedPlayerNumber) {
|
||||
@@ -1965,11 +2212,11 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
logger.info(`Player ${disconnectedPlayerNumber} has disconnected.`);
|
||||
disconnectedPlayerNumbers.push(disconnectedPlayerNumber);
|
||||
justDisconnectedPlayerNumbers.push(disconnectedPlayerNumber);
|
||||
}
|
||||
}
|
||||
|
||||
for (const playerNumber of disconnectedPlayerNumbers) {
|
||||
for (const playerNumber of justDisconnectedPlayerNumbers) {
|
||||
// When a player disconnects, as the host, we look at all the instances
|
||||
// they own and decide what to do with them.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
@@ -2000,29 +2247,60 @@ namespace gdjs {
|
||||
}
|
||||
};
|
||||
|
||||
const clearDisconnectedPeers = () => {
|
||||
_playerNumbersWhoJustLeft = [];
|
||||
};
|
||||
|
||||
const hasAnyPlayerLeft = () => {
|
||||
const hasAnyPlayerJustLeft = (): boolean => {
|
||||
return _playerNumbersWhoJustLeft.length > 0;
|
||||
};
|
||||
|
||||
const hasPlayerLeft = (playerNumber: number) => {
|
||||
const hasPlayerJustLeft = (playerNumber: number): boolean => {
|
||||
return _playerNumbersWhoJustLeft.includes(playerNumber);
|
||||
};
|
||||
|
||||
const getDisconnectedPlayers = () => {
|
||||
const getPlayersWhoJustLeft = (): number[] => {
|
||||
return _playerNumbersWhoJustLeft;
|
||||
};
|
||||
|
||||
const getNumberOfConnectedPlayers = () => {
|
||||
// Look at the player pings as a way to know how many players are in the lobby.
|
||||
return Object.keys(_playersPings).length;
|
||||
const getLatestPlayerWhoJustLeft = (): number => {
|
||||
return _playerNumbersWhoJustLeft[0] || 0;
|
||||
};
|
||||
const removePlayerWhoJustLeft = (): void => {
|
||||
// Avoid using shift for test purposes, as it modifies the reference.
|
||||
const playerNumberWhoLeft = _playerNumbersWhoJustLeft[0];
|
||||
if (playerNumberWhoLeft !== undefined) {
|
||||
_playerNumbersWhoJustLeft = _playerNumbersWhoJustLeft.slice(1);
|
||||
delete _temporaryPlayerNumberToUsername[playerNumberWhoLeft];
|
||||
}
|
||||
};
|
||||
|
||||
const hasAnyPlayerJustJoined = () => {
|
||||
return _playerNumbersWhoJustJoined.length > 0;
|
||||
};
|
||||
const hasPlayerJustJoined = (playerNumber: number): boolean => {
|
||||
return _playerNumbersWhoJustJoined.includes(playerNumber);
|
||||
};
|
||||
const getPlayersWhoJustJoined = () => {
|
||||
return _playerNumbersWhoJustJoined;
|
||||
};
|
||||
const getLatestPlayerWhoJustJoined = (): number => {
|
||||
return _playerNumbersWhoJustJoined[0] || 0;
|
||||
};
|
||||
const removePlayerWhoJustJoined = (): void => {
|
||||
// Avoid using shift for test purposes, as it modifies the reference.
|
||||
const playerNumberWhoJoined = _playerNumbersWhoJustJoined[0];
|
||||
if (playerNumberWhoJoined !== undefined) {
|
||||
_playerNumbersWhoJustJoined = _playerNumbersWhoJustJoined.slice(1);
|
||||
}
|
||||
};
|
||||
|
||||
const getConnectedPlayers = () => {
|
||||
return Object.keys(_playersInfo).map((playerNumber) => ({
|
||||
playerNumber: parseInt(playerNumber, 10),
|
||||
playerId: _playersInfo[playerNumber].playerId,
|
||||
}));
|
||||
};
|
||||
const getNumberOfConnectedPlayers = () => {
|
||||
// Look at the player info as a way to know how many players are in the lobby.
|
||||
// This object is updated when heartbeats are sent and received.
|
||||
return Object.keys(_playersInfo).length;
|
||||
};
|
||||
const isPlayerConnected = (playerNumber: number) => {
|
||||
return _playersPings[playerNumber] !== undefined;
|
||||
return _playersInfo[playerNumber] !== undefined;
|
||||
};
|
||||
|
||||
const endGameMessageName = '#endGame';
|
||||
@@ -2066,20 +2344,34 @@ namespace gdjs {
|
||||
|
||||
// If the message is received more than 1 time, we just ignore it and end the game.
|
||||
|
||||
_playersLastHeartbeatInfo = {};
|
||||
_playersPings = {};
|
||||
clearAllMessagesTempData();
|
||||
gdjs.multiplayer.handleLobbyGameEnded();
|
||||
};
|
||||
|
||||
const updatePlayersPingsForTests = (playersPings) => {
|
||||
_playersPings = playersPings;
|
||||
const clearAllMessagesTempData = () => {
|
||||
_playersLastRoundTripTimes = {};
|
||||
_playersInfo = {};
|
||||
lastReceivedGameSyncDataUpdates.clear();
|
||||
lastReceivedSceneSyncDataUpdates.clear();
|
||||
processedCustomMessagesCache.clear();
|
||||
_playerNumbersWhoJustLeft = [];
|
||||
_playerNumbersWhoJustJoined = [];
|
||||
expectedMessageAcknowledgements = {};
|
||||
_lastClockReceivedByInstanceByScene = {};
|
||||
};
|
||||
|
||||
const clearPlayerTempData = (playerNumber: number) => {
|
||||
// Remove the player from the list of players.
|
||||
// This will cause the next hearbeat to not include this player
|
||||
// and the others will consider them as disconnected.
|
||||
delete _playersLastRoundTripTimes[playerNumber];
|
||||
delete _playersInfo[playerNumber];
|
||||
};
|
||||
|
||||
return {
|
||||
sendDataTo,
|
||||
// Acks.
|
||||
addExpectedMessageAcknowledgement,
|
||||
clearExpectedMessageAcknowledgements,
|
||||
handleAcknowledgeMessagesReceived,
|
||||
resendClearOrCancelAcknowledgedMessages,
|
||||
// Instance ownership.
|
||||
@@ -2113,23 +2405,36 @@ namespace gdjs {
|
||||
createUpdateGameMessage,
|
||||
handleUpdateGameMessagesToSend,
|
||||
handleUpdateGameMessagesReceived,
|
||||
handleSavedUpdateMessages,
|
||||
// Heartbeats.
|
||||
handleHeartbeatsToSend,
|
||||
handleHeartbeatsReceived,
|
||||
// Connection/Disonnection.
|
||||
// Pings & usernames.
|
||||
getPlayerPing,
|
||||
getCurrentPlayerPing,
|
||||
updatePlayersPingsForTests,
|
||||
handleDisconnectedPeers,
|
||||
clearDisconnectedPeers,
|
||||
hasAnyPlayerLeft,
|
||||
hasPlayerLeft,
|
||||
getDisconnectedPlayers,
|
||||
getPlayerUsername,
|
||||
getPlayerId,
|
||||
// Connected players.
|
||||
handleJustDisconnectedPeers,
|
||||
getConnectedPlayers,
|
||||
getNumberOfConnectedPlayers,
|
||||
isPlayerConnected,
|
||||
// Leaving players.
|
||||
hasAnyPlayerJustLeft,
|
||||
hasPlayerJustLeft,
|
||||
getPlayersWhoJustLeft,
|
||||
getLatestPlayerWhoJustLeft,
|
||||
removePlayerWhoJustLeft,
|
||||
// Joining players.
|
||||
hasAnyPlayerJustJoined,
|
||||
hasPlayerJustJoined,
|
||||
getPlayersWhoJustJoined,
|
||||
getLatestPlayerWhoJustJoined,
|
||||
removePlayerWhoJustJoined,
|
||||
// End game.
|
||||
sendEndGameMessage,
|
||||
handleEndGameMessages,
|
||||
clearAllMessagesTempData,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -208,7 +208,10 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const handleChangeVariableOwnerMessagesToSend = function () {
|
||||
if (!gdjs.multiplayer.isLobbyGameRunning()) {
|
||||
if (
|
||||
!gdjs.multiplayer.isLobbyGameRunning() ||
|
||||
!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,8 @@ namespace gdjs {
|
||||
|
||||
let canLobbyBeClosed = true;
|
||||
|
||||
const notificationContainerIds: string[] = [];
|
||||
|
||||
export const getDomElementContainer = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): HTMLDivElement | null => {
|
||||
@@ -387,7 +389,7 @@ namespace gdjs {
|
||||
// to allow the player to leave the lobby.
|
||||
setTimeout(() => {
|
||||
closeContainer.style.visibility = 'inherit';
|
||||
}, 5000);
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -398,7 +400,6 @@ namespace gdjs {
|
||||
) {
|
||||
showNotification(
|
||||
runtimeScene,
|
||||
'error-notification',
|
||||
'An error occurred while displaying the game lobbies, please try again.',
|
||||
'error'
|
||||
);
|
||||
@@ -411,37 +412,82 @@ namespace gdjs {
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
playerName: string
|
||||
) {
|
||||
showNotification(
|
||||
runtimeScene,
|
||||
'player-left-notification',
|
||||
`${playerName} has left the game.`,
|
||||
'warning'
|
||||
);
|
||||
showNotification(runtimeScene, `${playerName} left.`, 'warning');
|
||||
};
|
||||
|
||||
/**
|
||||
* Create, display, and hide a notification when a player leaves the game.
|
||||
* Create, display, and hide a notification when a player joins the game.
|
||||
*/
|
||||
export const displayPlayerJoinedNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
playerName: string
|
||||
) {
|
||||
showNotification(runtimeScene, `${playerName} joined.`, 'success');
|
||||
};
|
||||
|
||||
/**
|
||||
* Create, display, and hide a notification when an error happens on connection.
|
||||
*/
|
||||
export const displayConnectionErrorNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
showNotification(
|
||||
runtimeScene,
|
||||
'connection-error-notification',
|
||||
'Could not connect to other players.',
|
||||
'error'
|
||||
);
|
||||
};
|
||||
|
||||
const removeNotificationAndShiftOthers = function (
|
||||
notificationContainerId: string
|
||||
) {
|
||||
const notification = document.getElementById(notificationContainerId);
|
||||
if (!notification) {
|
||||
logger.error('Notification not found.');
|
||||
return;
|
||||
}
|
||||
const index = notificationContainerIds.indexOf(notificationContainerId);
|
||||
if (index !== -1) {
|
||||
notificationContainerIds.splice(index, 1);
|
||||
}
|
||||
notification.remove();
|
||||
|
||||
// Shift the other notifications up.
|
||||
for (let i = 0; i < notificationContainerIds.length; i++) {
|
||||
const notification = document.getElementById(
|
||||
notificationContainerIds[i]
|
||||
);
|
||||
if (!notification) {
|
||||
logger.error('Notification not found.');
|
||||
continue;
|
||||
}
|
||||
notification.style.top = `${12 + i * 32}px`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to show a notification to the user, that disappears automatically.
|
||||
*/
|
||||
export const showNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
id: string,
|
||||
content: string,
|
||||
type: 'success' | 'warning' | 'error'
|
||||
) {
|
||||
// When we show a notification, we add it below the other ones.
|
||||
// We also remove the oldest one if there are too many > 5.
|
||||
if (notificationContainerIds.length > 5) {
|
||||
const oldestNotificationId = notificationContainerIds.shift();
|
||||
if (!oldestNotificationId) {
|
||||
logger.error('No oldest notification ID found.');
|
||||
return;
|
||||
}
|
||||
|
||||
removeNotificationAndShiftOthers(oldestNotificationId);
|
||||
}
|
||||
|
||||
// We generate a random ID for the notification, so they can stack.
|
||||
const id = `notification-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
const domContainer = runtimeScene
|
||||
.getGame()
|
||||
.getRenderer()
|
||||
@@ -461,7 +507,8 @@ namespace gdjs {
|
||||
: type === 'warning'
|
||||
? '#FFA500'
|
||||
: '#FF0000';
|
||||
divContainer.style.top = '12px';
|
||||
// Space the notifications vertically, based on how many there are.
|
||||
divContainer.style.top = `${12 + notificationContainerIds.length * 32}px`;
|
||||
divContainer.style.right = '16px';
|
||||
divContainer.style.padding = '6px 32px 6px 6px';
|
||||
// Use zIndex 1 to make sure it is below the iframe.
|
||||
@@ -496,8 +543,9 @@ namespace gdjs {
|
||||
loggedText.style.margin = '0px';
|
||||
|
||||
divContainer.appendChild(loggedText);
|
||||
|
||||
domContainer.appendChild(divContainer);
|
||||
notificationContainerIds.push(id);
|
||||
|
||||
const animationTime = 700;
|
||||
const notificationTime = 5000;
|
||||
setTimeout(() => {
|
||||
@@ -518,7 +566,7 @@ namespace gdjs {
|
||||
}, notificationTime);
|
||||
// Use timeout because onanimationend listener does not work.
|
||||
setTimeout(() => {
|
||||
divContainer.remove();
|
||||
removeNotificationAndShiftOthers(id);
|
||||
}, notificationTime + animationTime);
|
||||
};
|
||||
|
||||
|
@@ -92,9 +92,14 @@ namespace gdjs {
|
||||
// To handle this case and avoid having an object not synchronized, we set a timeout to destroy the object
|
||||
// if it has not been assigned a networkId after a short delay.
|
||||
this._destroyInstanceTimeoutId = setTimeout(() => {
|
||||
if (!owner.networkId && gdjs.multiplayer.isLobbyGameRunning()) {
|
||||
const sceneNetworkId = this.owner.getRuntimeScene().networkId;
|
||||
if (
|
||||
!owner.networkId &&
|
||||
gdjs.multiplayer.isLobbyGameRunning() &&
|
||||
sceneNetworkId
|
||||
) {
|
||||
debugLogger.info(
|
||||
`Lobby game is running and object ${owner.getName()} has not been assigned a networkId after a short delay, destroying it.`
|
||||
`Lobby game is running on a synced scene and object ${owner.getName()} has not been assigned a networkId after a short delay, destroying it.`
|
||||
);
|
||||
owner.deleteFromScene(instanceContainer);
|
||||
}
|
||||
@@ -162,12 +167,9 @@ namespace gdjs {
|
||||
|
||||
private _getOrCreateInstanceNetworkId() {
|
||||
if (!this.owner.networkId) {
|
||||
// no ID for this object, let's generate one so it can be identified by other players.
|
||||
// Either use the persistentUuid if it exists, or generate a new one.
|
||||
// No ID for this object, let's generate one so it can be identified by other players.
|
||||
// Keep it short to avoid sending too much data.
|
||||
const newID = this.owner.persistentUuid
|
||||
? this.owner.persistentUuid.substring(0, 8)
|
||||
: gdjs.makeUuid().substring(0, 8);
|
||||
const newID = gdjs.makeUuid().substring(0, 8);
|
||||
this.owner.networkId = newID;
|
||||
}
|
||||
|
||||
@@ -250,7 +252,8 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
// If game is running and object belong to a player that is not connected, destroy the object.
|
||||
// If game is running and the object belongs to a player who is not connected, destroy the object.
|
||||
// As the game may create objects before the lobby game starts, we don't want to destroy them if it's not running.
|
||||
if (
|
||||
this.playerNumber !== 0 && // Host is always connected.
|
||||
!gdjs.multiplayerMessageManager.isPlayerConnected(this.playerNumber)
|
||||
@@ -405,8 +408,7 @@ namespace gdjs {
|
||||
this._destroyInstanceTimeoutId = null;
|
||||
}
|
||||
|
||||
// If the lobby game is not running, do not try to destroy the object,
|
||||
// as the game may create objects before the lobby game starts, and we don't want to destroy them.
|
||||
// If the lobby game is not running, no need to send a message to destroy the object.
|
||||
if (!gdjs.multiplayer.isLobbyGameRunning()) {
|
||||
return;
|
||||
}
|
||||
|
@@ -1,16 +1,12 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Multiplayer');
|
||||
|
||||
type Lobby = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
players: { playerId: string; status: string }[];
|
||||
};
|
||||
export namespace multiplayer {
|
||||
/** Set to true in testing to avoid relying on the multiplayer extension. */
|
||||
export let disableMultiplayerForTesting = false;
|
||||
|
||||
export let _isReadyToSendOrReceiveGameUpdateMessages = false;
|
||||
|
||||
let _isGameRegistered: boolean | null = null;
|
||||
let _isCheckingIfGameIsRegistered = false;
|
||||
let _isWaitingForLogin = false;
|
||||
@@ -20,8 +16,6 @@ namespace gdjs {
|
||||
let _hasLobbyGameJustEnded = false;
|
||||
let _lobbyId: string | null = null;
|
||||
let _connectionId: string | null = null;
|
||||
export let _lobby: Lobby | null = null;
|
||||
let _playerPublicProfiles: { id: string; username?: string }[] = [];
|
||||
|
||||
// Communication methods.
|
||||
let _lobbiesMessageCallback: ((event: MessageEvent) => void) | null = null;
|
||||
@@ -46,6 +40,11 @@ namespace gdjs {
|
||||
|
||||
if (disableMultiplayerForTesting) return;
|
||||
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsToSend();
|
||||
gdjs.multiplayerMessageManager.handleJustDisconnectedPeers(
|
||||
runtimeScene
|
||||
);
|
||||
|
||||
gdjs.multiplayerMessageManager.handleChangeInstanceOwnerMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
@@ -60,14 +59,21 @@ namespace gdjs {
|
||||
gdjs.multiplayerMessageManager.handleChangeVariableOwnerMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
// In case we're joining an existing lobby, it's possible we haven't
|
||||
// fully caught up with the game state yet, especially if a scene is loading.
|
||||
// We look at them every frame, from the moment the lobby has started,
|
||||
// to ensure we don't miss any.
|
||||
if (_isLobbyGameRunning) {
|
||||
gdjs.multiplayerMessageManager.handleSavedUpdateMessages(
|
||||
runtimeScene
|
||||
);
|
||||
}
|
||||
gdjs.multiplayerMessageManager.handleUpdateGameMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
gdjs.multiplayerMessageManager.handleUpdateSceneMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsToSend();
|
||||
gdjs.multiplayerMessageManager.handleDisconnectedPeers(runtimeScene);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -75,6 +81,13 @@ namespace gdjs {
|
||||
(runtimeScene: gdjs.RuntimeScene) => {
|
||||
if (disableMultiplayerForTesting) return;
|
||||
|
||||
// Handle joining and leaving players to show notifications accordingly.
|
||||
handleLeavingPlayer(runtimeScene);
|
||||
handleJoiningPlayer(runtimeScene);
|
||||
|
||||
// Then look at the heartbeats received to know if a new player has joined/left.
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
|
||||
|
||||
gdjs.multiplayerMessageManager.handleDestroyInstanceMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
@@ -85,9 +98,6 @@ namespace gdjs {
|
||||
gdjs.multiplayerMessageManager.handleUpdateSceneMessagesToSend(
|
||||
runtimeScene
|
||||
);
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
|
||||
handleLeavingPlayer(runtimeScene);
|
||||
gdjs.multiplayerMessageManager.clearDisconnectedPeers();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -144,6 +154,9 @@ namespace gdjs {
|
||||
if (playerToken) {
|
||||
url.searchParams.set('playerToken', playerToken);
|
||||
}
|
||||
// Increment this value when a new feature is introduced so we can
|
||||
// adapt the interface of the lobbies.
|
||||
url.searchParams.set('multiplayerVersion', '2');
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
@@ -156,6 +169,9 @@ namespace gdjs {
|
||||
|
||||
export const isLobbyGameRunning = () => _isLobbyGameRunning;
|
||||
|
||||
export const isReadyToSendOrReceiveGameUpdateMessages = () =>
|
||||
_isReadyToSendOrReceiveGameUpdateMessages;
|
||||
|
||||
/**
|
||||
* Returns true if the game has just ended,
|
||||
* useful to switch back to to the main menu.
|
||||
@@ -166,17 +182,16 @@ namespace gdjs {
|
||||
* Returns the number of players in the lobby.
|
||||
*/
|
||||
export const getPlayersInLobbyCount = () => {
|
||||
// If the game has not started yet, look at the lobby.
|
||||
if (!_isLobbyGameRunning && _lobby) {
|
||||
return _lobby.players.length;
|
||||
}
|
||||
// Whether the lobby game has started or not, the number of players in the lobby
|
||||
// is the number of connected players.
|
||||
return gdjs.multiplayerMessageManager.getNumberOfConnectedPlayers();
|
||||
};
|
||||
|
||||
// If the game has started, look at the pings received from the players.
|
||||
if (_isLobbyGameRunning) {
|
||||
return gdjs.multiplayerMessageManager.getNumberOfConnectedPlayers();
|
||||
}
|
||||
|
||||
return 0;
|
||||
/**
|
||||
* Returns true if the player at this position is connected to the lobby.
|
||||
*/
|
||||
export const isPlayerConnected = (playerNumber: number) => {
|
||||
return gdjs.multiplayerMessageManager.isPlayerConnected(playerNumber);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -195,38 +210,12 @@ namespace gdjs {
|
||||
return playerNumber === 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the player ID of the player at the given number in the lobby.
|
||||
* The number is shifted by one, so that the first player has number 1.
|
||||
*/
|
||||
const getPlayerId = (playerNumber: number) => {
|
||||
if (!_lobby) {
|
||||
return '';
|
||||
}
|
||||
const index = playerNumber - 1;
|
||||
if (index < 0 || index >= _lobby.players.length) {
|
||||
return '';
|
||||
}
|
||||
return _lobby.players[index].playerId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the player username at the given number in the lobby.
|
||||
* The number is shifted by one, so that the first player has number 1.
|
||||
*/
|
||||
export const getPlayerUsername = (playerNumber: number) => {
|
||||
const playerId = getPlayerId(playerNumber);
|
||||
if (!playerId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const playerPublicProfile = _playerPublicProfiles.find(
|
||||
(profile) => profile.id === playerId
|
||||
);
|
||||
|
||||
return playerPublicProfile
|
||||
? playerPublicProfile.username
|
||||
: `Player ${playerNumber}`;
|
||||
return gdjs.multiplayerMessageManager.getPlayerUsername(playerNumber);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -238,30 +227,39 @@ 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);
|
||||
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();
|
||||
|
||||
if (!playerLeftId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playerLeftPublicProfile = _playerPublicProfiles.find(
|
||||
(profile) => profile.id === playerLeftId
|
||||
);
|
||||
|
||||
if (playerLeftPublicProfile) {
|
||||
gdjs.multiplayerComponents.displayPlayerLeftNotification(
|
||||
runtimeScene,
|
||||
(playerLeftPublicProfile && playerLeftPublicProfile.username) ||
|
||||
'Player'
|
||||
);
|
||||
}
|
||||
}
|
||||
// When a player leaves, we send a heartbeat to the backend so that they're aware of the players in the lobby.
|
||||
// Do not await as we want don't want to block the execution of the of the rest of the logic.
|
||||
sendHeartbeatToBackend();
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the game is registered, false otherwise.
|
||||
* Useful to display a message to the user to register the game before logging in.
|
||||
@@ -298,61 +296,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 +311,6 @@ namespace gdjs {
|
||||
_connectionId = null;
|
||||
playerNumber = null;
|
||||
_lobbyId = null;
|
||||
_lobby = null;
|
||||
_websocket = null;
|
||||
}
|
||||
|
||||
@@ -460,15 +403,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 +439,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 +570,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 +598,6 @@ namespace gdjs {
|
||||
);
|
||||
|
||||
if (!lobbiesIframe || !lobbiesIframe.contentWindow) {
|
||||
logger.info('The lobbies iframe is not opened, not sending message.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -725,6 +650,55 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
const sendHeartbeatToBackend = async function () {
|
||||
const gameId = gdjs.projectData.properties.projectUuid;
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
|
||||
if (!gameId || !playerId || !playerToken || !_lobbyId) {
|
||||
logger.error(
|
||||
'Cannot keep the lobby playing without the game ID or player ID.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootApi = isUsingGDevelopDevelopmentEnvironment
|
||||
? 'https://api-dev.gdevelop.io'
|
||||
: 'https://api.gdevelop.io';
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
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();
|
||||
try {
|
||||
await fetch(heartbeatUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
players,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error while sending heartbeat, retrying:', error);
|
||||
try {
|
||||
await fetch(heartbeatUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
players,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error while sending heartbeat a second time. Giving up:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When the game receives the information that the game has started, close the
|
||||
* lobbies window, focus on the game, and set the flag to true.
|
||||
@@ -745,7 +719,7 @@ namespace gdjs {
|
||||
runtimeScene
|
||||
);
|
||||
// Do as if the player left the lobby.
|
||||
handleLobbyLeaveEvent();
|
||||
handleLeaveLobbyEvent();
|
||||
removeLobbiesContainer(runtimeScene);
|
||||
focusOnGame(runtimeScene);
|
||||
return;
|
||||
@@ -753,36 +727,16 @@ namespace gdjs {
|
||||
|
||||
// If we are the host, start pinging the backend to let it know the lobby is running.
|
||||
if (isPlayerHost()) {
|
||||
const gameId = gdjs.projectData.properties.projectUuid;
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
|
||||
if (!gameId || !playerId || !playerToken || !_lobbyId) {
|
||||
logger.error(
|
||||
'Cannot keep the lobby playing without the game ID or player ID.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_lobbyHeartbeatInterval = setInterval(async () => {
|
||||
const rootApi = isUsingGDevelopDevelopmentEnvironment
|
||||
? 'https://api-dev.gdevelop.io'
|
||||
: 'https://api.gdevelop.io';
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
let heartbeatUrl = `${rootApi}/play/game/${gameId}/public-lobby/${_lobbyId}/action/heartbeat`;
|
||||
headers['Authorization'] = `player-game-token ${playerToken}`;
|
||||
heartbeatUrl += `?playerId=${playerId}`;
|
||||
await fetch(heartbeatUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
});
|
||||
await sendHeartbeatToBackend();
|
||||
}, 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 +756,8 @@ namespace gdjs {
|
||||
_hasLobbyGameJustEnded = true;
|
||||
_isLobbyGameRunning = false;
|
||||
_lobbyId = null;
|
||||
_lobby = null;
|
||||
playerNumber = null;
|
||||
_isReadyToSendOrReceiveGameUpdateMessages = false;
|
||||
if (_lobbyHeartbeatInterval) {
|
||||
clearInterval(_lobbyHeartbeatInterval);
|
||||
}
|
||||
@@ -812,15 +766,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 +803,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 +824,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 +838,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 +944,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 +1000,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 +1254,9 @@ namespace gdjs {
|
||||
/**
|
||||
* Action to allow the player to leave the lobby in-game.
|
||||
*/
|
||||
export const leaveGameLobby = async (runtimeScene: gdjs.RuntimeScene) => {
|
||||
export const leaveGameLobby = async () => {
|
||||
// Handle the case where the game has not started yet, so the player is in the lobby.
|
||||
handleLobbyLeaveEvent();
|
||||
handleLeaveLobbyEvent();
|
||||
// Handle the case where the game has started, so the player is in the game and connected to other players.
|
||||
handleLobbyGameEnded();
|
||||
};
|
||||
|
@@ -254,6 +254,11 @@ namespace gdjs {
|
||||
connection.on('close', () => {
|
||||
_onDisconnect(connection.peer);
|
||||
});
|
||||
connection.on('iceStateChanged', (state) => {
|
||||
if (state === 'disconnected') {
|
||||
_onDisconnect(connection.peer);
|
||||
}
|
||||
});
|
||||
|
||||
// Regularly check for disconnection as the built in way is not reliable.
|
||||
(function disconnectChecker() {
|
||||
@@ -428,7 +433,7 @@ namespace gdjs {
|
||||
export const isReady = () => ready;
|
||||
|
||||
/**
|
||||
* Return any disconnected peers.
|
||||
* Return peers that have disconnected in the frame.
|
||||
*/
|
||||
export const getJustDisconnectedPeers = () => justDisconnectedPeers;
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -340,7 +340,7 @@ namespace gdjs {
|
||||
setAdditiveRendering(enabled: boolean): void {
|
||||
// Access private members of the behavior to apply changes right away.
|
||||
const behavior: any = this.emitter.getBehavior('blendMode');
|
||||
behavior.blendMode = enabled ? 'ADD' : 'NORMAL';
|
||||
behavior.value = enabled ? 'ADD' : 'NORMAL';
|
||||
}
|
||||
|
||||
setAlpha(alpha1: number, alpha2: number): void {
|
||||
|
@@ -1395,6 +1395,7 @@ module.exports = {
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('getLinearVelocityLength');
|
||||
|
||||
// Deprecated
|
||||
aut
|
||||
.addCondition(
|
||||
'LinearVelocityAngle',
|
||||
@@ -1413,9 +1414,27 @@ module.exports = {
|
||||
_('Angle to compare to (in degrees)')
|
||||
)
|
||||
)
|
||||
.setHidden()
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('getLinearVelocityAngle');
|
||||
|
||||
aut
|
||||
.addScopedCondition(
|
||||
'IsLinearVelocityAngleAround',
|
||||
_('Linear velocity angle'),
|
||||
_('Compare the linear velocity angle of the object.'),
|
||||
_('Angle of movement of _PARAM0_ is _PARAM2_ ± _PARAM3_°'),
|
||||
_('Velocity'),
|
||||
'res/physics32.png',
|
||||
'res/physics32.png'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Angle (in degrees)'))
|
||||
.addParameter('expression', _('Tolerance (in degrees)'))
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('isLinearVelocityAngleAround');
|
||||
|
||||
aut
|
||||
.addAction(
|
||||
'LinearVelocityAngle',
|
||||
|
@@ -1528,6 +1528,17 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
isLinearVelocityAngleAround(degreeAngle: float, tolerance: float) {
|
||||
return (
|
||||
Math.abs(
|
||||
gdjs.evtTools.common.angleDifference(
|
||||
this.getLinearVelocityAngle(),
|
||||
degreeAngle
|
||||
)
|
||||
) <= tolerance
|
||||
);
|
||||
}
|
||||
|
||||
getAngularVelocity(): float {
|
||||
// If there is no body, set a new one
|
||||
if (this._body === null) {
|
||||
|
@@ -7,7 +7,8 @@ namespace gdjs {
|
||||
* Returned by _findHighestFloorAndMoveOnTop
|
||||
*/
|
||||
type PlatformSearchResult = {
|
||||
highestGround: gdjs.PlatformRuntimeBehavior | null;
|
||||
highestGroundPlatform: gdjs.PlatformRuntimeBehavior | null;
|
||||
highestGroundPolygon: gdjs.Polygon | null;
|
||||
isCollidingAnyPlatform: boolean;
|
||||
};
|
||||
|
||||
@@ -74,7 +75,8 @@ namespace gdjs {
|
||||
* Returned by _findHighestFloorAndMoveOnTop
|
||||
*/
|
||||
private static readonly _platformSearchResult: PlatformSearchResult = {
|
||||
highestGround: null,
|
||||
highestGroundPlatform: null,
|
||||
highestGroundPolygon: null,
|
||||
isCollidingAnyPlatform: false,
|
||||
};
|
||||
|
||||
@@ -612,7 +614,8 @@ namespace gdjs {
|
||||
this._potentialCollidingObjects,
|
||||
floorPlatformId,
|
||||
/*excludeJumpthrus=*/
|
||||
true
|
||||
true,
|
||||
this._onFloor.getFloorPolygon()
|
||||
)
|
||||
) {
|
||||
if (
|
||||
@@ -648,12 +651,12 @@ namespace gdjs {
|
||||
// This is to be consistent on all floor collision.
|
||||
// The object will land right on floor.
|
||||
|
||||
const { highestGround } = this._findHighestFloorAndMoveOnTop(
|
||||
const { highestGroundPlatform } = this._findHighestFloorAndMoveOnTop(
|
||||
this._potentialCollidingObjects,
|
||||
0,
|
||||
this._requestedDeltaY
|
||||
);
|
||||
if (!highestGround) {
|
||||
if (!highestGroundPlatform) {
|
||||
object.setY(object.getY() + this._requestedDeltaY);
|
||||
}
|
||||
} else {
|
||||
@@ -706,10 +709,13 @@ namespace gdjs {
|
||||
this._falling.enter(from);
|
||||
}
|
||||
|
||||
_setOnFloor(collidingPlatform: gdjs.PlatformRuntimeBehavior) {
|
||||
_setOnFloor(
|
||||
collidingPlatform: gdjs.PlatformRuntimeBehavior,
|
||||
floorPolygon: gdjs.Polygon
|
||||
) {
|
||||
this._state.leave();
|
||||
this._state = this._onFloor;
|
||||
this._onFloor.enter(collidingPlatform);
|
||||
this._onFloor.enter(collidingPlatform, floorPolygon);
|
||||
}
|
||||
|
||||
private _setJumping() {
|
||||
@@ -803,22 +809,28 @@ namespace gdjs {
|
||||
|
||||
// The interval could be smaller.
|
||||
// It's just for rounding errors.
|
||||
const { highestGround } = this._findHighestFloorAndMoveOnTop(
|
||||
const {
|
||||
highestGroundPlatform,
|
||||
highestGroundPolygon,
|
||||
} = this._findHighestFloorAndMoveOnTop(
|
||||
this._potentialCollidingObjects,
|
||||
-1,
|
||||
1
|
||||
);
|
||||
// don't fall if GrabbingPlatform or OnLadder
|
||||
if (this._state === this._onFloor) {
|
||||
if (!highestGround) {
|
||||
if (!highestGroundPlatform || !highestGroundPolygon) {
|
||||
this._setFalling();
|
||||
} else if (highestGround === this._onFloor.getFloorPlatform()) {
|
||||
} else if (
|
||||
highestGroundPlatform === this._onFloor.getFloorPlatform() &&
|
||||
highestGroundPolygon === this._onFloor.getFloorPolygon()
|
||||
) {
|
||||
this._onFloor.updateFloorPosition();
|
||||
} else {
|
||||
this._setOnFloor(highestGround);
|
||||
this._setOnFloor(highestGroundPlatform, highestGroundPolygon);
|
||||
}
|
||||
} else if (highestGround && canLand) {
|
||||
this._setOnFloor(highestGround);
|
||||
} else if (highestGroundPlatform && highestGroundPolygon && canLand) {
|
||||
this._setOnFloor(highestGroundPlatform, highestGroundPolygon);
|
||||
} else {
|
||||
// The object can't land.
|
||||
object.setY(oldY);
|
||||
@@ -930,13 +942,15 @@ namespace gdjs {
|
||||
*/
|
||||
_isCollidingWithOneOf(
|
||||
candidates: gdjs.PlatformRuntimeBehavior[],
|
||||
exceptThisOne?: number | null,
|
||||
excludeJumpThrus?: boolean
|
||||
ignoredPlatformId?: number | null,
|
||||
excludeJumpThrus?: boolean,
|
||||
ignoredPolygon?: gdjs.Polygon | null
|
||||
) {
|
||||
excludeJumpThrus = !!excludeJumpThrus;
|
||||
for (let i = 0; i < candidates.length; ++i) {
|
||||
const platform = candidates[i];
|
||||
if (platform.owner.id === exceptThisOne) {
|
||||
const isPlatformIgnored = platform.owner.id === ignoredPlatformId;
|
||||
if (isPlatformIgnored && !ignoredPolygon) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
@@ -952,9 +966,10 @@ namespace gdjs {
|
||||
}
|
||||
if (
|
||||
gdjs.RuntimeObject.collisionTest(
|
||||
this.owner,
|
||||
platform.owner,
|
||||
this._ignoreTouchingEdges
|
||||
this.owner,
|
||||
this._ignoreTouchingEdges,
|
||||
isPlatformIgnored ? ignoredPolygon : null
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
@@ -982,7 +997,8 @@ namespace gdjs {
|
||||
context.initializeBeforeSearch(this, upwardDeltaY, downwardDeltaY);
|
||||
|
||||
let totalHighestY = Number.MAX_VALUE;
|
||||
let highestGround: gdjs.PlatformRuntimeBehavior | null = null;
|
||||
let highestGroundPlatform: gdjs.PlatformRuntimeBehavior | null = null;
|
||||
let highestGroundPolygon: gdjs.Polygon | null = null;
|
||||
let isCollidingAnyPlatform = false;
|
||||
for (const platform of candidates) {
|
||||
if (
|
||||
@@ -1032,7 +1048,8 @@ namespace gdjs {
|
||||
// and is too high for the character to walk on.
|
||||
// This will still be an obstacle even if there
|
||||
// are other platforms that fit the requirements.
|
||||
highestGround = null;
|
||||
highestGroundPlatform = null;
|
||||
highestGroundPolygon = null;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1041,16 +1058,18 @@ namespace gdjs {
|
||||
highestRelativeY < totalHighestY
|
||||
) {
|
||||
totalHighestY = highestRelativeY;
|
||||
highestGround = platform;
|
||||
highestGroundPlatform = platform;
|
||||
highestGroundPolygon = context.highestFloorPolygon;
|
||||
}
|
||||
}
|
||||
if (highestGround) {
|
||||
if (highestGroundPlatform) {
|
||||
const object = this.owner;
|
||||
object.setY(object.getY() + totalHighestY);
|
||||
}
|
||||
const returnValue =
|
||||
gdjs.PlatformerObjectRuntimeBehavior._platformSearchResult;
|
||||
returnValue.highestGround = highestGround;
|
||||
returnValue.highestGroundPlatform = highestGroundPlatform;
|
||||
returnValue.highestGroundPolygon = highestGroundPolygon;
|
||||
returnValue.isCollidingAnyPlatform = isCollidingAnyPlatform;
|
||||
return returnValue;
|
||||
}
|
||||
@@ -1115,7 +1134,7 @@ namespace gdjs {
|
||||
(vertex[0] === context.ownerMaxX &&
|
||||
(previousVertex[0] < vertex[0] || nextVertex[0] < vertex[0]))
|
||||
) {
|
||||
context.addPointConstraint(vertex[1]);
|
||||
context.addPointConstraint(vertex[1], hitbox);
|
||||
}
|
||||
|
||||
const deltaX = vertex[0] - previousVertex[0];
|
||||
@@ -1133,7 +1152,7 @@ namespace gdjs {
|
||||
previousVertex[1] +
|
||||
((context.ownerMinX - previousVertex[0]) * deltaY) / deltaX;
|
||||
|
||||
context.addPointConstraint(intersectionY);
|
||||
context.addPointConstraint(intersectionY, hitbox);
|
||||
}
|
||||
// Check intersection on the right side of owner
|
||||
if (
|
||||
@@ -1147,7 +1166,7 @@ namespace gdjs {
|
||||
previousVertex[1] +
|
||||
((context.ownerMaxX - previousVertex[0]) * deltaY) / deltaX;
|
||||
|
||||
context.addPointConstraint(intersectionY);
|
||||
context.addPointConstraint(intersectionY, hitbox);
|
||||
}
|
||||
}
|
||||
if (context.floorIsTooHigh()) {
|
||||
@@ -1867,6 +1886,7 @@ namespace gdjs {
|
||||
class OnFloor implements State {
|
||||
private _behavior: PlatformerObjectRuntimeBehavior;
|
||||
private _floorPlatform: gdjs.PlatformRuntimeBehavior | null = null;
|
||||
private _floorPolygon: gdjs.Polygon | null = null;
|
||||
private _floorLastX: float = 0;
|
||||
private _floorLastY: float = 0;
|
||||
_oldHeight: float = 0;
|
||||
@@ -1879,8 +1899,16 @@ namespace gdjs {
|
||||
return this._floorPlatform;
|
||||
}
|
||||
|
||||
enter(floorPlatform: gdjs.PlatformRuntimeBehavior) {
|
||||
getFloorPolygon() {
|
||||
return this._floorPolygon;
|
||||
}
|
||||
|
||||
enter(
|
||||
floorPlatform: gdjs.PlatformRuntimeBehavior,
|
||||
floorPolygon: gdjs.Polygon
|
||||
) {
|
||||
this._floorPlatform = floorPlatform;
|
||||
this._floorPolygon = floorPolygon;
|
||||
this.updateFloorPosition();
|
||||
this._behavior._canJump = true;
|
||||
this._behavior._currentFallSpeed = 0;
|
||||
@@ -1888,6 +1916,7 @@ namespace gdjs {
|
||||
|
||||
leave() {
|
||||
this._floorPlatform = null;
|
||||
this._floorPolygon = null;
|
||||
}
|
||||
|
||||
updateFloorPosition() {
|
||||
@@ -2000,17 +2029,23 @@ namespace gdjs {
|
||||
behavior._requestedDeltaX * behavior._slopeClimbingFactor
|
||||
);
|
||||
const {
|
||||
highestGround,
|
||||
highestGroundPlatform,
|
||||
highestGroundPolygon,
|
||||
isCollidingAnyPlatform,
|
||||
} = behavior._findHighestFloorAndMoveOnTop(
|
||||
behavior._potentialCollidingObjects,
|
||||
-deltaMaxY,
|
||||
deltaMaxY
|
||||
);
|
||||
if (highestGround && highestGround !== this._floorPlatform) {
|
||||
behavior._setOnFloor(highestGround);
|
||||
if (
|
||||
highestGroundPlatform &&
|
||||
highestGroundPolygon &&
|
||||
(highestGroundPlatform !== this._floorPlatform ||
|
||||
highestGroundPolygon !== this._floorPolygon)
|
||||
) {
|
||||
behavior._setOnFloor(highestGroundPlatform, highestGroundPolygon);
|
||||
}
|
||||
if (highestGround === null && isCollidingAnyPlatform) {
|
||||
if (highestGroundPlatform === null && isCollidingAnyPlatform) {
|
||||
// Unable to follow the floor (too steep): go back to the original position.
|
||||
behavior.owner.setX(oldX);
|
||||
}
|
||||
@@ -2020,7 +2055,7 @@ namespace gdjs {
|
||||
|
||||
// Try to follow the platform until the obstacle.
|
||||
const {
|
||||
highestGround: highestGroundOnPlatform,
|
||||
highestGroundPlatform: highestGroundOnPlatform,
|
||||
isCollidingAnyPlatform,
|
||||
} = behavior._findHighestFloorAndMoveOnTop(
|
||||
behavior._potentialCollidingObjects,
|
||||
@@ -2055,7 +2090,7 @@ namespace gdjs {
|
||||
// 1. Try to move 1 pixel on the X axis to climb the junction.
|
||||
object.setX(object.getX() + Math.sign(requestedDeltaX));
|
||||
const {
|
||||
highestGround: highestGroundAtJunction,
|
||||
highestGroundPlatform: highestGroundAtJunction,
|
||||
} = behavior._findHighestFloorAndMoveOnTop(
|
||||
behavior._potentialCollidingObjects,
|
||||
// Look up from at least 1 pixel to bypass not perfectly aligned floors.
|
||||
@@ -2075,23 +2110,27 @@ namespace gdjs {
|
||||
);
|
||||
object.setX(object.getX() + deltaX);
|
||||
const {
|
||||
highestGround: highestGroundOnObstacle,
|
||||
highestGroundPlatform: highestGroundOnObstacle,
|
||||
highestGroundPolygon,
|
||||
} = behavior._findHighestFloorAndMoveOnTop(
|
||||
behavior._potentialCollidingObjects,
|
||||
// Do an exact slope angle check.
|
||||
-Math.abs(deltaX) * behavior._slopeClimbingFactor,
|
||||
0
|
||||
);
|
||||
if (highestGroundOnObstacle) {
|
||||
if (highestGroundOnObstacle && highestGroundPolygon) {
|
||||
// The obstacle slope can be climbed.
|
||||
if (Math.abs(remainingDeltaX) >= 2) {
|
||||
behavior._setOnFloor(highestGroundOnObstacle);
|
||||
behavior._setOnFloor(
|
||||
highestGroundOnObstacle,
|
||||
highestGroundPolygon
|
||||
);
|
||||
} else {
|
||||
// We went too far in order to check that.
|
||||
// Now, find the right position on the obstacles.
|
||||
object.setPosition(oldX + requestedDeltaX, beforeObstacleY);
|
||||
const {
|
||||
highestGround: highestGroundOnObstacle,
|
||||
highestGroundPlatform: highestGroundOnObstacle,
|
||||
} = behavior._findHighestFloorAndMoveOnTop(
|
||||
behavior._potentialCollidingObjects,
|
||||
// requestedDeltaX can be small when the object start moving.
|
||||
@@ -2103,8 +2142,11 @@ namespace gdjs {
|
||||
0
|
||||
);
|
||||
// Should always be true
|
||||
if (highestGroundOnObstacle) {
|
||||
behavior._setOnFloor(highestGroundOnObstacle);
|
||||
if (highestGroundOnObstacle && highestGroundPolygon) {
|
||||
behavior._setOnFloor(
|
||||
highestGroundOnObstacle,
|
||||
highestGroundPolygon
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -2578,6 +2620,8 @@ namespace gdjs {
|
||||
*/
|
||||
foundUnderBottom: boolean = false;
|
||||
|
||||
highestFloorPolygon: gdjs.Polygon | null = null;
|
||||
|
||||
initializeBeforeSearch(
|
||||
behavior: PlatformerObjectRuntimeBehavior,
|
||||
upwardDeltaY: float,
|
||||
@@ -2653,7 +2697,7 @@ namespace gdjs {
|
||||
* and update the context with this new constraint.
|
||||
* @param y
|
||||
*/
|
||||
addPointConstraint(y: float): void {
|
||||
addPointConstraint(y: float, sourcePolygon: gdjs.Polygon): void {
|
||||
if (y < this.floorMinY) {
|
||||
// The platform is too high to walk on...
|
||||
if (y > this.headMaxY) {
|
||||
@@ -2693,6 +2737,7 @@ namespace gdjs {
|
||||
this.allowedMaxDeltaY,
|
||||
y - this.ownerMaxY
|
||||
);
|
||||
this.highestFloorPolygon = sourcePolygon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1644,8 +1644,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
behavior = object.getBehavior('auto1');
|
||||
object.setCustomWidthAndHeight(10, 20);
|
||||
runtimeScene.addObject(object);
|
||||
// The object is in the corner of the platform.
|
||||
object.setPosition(80 - 10, 80 - 20);
|
||||
});
|
||||
|
||||
[
|
||||
@@ -1672,6 +1670,9 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
it(`can jump while moving against a wall ${wallBeing}`, function () {
|
||||
createPlatforms(runtimeScene);
|
||||
|
||||
// The object is in the corner of the platform.
|
||||
object.setPosition(80 - 10, 80 - 20);
|
||||
|
||||
// The object stays on the platform.
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
@@ -1696,6 +1697,32 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
expect(object.getY()).to.be.lessThan(oldY);
|
||||
}
|
||||
});
|
||||
|
||||
it(`can move against a wall ${wallBeing}`, function () {
|
||||
createPlatforms(runtimeScene);
|
||||
|
||||
// The object is 20 pixels from the wall.
|
||||
object.setPosition(80 - 10 - 10, 80 - 20);
|
||||
|
||||
// The object stays on the platform.
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object.getY()).to.within(60 - epsilon, 60 + epsilon);
|
||||
expect(behavior.isFalling()).to.be(false);
|
||||
expect(behavior.isFallingWithoutJumping()).to.be(false);
|
||||
expect(behavior.isMoving()).to.be(false);
|
||||
|
||||
// It's important the character moves from several pixels per frame.
|
||||
behavior.setCurrentSpeed(behavior.getMaxSpeed());
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const oldY = object.getY();
|
||||
behavior.simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
// The object is against the wall.
|
||||
expect(object.getX()).to.be(80 - 10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -86,6 +86,12 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
return platform;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {gdjs.PlatformerObjectRuntimeBehavior} characterBehavior
|
||||
* @param {gdjs.PlatformRuntimeBehavior} platformBehavior
|
||||
* @param {float} upwardDeltaY
|
||||
* @param {float} downwardDeltaY
|
||||
*/
|
||||
const checkMoveOn = (
|
||||
characterBehavior,
|
||||
platformBehavior,
|
||||
@@ -97,9 +103,15 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
upwardDeltaY,
|
||||
downwardDeltaY
|
||||
);
|
||||
expect(result.highestGround).to.be(platformBehavior);
|
||||
expect(result.highestGroundPlatform).to.be(platformBehavior);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {gdjs.PlatformerObjectRuntimeBehavior} characterBehavior
|
||||
* @param {gdjs.PlatformRuntimeBehavior} platformBehavior
|
||||
* @param {float} upwardDeltaY
|
||||
* @param {float} downwardDeltaY
|
||||
*/
|
||||
const checkNoFloor = (
|
||||
characterBehavior,
|
||||
platformBehavior,
|
||||
@@ -112,11 +124,17 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
upwardDeltaY,
|
||||
downwardDeltaY
|
||||
);
|
||||
expect(result.highestGround).to.be(null);
|
||||
expect(result.highestGroundPlatform).to.be(null);
|
||||
expect(result.isCollidingAnyPlatform).to.be(false);
|
||||
expect(characterBehavior.owner.getY()).to.be(oldY);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {gdjs.PlatformerObjectRuntimeBehavior} characterBehavior
|
||||
* @param {gdjs.PlatformRuntimeBehavior} platformBehavior
|
||||
* @param {float} upwardDeltaY
|
||||
* @param {float} downwardDeltaY
|
||||
*/
|
||||
const checkObstacle = (
|
||||
characterBehavior,
|
||||
platformBehavior,
|
||||
@@ -129,7 +147,7 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
upwardDeltaY,
|
||||
downwardDeltaY
|
||||
);
|
||||
expect(result.highestGround).to.be(null);
|
||||
expect(result.highestGroundPlatform).to.be(null);
|
||||
expect(result.isCollidingAnyPlatform).to.be(true);
|
||||
expect(characterBehavior.owner.getY()).to.be(oldY);
|
||||
};
|
||||
@@ -293,11 +311,13 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
describe(description, function () {
|
||||
const runtimeScene = makeTestRuntimeScene();
|
||||
const character = addCharacter(runtimeScene);
|
||||
/** @type gdjs.PlatformerObjectRuntimeBehavior */
|
||||
const behavior = character.getBehavior('auto1');
|
||||
|
||||
const platform = addPlatform(runtimeScene, mask);
|
||||
platform.setCustomWidthAndHeight(300, 300);
|
||||
platform.setPosition(position[0], position[1]);
|
||||
/** @type gdjs.PlatformRuntimeBehavior */
|
||||
const platformBehavior = platform.getBehavior('Platform');
|
||||
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
|
||||
runtimeScene
|
||||
@@ -365,11 +385,13 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
describe(description, function () {
|
||||
const runtimeScene = makeTestRuntimeScene();
|
||||
const character = addCharacter(runtimeScene);
|
||||
/** @type gdjs.PlatformerObjectRuntimeBehavior */
|
||||
const behavior = character.getBehavior('auto1');
|
||||
|
||||
const platform = addPlatform(runtimeScene, mask);
|
||||
platform.setCustomWidthAndHeight(300, 300);
|
||||
platform.setPosition(position[0], position[1]);
|
||||
/** @type gdjs.PlatformRuntimeBehavior */
|
||||
const platformBehavior = platform.getBehavior('Platform');
|
||||
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
|
||||
runtimeScene
|
||||
|
@@ -116,6 +116,18 @@ void SpineObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker &work
|
||||
worker.ExposeEmbeddeds(spineResourceName);
|
||||
}
|
||||
|
||||
const gd::String &
|
||||
SpineObjectConfiguration::GetAnimationName(size_t index) const {
|
||||
return GetAnimation(index).GetName();
|
||||
}
|
||||
|
||||
bool SpineObjectConfiguration::HasAnimationNamed(const gd::String &name) const {
|
||||
return !name.empty() && (find_if(animations.begin(), animations.end(),
|
||||
[&name](const SpineAnimation &animation) {
|
||||
return animation.GetName() == name;
|
||||
}) != animations.end());
|
||||
}
|
||||
|
||||
const SpineAnimation &
|
||||
SpineObjectConfiguration::GetAnimation(std::size_t nb) const {
|
||||
if (nb >= animations.size()) return badAnimation;
|
||||
@@ -129,13 +141,6 @@ SpineAnimation &SpineObjectConfiguration::GetAnimation(std::size_t nb) {
|
||||
return animations[nb];
|
||||
}
|
||||
|
||||
bool SpineObjectConfiguration::HasAnimationNamed(const gd::String &name) const {
|
||||
return !name.empty() && (find_if(animations.begin(), animations.end(),
|
||||
[&name](const SpineAnimation &animation) {
|
||||
return animation.GetName() == name;
|
||||
}) != animations.end());
|
||||
}
|
||||
|
||||
void SpineObjectConfiguration::AddAnimation(const SpineAnimation &animation) {
|
||||
animations.push_back(animation);
|
||||
}
|
||||
|
@@ -93,15 +93,11 @@ public:
|
||||
*/
|
||||
SpineAnimation &GetAnimation(std::size_t nb);
|
||||
|
||||
/**
|
||||
* \brief Return the number of animations this object has.
|
||||
*/
|
||||
std::size_t GetAnimationsCount() const { return animations.size(); };
|
||||
std::size_t GetAnimationsCount() const override { return animations.size(); };
|
||||
|
||||
/**
|
||||
* \brief Return true if the animation called "name" exists.
|
||||
*/
|
||||
bool HasAnimationNamed(const gd::String& name) const;
|
||||
const gd::String &GetAnimationName(size_t index) const override;
|
||||
|
||||
bool HasAnimationNamed(const gd::String &animationName) const override;
|
||||
|
||||
/**
|
||||
* \brief Add an animation at the end of the existing ones.
|
||||
|
@@ -397,7 +397,6 @@ module.exports = {
|
||||
'telephone number',
|
||||
'url',
|
||||
'search',
|
||||
'email',
|
||||
])
|
||||
)
|
||||
)
|
||||
|
@@ -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() {
|
||||
|
@@ -176,7 +176,7 @@ const defineTileMap = function (extension, _, gd) {
|
||||
const object = extension
|
||||
.addObject(
|
||||
'TileMap',
|
||||
_('Tilemap'),
|
||||
_('External Tilemap (Tiled/LDtk)'),
|
||||
_(
|
||||
'Displays a tiled-based map, made with the Tiled editor (https://www.mapeditor.org/) or the LDtk editor (https://ldtk.io/).'
|
||||
),
|
||||
@@ -591,6 +591,471 @@ const defineTileMap = function (extension, _, gd) {
|
||||
.setFunctionName('setHeight');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {gd.PlatformExtension} extension
|
||||
* @param {(translationSource: string) => string} _
|
||||
* @param {GDNamespace} gd
|
||||
*/
|
||||
const defineSimpleTileMap = function (extension, _, gd) {
|
||||
var objectSimpleTileMap = new gd.ObjectJsImplementation();
|
||||
objectSimpleTileMap.updateProperty = function (
|
||||
objectContent,
|
||||
propertyName,
|
||||
newValue
|
||||
) {
|
||||
if (propertyName === 'atlasImage') {
|
||||
objectContent.atlasImage = newValue;
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'columnCount') {
|
||||
objectContent.columnCount = parseFloat(newValue);
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'rowCount') {
|
||||
objectContent.rowCount = parseFloat(newValue);
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'tileSize') {
|
||||
objectContent.tileSize = parseFloat(newValue);
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'tilesWithHitBox') {
|
||||
objectContent.tilesWithHitBox = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
objectSimpleTileMap.getProperties = function (objectContent) {
|
||||
var objectProperties = new gd.MapStringPropertyDescriptor();
|
||||
|
||||
objectProperties.set(
|
||||
'columnCount',
|
||||
new gd.PropertyDescriptor((objectContent.columnCount || 4).toString())
|
||||
.setType('number')
|
||||
.setLabel(_('Columns'))
|
||||
.setDescription(_('Number of columns.'))
|
||||
.setHidden(true)
|
||||
);
|
||||
objectProperties.set(
|
||||
'rowCount',
|
||||
new gd.PropertyDescriptor((objectContent.rowCount || 4).toString())
|
||||
.setType('number')
|
||||
.setLabel(_('Rows'))
|
||||
.setDescription(_('Number of rows.'))
|
||||
.setHidden(true)
|
||||
);
|
||||
objectProperties.set(
|
||||
'tileSize',
|
||||
new gd.PropertyDescriptor((objectContent.tileSize || 8).toString())
|
||||
.setType('number')
|
||||
.setLabel(_('Tile size'))
|
||||
.setDescription(_('Tile size in pixels.'))
|
||||
);
|
||||
objectProperties.set(
|
||||
'tilesWithHitBox',
|
||||
new gd.PropertyDescriptor(objectContent.tilesWithHitBox || '')
|
||||
.setType('string')
|
||||
.setLabel(_('Tile ids with hit box'))
|
||||
.setDescription(
|
||||
_('The list of tile ids with a hit box (separated by commas).')
|
||||
)
|
||||
.setHidden(true)
|
||||
);
|
||||
|
||||
objectProperties.set(
|
||||
'atlasImage',
|
||||
new gd.PropertyDescriptor(objectContent.atlasImage)
|
||||
.setType('resource')
|
||||
.addExtraInfo('image')
|
||||
.setLabel(_('Atlas image'))
|
||||
.setDescription(_('The Atlas image containing the tileset.'))
|
||||
);
|
||||
|
||||
return objectProperties;
|
||||
};
|
||||
objectSimpleTileMap.setRawJSONContent(
|
||||
JSON.stringify({
|
||||
atlasImage: '',
|
||||
rowCount: 1,
|
||||
columnCount: 1,
|
||||
tileSize: 8,
|
||||
tilesWithHitBox: '',
|
||||
})
|
||||
);
|
||||
|
||||
objectSimpleTileMap.updateInitialInstanceProperty = function (
|
||||
instance,
|
||||
propertyName,
|
||||
newValue
|
||||
) {
|
||||
if (propertyName === 'tilemap') {
|
||||
instance.setRawStringProperty('tilemap', newValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
objectSimpleTileMap.getInitialInstanceProperties = function (
|
||||
objectContent,
|
||||
instance
|
||||
) {
|
||||
var instanceProperties = new gd.MapStringPropertyDescriptor();
|
||||
|
||||
instanceProperties
|
||||
.getOrCreate('tilemap')
|
||||
.setValue(instance.getRawStringProperty('tileMap'))
|
||||
.setType('string')
|
||||
.setLabel('Tilemap')
|
||||
.setHidden(true);
|
||||
|
||||
return instanceProperties;
|
||||
};
|
||||
|
||||
const object = extension
|
||||
.addObject(
|
||||
'SimpleTileMap',
|
||||
_('Tile map'),
|
||||
_('Displays a tiled-based map.'),
|
||||
'JsPlatform/Extensions/tile_map.svg',
|
||||
objectSimpleTileMap
|
||||
)
|
||||
.setCategoryFullName(_('General'))
|
||||
.addDefaultBehavior('EffectCapability::EffectBehavior')
|
||||
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
|
||||
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
|
||||
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
|
||||
.setIncludeFile('Extensions/TileMap/simpletilemapruntimeobject.js')
|
||||
.addIncludeFile('Extensions/TileMap/TileMapRuntimeManager.js')
|
||||
.addIncludeFile('Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js')
|
||||
.addIncludeFile('Extensions/TileMap/pixi-tilemap/dist/pixi-tilemap.umd.js')
|
||||
.addIncludeFile('Extensions/TileMap/collision/TransformedTileMap.js')
|
||||
.addIncludeFile('Extensions/TileMap/pako/dist/pako.min.js')
|
||||
.addIncludeFile('Extensions/TileMap/helper/TileMapHelper.js');
|
||||
|
||||
object
|
||||
.addExpression(
|
||||
'TilesetColumnCount',
|
||||
_('Tileset column count'),
|
||||
_('Get the number of column in the tileset.'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.setFunctionName('getTilesetColumnCount');
|
||||
|
||||
object
|
||||
.addExpression(
|
||||
'TilesetRowCount',
|
||||
_('Tileset row count'),
|
||||
_('Get the number of row in the tileset.'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.setFunctionName('getTilesetRowCount');
|
||||
|
||||
object
|
||||
.addExpression(
|
||||
'TileCenterX',
|
||||
_('Scene X coordinate of tile'),
|
||||
_('Get the scene X position of the center of the tile.'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.setFunctionName('getSceneXCoordinateOfTileCenter');
|
||||
|
||||
object
|
||||
.addExpression(
|
||||
'TileCenterY',
|
||||
_('Scene Y coordinate of tile'),
|
||||
_('Get the scene Y position of the center of the tile.'),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.addParameter('object', _('Tile map'), 'SimpleTileMap', false)
|
||||
.addParameter('number', _('Grid X'), '', false)
|
||||
.addParameter('number', _('Grid Y'), '', false)
|
||||
.setFunctionName('getSceneYCoordinateOfTileCenter');
|
||||
|
||||
object
|
||||
.addExpression(
|
||||
'GridX',
|
||||
_('Tile map grid column coordinate'),
|
||||
_(
|
||||
'Get the grid column coordinates in the tile map corresponding to the scene coordinates.'
|
||||
),
|
||||
'',
|
||||
'JsPlatform/Extensions/tile_map.svg'
|
||||
)
|
||||
.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('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(
|
||||
'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', _('Position X'), '', false)
|
||||
.addParameter('number', _('Position Y'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.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 _PARAM3_ ; _PARAM4_'),
|
||||
'',
|
||||
'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');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {gd.PlatformExtension} extension
|
||||
* @param {(translationSource: string) => string} _
|
||||
@@ -769,7 +1234,7 @@ const defineCollisionMask = function (extension, _, gd) {
|
||||
const object = extension
|
||||
.addObject(
|
||||
'CollisionMask',
|
||||
_('Tilemap collision mask'),
|
||||
_('External Tilemap (Tiled/LDtk) collision mask'),
|
||||
_('Invisible object handling collisions with parts of a tilemap.'),
|
||||
'JsPlatform/Extensions/tile_map_collision_mask32.svg',
|
||||
collisionMaskObject
|
||||
@@ -1020,7 +1485,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..."
|
||||
),
|
||||
@@ -1035,6 +1500,7 @@ module.exports = {
|
||||
.setIcon('JsPlatform/Extensions/tile_map.svg');
|
||||
|
||||
defineTileMap(extension, _, gd);
|
||||
defineSimpleTileMap(extension, _, gd);
|
||||
defineCollisionMask(extension, _, gd);
|
||||
|
||||
return extension;
|
||||
@@ -1411,7 +1877,367 @@ module.exports = {
|
||||
);
|
||||
|
||||
/**
|
||||
* Renderer for instances of TileMap inside the IDE.
|
||||
* Renderer for instances of SimpleTileMap inside the IDE.
|
||||
*/
|
||||
class RenderedSimpleTileMapInstance extends RenderedInstance {
|
||||
_placeholderTextPixiObject = new PIXI.Text(
|
||||
'Select this instance\nto start painting',
|
||||
new PIXI.TextStyle({
|
||||
fontFamily: 'Arial',
|
||||
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,
|
||||
instance,
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader
|
||||
) {
|
||||
super(
|
||||
project,
|
||||
instance,
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader
|
||||
);
|
||||
|
||||
// This setting allows tile maps with more than 16K tiles.
|
||||
Tilemap.settings.use32bitIndex = true;
|
||||
|
||||
this.tileMapPixiObject = new Tilemap.CompositeTilemap();
|
||||
this._pixiObject = new PIXI.Container();
|
||||
this._pixiObject.addChild(this.tileMapPixiObject);
|
||||
this._editableTileMap = null;
|
||||
|
||||
// Implement `containsPoint` so that we can set `interactive` to true and
|
||||
// the Tilemap will properly emit events when hovered/clicked.
|
||||
// By default, this is not implemented in pixi-tilemap.
|
||||
this._pixiObject.containsPoint = (position) => {
|
||||
// Turns the world position to the local object coordinates
|
||||
const localPosition = new PIXI.Point();
|
||||
if (this.tileMapPixiObject.visible) {
|
||||
this.tileMapPixiObject.worldTransform.applyInverse(
|
||||
position,
|
||||
localPosition
|
||||
);
|
||||
} else {
|
||||
this._placeholderImagePixiObject.worldTransform.applyInverse(
|
||||
position,
|
||||
localPosition
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
localPosition.x >= 0 &&
|
||||
localPosition.x < this.width &&
|
||||
localPosition.y >= 0 &&
|
||||
localPosition.y < this.height
|
||||
);
|
||||
};
|
||||
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;
|
||||
this.height = 48;
|
||||
this._objectName = instance.getObjectName();
|
||||
this.update();
|
||||
this.updateTileMap();
|
||||
}
|
||||
|
||||
onRemovedFromScene() {
|
||||
super.onRemovedFromScene();
|
||||
// Keep textures because they are shared by all tile maps.
|
||||
this._pixiObject.destroy(false);
|
||||
}
|
||||
|
||||
onLoadingError() {
|
||||
this.errorPixiObject =
|
||||
this.errorPixiObject ||
|
||||
new PIXI.Sprite(this._pixiResourcesLoader.getInvalidPIXITexture());
|
||||
this._pixiContainer.addChild(this.errorPixiObject);
|
||||
this._pixiObject = this.errorPixiObject;
|
||||
}
|
||||
|
||||
onLoadingSuccess() {
|
||||
if (this.errorPixiObject) {
|
||||
this._pixiContainer.removeChild(this.errorPixiObject);
|
||||
this.errorPixiObject = null;
|
||||
this._pixiObject = this.tileMapPixiObject;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path to the thumbnail of the specified object.
|
||||
*/
|
||||
static getThumbnail(project, resourcesLoader, objectConfiguration) {
|
||||
const atlasImageResourceName = objectConfiguration
|
||||
.getProperties()
|
||||
.get('atlasImage')
|
||||
.getValue();
|
||||
return resourcesLoader.getResourceFullUrl(
|
||||
project,
|
||||
atlasImageResourceName,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
getEditableTileMap() {
|
||||
return this._editableTileMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to reload the Tilemap
|
||||
*/
|
||||
updateTileMap() {
|
||||
const atlasImageResourceName = this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('atlasImage')
|
||||
.getValue();
|
||||
const tilemapAsJSObject = JSON.parse(
|
||||
this._instance.getRawStringProperty('tilemap') || '{}'
|
||||
);
|
||||
|
||||
const tileSize = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('tileSize')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
const columnCount = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('columnCount')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
const rowCount = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('rowCount')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
|
||||
const atlasTexture = this._pixiResourcesLoader.getPIXITexture(
|
||||
this._project,
|
||||
atlasImageResourceName
|
||||
);
|
||||
|
||||
const loadTileMap = () => {
|
||||
/** @type {TileMapHelper.TileMapManager} */
|
||||
const manager = TilemapHelper.TileMapManager.getManager(
|
||||
this._project
|
||||
);
|
||||
manager.getOrLoadSimpleTileMap(
|
||||
tilemapAsJSObject,
|
||||
this._objectName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
(tileMap) => {
|
||||
if (!tileMap) {
|
||||
this.onLoadingError();
|
||||
console.error('Could not parse tilemap.');
|
||||
return;
|
||||
}
|
||||
|
||||
this._editableTileMap = tileMap;
|
||||
|
||||
manager.getOrLoadSimpleTileMapTextureCache(
|
||||
(textureName) =>
|
||||
this._pixiResourcesLoader.getPIXITexture(
|
||||
this._project,
|
||||
textureName
|
||||
),
|
||||
atlasImageResourceName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
(
|
||||
/** @type {TileMapHelper.TileTextureCache | null} */
|
||||
textureCache
|
||||
) => {
|
||||
this.onLoadingSuccess();
|
||||
if (!this._editableTileMap) return;
|
||||
|
||||
this.width = this._editableTileMap.getWidth();
|
||||
this.height = this._editableTileMap.getHeight();
|
||||
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
|
||||
this.tileMapPixiObject,
|
||||
this._editableTileMap,
|
||||
textureCache,
|
||||
'all', // No notion of visibility on simple tile maps.
|
||||
0 // Only one layer is used on simple tile maps.
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (atlasTexture.valid) {
|
||||
loadTileMap();
|
||||
} else {
|
||||
// Wait for the atlas image to load.
|
||||
atlasTexture.once('update', () => {
|
||||
loadTileMap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updatePixiTileMap() {
|
||||
const atlasImageResourceName = this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('atlasImage')
|
||||
.getValue();
|
||||
|
||||
const tileSize = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('tileSize')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
const columnCount = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('columnCount')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
const rowCount = parseInt(
|
||||
this._associatedObjectConfiguration
|
||||
.getProperties()
|
||||
.get('rowCount')
|
||||
.getValue(),
|
||||
10
|
||||
);
|
||||
/** @type {TileMapHelper.TileMapManager} */
|
||||
const manager = TilemapHelper.TileMapManager.getManager(this._project);
|
||||
|
||||
manager.getOrLoadSimpleTileMapTextureCache(
|
||||
(textureName) =>
|
||||
this._pixiResourcesLoader.getPIXITexture(
|
||||
this._project,
|
||||
textureName
|
||||
),
|
||||
atlasImageResourceName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
(
|
||||
/** @type {TileMapHelper.TileTextureCache | null} */
|
||||
textureCache
|
||||
) => {
|
||||
this.onLoadingSuccess();
|
||||
if (!this._editableTileMap) return;
|
||||
|
||||
this.width = this._editableTileMap.getWidth();
|
||||
this.height = this._editableTileMap.getHeight();
|
||||
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
|
||||
this.tileMapPixiObject,
|
||||
this._editableTileMap,
|
||||
textureCache,
|
||||
'all',
|
||||
0
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called to update the PIXI object on the scene editor
|
||||
*/
|
||||
update() {
|
||||
const isTileMapEmpty = this._editableTileMap
|
||||
? this._editableTileMap.isEmpty()
|
||||
: false;
|
||||
let objectToChange;
|
||||
if (isTileMapEmpty) {
|
||||
this._placeholderPixiObject.visible = true;
|
||||
this.tileMapPixiObject.visible = false;
|
||||
objectToChange = this._placeholderPixiObject;
|
||||
} else {
|
||||
this._placeholderPixiObject.visible = false;
|
||||
this.tileMapPixiObject.visible = true;
|
||||
objectToChange = this.tileMapPixiObject;
|
||||
}
|
||||
|
||||
if (!isTileMapEmpty) {
|
||||
// Don't change size of placeholder object.
|
||||
if (this._instance.hasCustomSize()) {
|
||||
objectToChange.scale.x = this.getCustomWidth() / this.width;
|
||||
objectToChange.scale.y = this.getCustomHeight() / this.height;
|
||||
} else {
|
||||
objectToChange.scale.x = 1;
|
||||
objectToChange.scale.y = 1;
|
||||
}
|
||||
|
||||
// Place the center of rotation in the center of the object. Because pivot position in Pixi
|
||||
// is in the **local coordinates of the object**, we need to find back the original width
|
||||
// and height of the object before scaling (then divide by 2 to find the center)
|
||||
const originalWidth = this.width;
|
||||
const originalHeight = this.height;
|
||||
objectToChange.pivot.x = originalWidth / 2;
|
||||
objectToChange.pivot.y = originalHeight / 2;
|
||||
}
|
||||
// Modifying the pivot position also has an impact on the transform. The instance (X,Y) position
|
||||
// of this object refers to the top-left point, but now in Pixi, as we changed the pivot, the Pixi
|
||||
// object (X,Y) position refers to the center. So we add an offset to convert from top-left to center.
|
||||
objectToChange.x =
|
||||
this._instance.getX() +
|
||||
objectToChange.pivot.x * objectToChange.scale.x;
|
||||
objectToChange.y =
|
||||
this._instance.getY() +
|
||||
objectToChange.pivot.y * objectToChange.scale.y;
|
||||
|
||||
// Rotation works as intended because we put the pivot in the center
|
||||
objectToChange.rotation = RenderedInstance.toRad(
|
||||
this._instance.getAngle()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of the instance, when it's not resized.
|
||||
*/
|
||||
getDefaultWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the height of the instance, when it's not resized.
|
||||
*/
|
||||
getDefaultHeight() {
|
||||
return this.height;
|
||||
}
|
||||
}
|
||||
|
||||
objectsRenderingService.registerInstanceRenderer(
|
||||
'TileMap::SimpleTileMap',
|
||||
RenderedSimpleTileMapInstance
|
||||
);
|
||||
|
||||
/**
|
||||
* Renderer for instances of TileMap collision mask inside the IDE.
|
||||
*/
|
||||
class RenderedCollisionMaskInstance extends RenderedInstance {
|
||||
constructor(
|
||||
|
@@ -79,6 +79,24 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
callback: (tileMap: TileMapHelper.EditableTileMap) => void
|
||||
): void {
|
||||
this._manager.getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject,
|
||||
objectName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
* @param atlasImageResourceName The resource name of the atlas image.
|
||||
@@ -106,6 +124,32 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
* @param atlasImageResourceName The resource name of the atlas image.
|
||||
* @param tileSize
|
||||
* @param columnCount
|
||||
* @param rowCount
|
||||
* @param callback A function called when the tiles textures are split.
|
||||
*/
|
||||
getOrLoadSimpleTileMapTextureCache(
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
atlasImageResourceName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
callback: (textureCache: TileMapHelper.TileTextureCache | null) => void
|
||||
): void {
|
||||
this._manager.getOrLoadSimpleTileMapTextureCache(
|
||||
getTexture,
|
||||
atlasImageResourceName,
|
||||
tileSize,
|
||||
columnCount,
|
||||
rowCount,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse both JSON and set the content of the tile set in the right
|
||||
* attribute in the tile map to merge both parsed data.
|
||||
|
@@ -30,7 +30,7 @@ namespace gdjs {
|
||||
*/
|
||||
_transformationUpToDateCount: integer = 1;
|
||||
/**
|
||||
* An reusable Point to avoid allocations.
|
||||
* A reusable Point to avoid allocations.
|
||||
*/
|
||||
private static readonly workingPoint: FloatPoint = [0, 0];
|
||||
|
||||
@@ -41,14 +41,28 @@ namespace gdjs {
|
||||
this._source = source;
|
||||
this.tag = tag;
|
||||
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
|
||||
for (const sourceLayer of source.getLayers()) {
|
||||
this._buildLayersFromTileMap(source, this._layers);
|
||||
}
|
||||
|
||||
updateFromTileMap(tileMap: TileMapHelper.EditableTileMap) {
|
||||
this._source = tileMap;
|
||||
this._layers = new Map<integer, TransformedCollisionTileMapLayer>();
|
||||
|
||||
this._buildLayersFromTileMap(tileMap, this._layers);
|
||||
}
|
||||
|
||||
_buildLayersFromTileMap(
|
||||
tileMap: TileMapHelper.EditableTileMap,
|
||||
layers: Map<integer, TransformedCollisionTileMapLayer>
|
||||
) {
|
||||
for (const sourceLayer of tileMap.getLayers()) {
|
||||
// TODO A visitor could be used to avoid a cast.
|
||||
if (!(sourceLayer instanceof TileMapHelper.EditableTileMapLayer)) {
|
||||
// TODO Collision mask for object layers is not handled.
|
||||
continue;
|
||||
}
|
||||
const tileLayer = sourceLayer as TileMapHelper.EditableTileMapLayer;
|
||||
this._layers.set(
|
||||
layers.set(
|
||||
tileLayer.id,
|
||||
new TransformedCollisionTileMapLayer(this, tileLayer)
|
||||
);
|
||||
@@ -64,7 +78,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transformation the transformation from the time map coordinate
|
||||
* @param transformation the transformation from the tile map coordinate
|
||||
* (in pixels) to the scene coordinate (in pixels).
|
||||
*/
|
||||
setTransformation(transformation: gdjs.AffineTransformation) {
|
||||
|
2
Extensions/TileMap/helper/TileMapHelper.d.ts
vendored
2
Extensions/TileMap/helper/TileMapHelper.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
EditableTileMap,
|
||||
EditableTileMapLayer,
|
||||
EditableTileMapAsJsObject,
|
||||
PixiTileMapHelper,
|
||||
TileDefinition,
|
||||
TileMapFileContent,
|
||||
@@ -14,6 +15,7 @@ declare global {
|
||||
export {
|
||||
EditableTileMap,
|
||||
EditableTileMapLayer,
|
||||
EditableTileMapAsJsObject,
|
||||
PixiTileMapHelper,
|
||||
TileDefinition,
|
||||
TileMapFileContent,
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,4 +2,16 @@ export declare type integer = number;
|
||||
export declare type float = number;
|
||||
export type FloatPoint = [float, float];
|
||||
export type PolygonVertices = FloatPoint[];
|
||||
export type EditableTileMapLayerAsJsObject = {
|
||||
id: number;
|
||||
alpha: number;
|
||||
tiles: number[][];
|
||||
};
|
||||
export type EditableTileMapAsJsObject = {
|
||||
tileWidth: number;
|
||||
tileHeight: number;
|
||||
dimX: number;
|
||||
dimY: number;
|
||||
layers: EditableTileMapLayerAsJsObject[];
|
||||
};
|
||||
//# sourceMappingURL=CommonTypes.d.ts.map
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"CommonTypes.d.ts","sourceRoot":"","sources":["../../src/model/CommonTypes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,MAAM,OAAO,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,MAAM,CAAC;AACnC,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,UAAU,EAAE,CAAC"}
|
||||
{"version":3,"file":"CommonTypes.d.ts","sourceRoot":"","sources":["../../src/model/CommonTypes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,MAAM,OAAO,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,MAAM,CAAC;AACnC,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,UAAU,EAAE,CAAC;AAE3C,MAAM,MAAM,8BAA8B,GAAG;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,8BAA8B,EAAE,CAAC;CAC1C,CAAC"}
|
@@ -1,4 +1,10 @@
|
||||
import { PolygonVertices, integer, float } from './CommonTypes';
|
||||
import {
|
||||
PolygonVertices,
|
||||
integer,
|
||||
float,
|
||||
EditableTileMapAsJsObject,
|
||||
EditableTileMapLayerAsJsObject,
|
||||
} from './CommonTypes';
|
||||
/**
|
||||
* A tile map model.
|
||||
*
|
||||
@@ -22,11 +28,15 @@ export declare class EditableTileMap {
|
||||
/**
|
||||
* The number of tile columns in the map.
|
||||
*/
|
||||
private readonly dimX;
|
||||
private dimX;
|
||||
/**
|
||||
* The number of tile rows in the map.
|
||||
*/
|
||||
private readonly dimY;
|
||||
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.
|
||||
@@ -41,6 +51,27 @@ export declare class EditableTileMap {
|
||||
dimY: integer,
|
||||
tileSet: Map<integer, TileDefinition>
|
||||
);
|
||||
/**
|
||||
* Loads EditableTileMap from serialized data.
|
||||
* Uses object configuration as the source of truth as the serialized data
|
||||
* might contain expired data (if the tile set configuration has changed and
|
||||
* the serialized data was not updated).
|
||||
* @param editableTileMapAsJsObject Serialized editable tile map object
|
||||
* @param objectConfiguration
|
||||
*/
|
||||
static from(
|
||||
editableTileMapAsJsObject: EditableTileMapAsJsObject,
|
||||
{
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
}: {
|
||||
tileSize: number;
|
||||
tileSetColumnCount: number;
|
||||
tileSetRowCount: number;
|
||||
}
|
||||
): EditableTileMap;
|
||||
toJSObject(): Object;
|
||||
/**
|
||||
* @returns The tile map width in pixels.
|
||||
*/
|
||||
@@ -65,6 +96,28 @@ export declare class EditableTileMap {
|
||||
* @returns The number of tile rows in the map.
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* @param tileId The tile identifier
|
||||
* @returns The tile definition form the tile set.
|
||||
@@ -78,7 +131,12 @@ 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.
|
||||
*/
|
||||
addTileLayer(layer: EditableTileMapLayer): void;
|
||||
getTileLayer(id: integer): EditableTileMapLayer | null;
|
||||
/**
|
||||
* @param id The identifier of the new layer.
|
||||
* @returns The new layer.
|
||||
@@ -108,6 +166,27 @@ export declare class EditableTileMap {
|
||||
* @param resourceName The name of the resource
|
||||
*/
|
||||
setBackgroundResourceName(resourceName: string): void;
|
||||
/**
|
||||
* 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.
|
||||
@@ -128,10 +207,12 @@ declare abstract class AbstractEditableLayer {
|
||||
*/
|
||||
constructor(tileMap: EditableTileMap, id: integer);
|
||||
setVisible(visible: boolean): void;
|
||||
toJSObject(): Object;
|
||||
/**
|
||||
* @returns true if the layer is visible.
|
||||
*/
|
||||
isVisible(): boolean;
|
||||
isEmpty(): boolean;
|
||||
}
|
||||
/**
|
||||
* A layer where tiles are placed with pixel coordinates.
|
||||
@@ -144,6 +225,7 @@ export declare class EditableObjectLayer extends AbstractEditableLayer {
|
||||
*/
|
||||
constructor(tileMap: EditableTileMap, id: integer);
|
||||
add(object: TileObject): void;
|
||||
isEmpty(): boolean;
|
||||
}
|
||||
/**
|
||||
* A tile that is placed with pixel coordinates.
|
||||
@@ -191,13 +273,20 @@ export declare class TileObject {
|
||||
* A tile map layer with tile organized in grid.
|
||||
*/
|
||||
export declare class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
private readonly _tiles;
|
||||
private _tiles;
|
||||
private _alpha;
|
||||
/**
|
||||
* @param tileMap The layer tile map.
|
||||
* @param id The layer identifier.
|
||||
*/
|
||||
constructor(tileMap: EditableTileMap, id: integer);
|
||||
buildEmptyLayer(dimensionX: number, dimensionY: number): void;
|
||||
static from(
|
||||
editableTileMapLayerAsJsObject: EditableTileMapLayerAsJsObject,
|
||||
tileMap: EditableTileMap,
|
||||
isTileIdValid: (tileId: number) => boolean
|
||||
): EditableTileMapLayer;
|
||||
toJSObject(): Object;
|
||||
/**
|
||||
* The opacity (between 0-1) of the layer
|
||||
*/
|
||||
@@ -206,12 +295,37 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
* @param alpha The opacity between 0-1
|
||||
*/
|
||||
setAlpha(alpha: float): void;
|
||||
isEmpty(): boolean;
|
||||
reduceDimensions(
|
||||
columnsToPop: number,
|
||||
columnsToShift: number,
|
||||
rowsToPop: number,
|
||||
rowsToShift: number
|
||||
): void;
|
||||
increaseDimensions(
|
||||
columnsToAppend: number,
|
||||
columnsToUnshift: number,
|
||||
rowsToAppend: number,
|
||||
rowsToUnshift: number
|
||||
): void;
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileId The tile.
|
||||
*/
|
||||
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;
|
||||
getTrimmingData(): {
|
||||
rowsToShift: number;
|
||||
columnsToShift: number;
|
||||
rowsToPop: number;
|
||||
columnsToPop: number;
|
||||
};
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
import { EditableTileMap } from '../model/TileMapModel';
|
||||
import { TileTextureCache } from './TileTextureCache';
|
||||
import { TileMapFileContent } from '../load/TileMapFileContent';
|
||||
import { EditableTileMapAsJsObject } from '../model/CommonTypes';
|
||||
/**
|
||||
* A holder to share tile maps across the 2 extension objects.
|
||||
*
|
||||
@@ -43,6 +44,14 @@ export declare class TileMapManager {
|
||||
pako: any,
|
||||
callback: (tileMap: EditableTileMap | null) => void
|
||||
): void;
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: EditableTileMapAsJsObject,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
tileSetColumnCount: number,
|
||||
tileSetRowCount: number,
|
||||
callback: (tileMap: EditableTileMap) => void
|
||||
): void;
|
||||
/**
|
||||
* @param loadTileMap The method that loads the Tiled JSON file in memory.
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
@@ -65,6 +74,22 @@ export declare class TileMapManager {
|
||||
levelIndex: number,
|
||||
callback: (textureCache: TileTextureCache | null) => void
|
||||
): void;
|
||||
/**
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
* @param atlasImageResourceName The resource name of the atlas image.
|
||||
* @param tileSize
|
||||
* @param columnCount
|
||||
* @param rowCount
|
||||
* @param callback A function called when the tiles textures are split.
|
||||
*/
|
||||
getOrLoadSimpleTileMapTextureCache(
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
atlasImageResourceName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
callback: (textureCache: TileTextureCache | null) => void
|
||||
): void;
|
||||
clearCaches(): void;
|
||||
}
|
||||
//# sourceMappingURL=TileMapManager.d.ts.map
|
||||
|
@@ -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;;;;;;;;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,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"}
|
@@ -6,7 +6,7 @@ export declare namespace PixiTileMapHelper {
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
*
|
||||
* @param tiledMap A tile map exported from Tiled.
|
||||
* @param tiledMap A tile map exported from Tiled or LDtk.
|
||||
* @param levelIndex The level of the tile map to load from.
|
||||
* @param atlasTexture The texture containing the whole tile set.
|
||||
* @param getTexture A getter to load a texture. Used if atlasTexture is not specified.
|
||||
@@ -18,6 +18,21 @@ export declare namespace PixiTileMapHelper {
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
|
||||
): TileTextureCache | null;
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
*
|
||||
* @param atlasTexture The texture containing the whole tile set.
|
||||
* @param columnCount The number of columns.
|
||||
* @param rowCount The number of rows.
|
||||
* @param tileSize The squared tile size.
|
||||
* @returns A textures cache.
|
||||
*/
|
||||
function parseSimpleTileMapAtlas(
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource>,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
tileSize: number
|
||||
): TileTextureCache;
|
||||
/**
|
||||
* Re-renders the tile map whenever its rendering settings have been changed
|
||||
*
|
||||
|
@@ -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;;;;;;;;;;;;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"}
|
744
Extensions/TileMap/simpletilemapruntimeobject.ts
Normal file
744
Extensions/TileMap/simpletilemapruntimeobject.ts
Normal file
@@ -0,0 +1,744 @@
|
||||
/// <reference path="helper/TileMapHelper.d.ts" />
|
||||
namespace gdjs {
|
||||
export type SimpleTileMapObjectDataType = {
|
||||
content: {
|
||||
opacity: number;
|
||||
atlasImage: string;
|
||||
rowCount: number;
|
||||
columnCount: number;
|
||||
tileSize: number;
|
||||
tilesWithHitBox: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type SimpleTileMapObjectData = ObjectData &
|
||||
SimpleTileMapObjectDataType;
|
||||
|
||||
export type SimpleTileMapNetworkSyncDataType = {
|
||||
op: number;
|
||||
ai: string;
|
||||
wid: number;
|
||||
hei: number;
|
||||
// TODO: Support tilemap synchronization. Find an efficient way to send tiles changes.
|
||||
};
|
||||
|
||||
export type SimpleTileMapNetworkSyncData = ObjectNetworkSyncData &
|
||||
SimpleTileMapNetworkSyncDataType;
|
||||
|
||||
/**
|
||||
* Displays a SimpleTileMap object.
|
||||
*/
|
||||
export class SimpleTileMapRuntimeObject
|
||||
extends gdjs.RuntimeObject
|
||||
implements gdjs.Resizable, gdjs.Scalable, gdjs.OpacityHandler {
|
||||
/**
|
||||
* A reusable Point to avoid allocations.
|
||||
*/
|
||||
private static readonly workingPoint: FloatPoint = [0, 0];
|
||||
|
||||
_opacity: float;
|
||||
_atlasImage: string;
|
||||
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
|
||||
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
|
||||
readonly _rowCount: number;
|
||||
readonly _columnCount: number;
|
||||
readonly _tileSize: number;
|
||||
_displayMode = 'all';
|
||||
_layerIndex = 0;
|
||||
_initialTileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject | null = null;
|
||||
readonly _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;
|
||||
|
||||
// TODO: Add a debug mode like for TileMapCollisionMaskRuntimeObject to draw?
|
||||
|
||||
constructor(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
objectData: SimpleTileMapObjectDataType
|
||||
) {
|
||||
super(instanceContainer, objectData);
|
||||
this._opacity = objectData.content.opacity;
|
||||
this._atlasImage = objectData.content.atlasImage;
|
||||
this._rowCount = objectData.content.rowCount;
|
||||
this._columnCount = objectData.content.columnCount;
|
||||
this._tileSize = objectData.content.tileSize;
|
||||
this._initialTilesWithHitBox = (objectData.content
|
||||
.tilesWithHitBox as string)
|
||||
.split(',')
|
||||
.filter((id) => !!id)
|
||||
.map((idAsString) => parseInt(idAsString, 10));
|
||||
this._tileMapManager = gdjs.TileMap.TileMapRuntimeManager.getManager(
|
||||
instanceContainer
|
||||
);
|
||||
this._renderer = new gdjs.TileMapRuntimeObjectRenderer(
|
||||
this,
|
||||
instanceContainer
|
||||
);
|
||||
|
||||
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
getRendererObject() {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
if (this._isTileMapDirty) {
|
||||
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
|
||||
(textureName) => {
|
||||
return (this.getInstanceContainer()
|
||||
.getGame()
|
||||
.getImageManager()
|
||||
.getPIXITexture(textureName) as unknown) as PIXI.BaseTexture<
|
||||
PIXI.Resource
|
||||
>;
|
||||
},
|
||||
this._atlasImage,
|
||||
this._tileSize,
|
||||
this._columnCount,
|
||||
this._rowCount,
|
||||
(textureCache: TileMapHelper.TileTextureCache | null) => {
|
||||
if (!textureCache) {
|
||||
// getOrLoadTextureCache already log warns and errors.
|
||||
return;
|
||||
}
|
||||
this._renderer.refreshPixiTileMap(textureCache);
|
||||
}
|
||||
);
|
||||
if (this._collisionTileMap) {
|
||||
const tileMap = this._renderer.getTileMap();
|
||||
if (tileMap) this._collisionTileMap.updateFromTileMap(tileMap);
|
||||
}
|
||||
this._isTileMapDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateFromObjectData(
|
||||
oldObjectData: SimpleTileMapObjectData,
|
||||
newObjectData: SimpleTileMapObjectData
|
||||
): boolean {
|
||||
if (oldObjectData.content.opacity !== newObjectData.content.opacity) {
|
||||
this.setOpacity(newObjectData.content.opacity);
|
||||
}
|
||||
if (
|
||||
oldObjectData.content.atlasImage !== newObjectData.content.atlasImage
|
||||
) {
|
||||
// TODO: support changing the atlas texture
|
||||
return false;
|
||||
}
|
||||
// Map content is updated at hot-reload by extraInitializationFromInitialInstance.
|
||||
return true;
|
||||
}
|
||||
|
||||
getNetworkSyncData(): SimpleTileMapNetworkSyncData {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
op: this._opacity,
|
||||
ai: this._atlasImage,
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
updateFromNetworkSyncData(
|
||||
networkSyncData: SimpleTileMapNetworkSyncData
|
||||
): void {
|
||||
super.updateFromNetworkSyncData(networkSyncData);
|
||||
|
||||
if (
|
||||
networkSyncData.op !== undefined &&
|
||||
networkSyncData.op !== this._opacity
|
||||
) {
|
||||
this.setOpacity(networkSyncData.op);
|
||||
}
|
||||
if (
|
||||
networkSyncData.wid !== undefined &&
|
||||
networkSyncData.wid !== this.getWidth()
|
||||
) {
|
||||
this.setWidth(networkSyncData.wid);
|
||||
}
|
||||
if (
|
||||
networkSyncData.hei !== undefined &&
|
||||
networkSyncData.hei !== this.getHeight()
|
||||
) {
|
||||
this.setHeight(networkSyncData.hei);
|
||||
}
|
||||
if (networkSyncData.ai !== undefined) {
|
||||
// TODO: support changing the atlas texture
|
||||
}
|
||||
}
|
||||
|
||||
extraInitializationFromInitialInstance(
|
||||
initialInstanceData: InstanceData
|
||||
): void {
|
||||
// 1. load the tilemap from the instance.
|
||||
for (const property of initialInstanceData.stringProperties) {
|
||||
if (property.name === 'tilemap') {
|
||||
this._initialTileMapAsJsObject = JSON.parse(property.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update the renderer so that it updates the tilemap object
|
||||
// (used for width and position calculations).
|
||||
this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => {
|
||||
// 3. Set custom dimensions if applicable.
|
||||
if (initialInstanceData.customSize) {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
|
||||
// 4. Update position (calculations based on renderer's dimensions).
|
||||
this._renderer.updatePosition();
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
private _loadInitialTileMap(
|
||||
tileMapLoadingCallback: (tileMap: TileMapHelper.EditableTileMap) => void
|
||||
): void {
|
||||
if (!this._initialTileMapAsJsObject) return;
|
||||
|
||||
this._tileMapManager.getOrLoadSimpleTileMap(
|
||||
this._initialTileMapAsJsObject,
|
||||
this.name,
|
||||
this._tileSize,
|
||||
this._columnCount,
|
||||
this._rowCount,
|
||||
(tileMap: TileMapHelper.EditableTileMap) => {
|
||||
this._initialTilesWithHitBox.forEach((tileId) => {
|
||||
const tileDefinition = tileMap.getTileDefinition(tileId);
|
||||
if (!tileDefinition) {
|
||||
console.warn(
|
||||
`Could not set hit box for tile with id ${tileId}. Continuing.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
tileDefinition.addHitBox(this._hitBoxTag, [
|
||||
[0, 0],
|
||||
[0, tileMap.getTileHeight()],
|
||||
[tileMap.getTileWidth(), tileMap.getTileHeight()],
|
||||
[tileMap.getTileWidth(), 0],
|
||||
]);
|
||||
});
|
||||
|
||||
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
|
||||
(textureName) => {
|
||||
return (this.getInstanceContainer()
|
||||
.getGame()
|
||||
.getImageManager()
|
||||
.getPIXITexture(textureName) as unknown) as PIXI.BaseTexture<
|
||||
PIXI.Resource
|
||||
>;
|
||||
},
|
||||
this._atlasImage,
|
||||
this._tileSize,
|
||||
this._columnCount,
|
||||
this._rowCount,
|
||||
(textureCache: TileMapHelper.TileTextureCache | null) => {
|
||||
if (!textureCache) {
|
||||
// getOrLoadTextureCache already log warns and errors.
|
||||
return;
|
||||
}
|
||||
this._renderer.updatePixiTileMap(tileMap, textureCache);
|
||||
tileMapLoadingCallback(tileMap);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onDestroyed(): void {
|
||||
super.onDestroyed();
|
||||
this._renderer.destroy();
|
||||
}
|
||||
|
||||
setWidth(width: float): void {
|
||||
if (this.getWidth() === width) return;
|
||||
|
||||
this._transformationIsUpToDate = false;
|
||||
this._renderer.setWidth(width);
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
setHeight(height: float): void {
|
||||
if (this.getHeight() === height) return;
|
||||
|
||||
this._transformationIsUpToDate = false;
|
||||
this._renderer.setHeight(height);
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
setSize(newWidth: float, newHeight: float): void {
|
||||
this.setWidth(newWidth);
|
||||
this.setHeight(newHeight);
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*
|
||||
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*/
|
||||
getScale(): float {
|
||||
const scaleX = this.getScaleX();
|
||||
const scaleY = this.getScaleY();
|
||||
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X and Y axis of the object.
|
||||
*
|
||||
* @param scale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScale(scale: float): void {
|
||||
this.setScaleX(scale);
|
||||
this.setScaleY(scale);
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X axis of the object (changing its width).
|
||||
*
|
||||
* @param scaleX The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleX(scaleX: float): void {
|
||||
if (scaleX < 0) {
|
||||
scaleX = 0;
|
||||
}
|
||||
if (this.getScaleX() === scaleX) return;
|
||||
|
||||
this._renderer.setScaleX(scaleX);
|
||||
this.invalidateHitboxes();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on Y axis of the object (changing its width).
|
||||
*
|
||||
* @param scaleY The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleY(scaleY: float): void {
|
||||
if (scaleY < 0) {
|
||||
scaleY = 0;
|
||||
}
|
||||
if (this.getScaleY() === scaleY) return;
|
||||
|
||||
this._renderer.setScaleY(scaleY);
|
||||
this.invalidateHitboxes();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
setX(x: float): void {
|
||||
super.setX(x);
|
||||
this._renderer.updatePosition();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
setY(y: float): void {
|
||||
super.setY(y);
|
||||
this._renderer.updatePosition();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
setAngle(angle: float): void {
|
||||
super.setAngle(angle);
|
||||
this._renderer.updateAngle();
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
setOpacity(opacity: float): void {
|
||||
this._opacity = opacity;
|
||||
this._renderer.updateOpacity();
|
||||
this._isTileMapDirty = true;
|
||||
}
|
||||
|
||||
getOpacity(): float {
|
||||
return this._opacity;
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this._renderer.getWidth();
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
getScaleX(): float {
|
||||
return this._renderer.getScaleX();
|
||||
}
|
||||
|
||||
getScaleY(): float {
|
||||
return this._renderer.getScaleY();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is expensive and should not be called.
|
||||
* Prefer using {@link getHitBoxesAround} rather than getHitBoxes.
|
||||
*/
|
||||
getHitBoxes(): gdjs.Polygon[] {
|
||||
if (this.hitBoxesDirty) {
|
||||
this.updateHitBoxes();
|
||||
this.updateAABB();
|
||||
this.hitBoxesDirty = false;
|
||||
}
|
||||
return this.hitBoxes;
|
||||
}
|
||||
|
||||
updateHitBoxes(): void {
|
||||
this.updateTransformation();
|
||||
if (!this._collisionTileMap) return;
|
||||
this.hitBoxes = Array.from(
|
||||
this._collisionTileMap.getAllHitboxes(this._hitBoxTag)
|
||||
);
|
||||
this.hitBoxesDirty = false;
|
||||
this.updateAABB();
|
||||
}
|
||||
|
||||
// This implementation doesn't use updateHitBoxes.
|
||||
// It's important for good performances.
|
||||
updateAABB(): void {
|
||||
if (this.getAngle() === 0) {
|
||||
// Fast computation of AABB for non rotated object
|
||||
this.aabb.min[0] = this.x;
|
||||
this.aabb.min[1] = this.y;
|
||||
this.aabb.max[0] = this.aabb.min[0] + this.getWidth();
|
||||
this.aabb.max[1] = this.aabb.min[1] + this.getHeight();
|
||||
} else {
|
||||
if (!this._collisionTileMap) return;
|
||||
const affineTransformation = this._collisionTileMap.getTransformation();
|
||||
|
||||
const left = 0;
|
||||
const right = this._collisionTileMap.getWidth();
|
||||
const top = 0;
|
||||
const bottom = this._collisionTileMap.getHeight();
|
||||
|
||||
const workingPoint = this.aabb.min;
|
||||
|
||||
workingPoint[0] = left;
|
||||
workingPoint[1] = top;
|
||||
affineTransformation.transform(workingPoint, workingPoint);
|
||||
const topLeftX = workingPoint[0];
|
||||
const topLeftY = workingPoint[1];
|
||||
|
||||
workingPoint[0] = right;
|
||||
workingPoint[1] = top;
|
||||
affineTransformation.transform(workingPoint, workingPoint);
|
||||
const topRightX = workingPoint[0];
|
||||
const topRightY = workingPoint[1];
|
||||
|
||||
workingPoint[0] = right;
|
||||
workingPoint[1] = bottom;
|
||||
affineTransformation.transform(workingPoint, workingPoint);
|
||||
const bottomRightX = workingPoint[0];
|
||||
const bottomRightY = workingPoint[1];
|
||||
|
||||
workingPoint[0] = left;
|
||||
workingPoint[1] = bottom;
|
||||
affineTransformation.transform(workingPoint, workingPoint);
|
||||
const bottomLeftX = workingPoint[0];
|
||||
const bottomLeftY = workingPoint[1];
|
||||
|
||||
this.aabb.min[0] = Math.min(
|
||||
topLeftX,
|
||||
topRightX,
|
||||
bottomRightX,
|
||||
bottomLeftX
|
||||
);
|
||||
this.aabb.max[0] = Math.max(
|
||||
topLeftX,
|
||||
topRightX,
|
||||
bottomRightX,
|
||||
bottomLeftX
|
||||
);
|
||||
this.aabb.min[1] = Math.min(
|
||||
topLeftY,
|
||||
topRightY,
|
||||
bottomRightY,
|
||||
bottomLeftY
|
||||
);
|
||||
this.aabb.max[1] = Math.max(
|
||||
topLeftY,
|
||||
topRightY,
|
||||
bottomRightY,
|
||||
bottomLeftY
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getHitBoxesAround(
|
||||
left: float,
|
||||
top: float,
|
||||
right: float,
|
||||
bottom: float
|
||||
): Iterable<gdjs.Polygon> {
|
||||
// This implementation doesn't call updateHitBoxes.
|
||||
// It's important for good performances because there is no need to
|
||||
// update the whole collision mask where only a few hitboxes must be
|
||||
// checked.
|
||||
this.updateTransformation();
|
||||
if (!this._collisionTileMap) return [];
|
||||
return this._collisionTileMap.getHitboxesAround(
|
||||
this._hitBoxTag,
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom
|
||||
);
|
||||
}
|
||||
|
||||
updateTransformation() {
|
||||
if (this._transformationIsUpToDate) {
|
||||
return;
|
||||
}
|
||||
const absScaleX = Math.abs(this._renderer.getScaleX());
|
||||
const absScaleY = Math.abs(this._renderer.getScaleY());
|
||||
|
||||
this._tileMapToSceneTransformation.setToIdentity();
|
||||
|
||||
// Translation
|
||||
this._tileMapToSceneTransformation.translate(this.getX(), this.getY());
|
||||
|
||||
// Rotation
|
||||
const angleInRadians = (this.getAngle() * Math.PI) / 180;
|
||||
this._tileMapToSceneTransformation.rotateAround(
|
||||
angleInRadians,
|
||||
this.getCenterX(),
|
||||
this.getCenterY()
|
||||
);
|
||||
|
||||
// Scale
|
||||
this._tileMapToSceneTransformation.scale(absScaleX, absScaleY);
|
||||
if (this._collisionTileMap) {
|
||||
const collisionTileMapTransformation = this._collisionTileMap.getTransformation();
|
||||
collisionTileMapTransformation.copyFrom(
|
||||
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: float,
|
||||
y: float
|
||||
): [integer, integer] {
|
||||
this.updateTransformation();
|
||||
|
||||
const gridCoordinates: FloatPoint =
|
||||
SimpleTileMapRuntimeObject.workingPoint;
|
||||
this._sceneToTileMapTransformation.transform([x, y], gridCoordinates);
|
||||
|
||||
const columnIndex = Math.floor(gridCoordinates[0] / this._tileSize);
|
||||
const rowIndex = Math.floor(gridCoordinates[1] / this._tileSize);
|
||||
|
||||
return [columnIndex, rowIndex];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
setTileAtPosition(tileId: number, x: float, y: float) {
|
||||
const [
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
] = this.getGridCoordinatesFromSceneCoordinates(x, y);
|
||||
this.setTileAtGridCoordinates(tileId, columnIndex, rowIndex);
|
||||
}
|
||||
|
||||
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;
|
||||
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();
|
||||
}
|
||||
|
||||
getTilesetColumnCount(): integer {
|
||||
return this._columnCount;
|
||||
}
|
||||
|
||||
getTilesetRowCount(): integer {
|
||||
return this._rowCount;
|
||||
}
|
||||
}
|
||||
gdjs.registerObject(
|
||||
'TileMap::SimpleTileMap',
|
||||
gdjs.SimpleTileMapRuntimeObject
|
||||
);
|
||||
}
|
@@ -7,7 +7,9 @@ namespace gdjs {
|
||||
* @class TileMapRuntimeObjectPixiRenderer
|
||||
*/
|
||||
export class TileMapRuntimeObjectPixiRenderer {
|
||||
private _object: any;
|
||||
private _object:
|
||||
| gdjs.TileMapRuntimeObject
|
||||
| gdjs.SimpleTileMapRuntimeObject;
|
||||
private _tileMap: TileMapHelper.EditableTileMap | null = null;
|
||||
|
||||
private _pixiObject: PIXI.tilemap.CompositeTilemap;
|
||||
@@ -17,7 +19,9 @@ namespace gdjs {
|
||||
* @param instanceContainer The gdjs.RuntimeScene in which the object is
|
||||
*/
|
||||
constructor(
|
||||
runtimeObject: gdjs.TileMapRuntimeObject,
|
||||
runtimeObject:
|
||||
| gdjs.TileMapRuntimeObject
|
||||
| gdjs.SimpleTileMapRuntimeObject,
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
) {
|
||||
this._object = runtimeObject;
|
||||
@@ -55,11 +59,28 @@ namespace gdjs {
|
||||
this._pixiObject,
|
||||
tileMap,
|
||||
textureCache,
|
||||
// @ts-ignore
|
||||
this._object._displayMode,
|
||||
this._object._layerIndex
|
||||
);
|
||||
}
|
||||
|
||||
refreshPixiTileMap(textureCache: TileMapHelper.TileTextureCache) {
|
||||
if (!this._tileMap) return;
|
||||
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
|
||||
this._pixiObject,
|
||||
this._tileMap,
|
||||
textureCache,
|
||||
// @ts-ignore
|
||||
this._object._displayMode,
|
||||
this._object._layerIndex
|
||||
);
|
||||
}
|
||||
|
||||
getTileMap(): TileMapHelper.EditableTileMap | null {
|
||||
return this._tileMap;
|
||||
}
|
||||
|
||||
updatePosition(): void {
|
||||
this._pixiObject.pivot.x = this.getTileMapWidth() / 2;
|
||||
this._pixiObject.pivot.y = this.getTileMapHeight() / 2;
|
||||
@@ -72,7 +93,24 @@ 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;
|
||||
for (const layer of tileMap.getLayers()) {
|
||||
if (
|
||||
(this._object._displayMode === 'index' &&
|
||||
this._object._layerIndex !== layer.id) ||
|
||||
(this._object._displayMode === 'visible' && !layer.isVisible())
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (layer instanceof TileMapHelper.EditableTileMapLayer) {
|
||||
layer.setAlpha(this._pixiObject.alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTileMapWidth() {
|
||||
@@ -123,6 +161,116 @@ namespace gdjs {
|
||||
return this._pixiObject.scale.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
* @returns The tile's id.
|
||||
*/
|
||||
getTileId(x: integer, y: integer, layerIndex: integer): integer {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return -1;
|
||||
return tileMap.getTileId(x, y, layerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
* @param tileId The tile's id.
|
||||
*/
|
||||
setTileId(x: integer, y: integer, layerIndex: integer, tileId: number) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param layerIndex The layer index.
|
||||
*/
|
||||
removeTile(x: integer, y: integer, layerIndex: integer) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
return tileMap.removeTile(x, y, layerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetRowCount The number of rows to have.
|
||||
*/
|
||||
setGridRowCount(targetRowCount: integer) {
|
||||
const tileMap = this._tileMap;
|
||||
if (!tileMap) return;
|
||||
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 {
|
||||
// Keep textures because they are shared by all tile maps.
|
||||
this._pixiObject.destroy(false);
|
||||
|
@@ -33,7 +33,7 @@ namespace gdjs {
|
||||
TilemapNetworkSyncDataType;
|
||||
|
||||
/**
|
||||
* Displays a Tilemap object (mapeditor.org supported).
|
||||
* Displays a Tilemap object (LDtk and Tiled).
|
||||
*/
|
||||
export class TileMapRuntimeObject
|
||||
extends gdjs.RuntimeObject
|
||||
|
@@ -871,8 +871,13 @@ gd::String EventsCodeGenerator::GenerateObjectAction(
|
||||
// Create call
|
||||
gd::String call;
|
||||
if (instrInfos.codeExtraInformation.type == "number" ||
|
||||
instrInfos.codeExtraInformation.type == "string" ||
|
||||
instrInfos.codeExtraInformation.type == "boolean") {
|
||||
instrInfos.codeExtraInformation.type == "string" ||
|
||||
// Boolean actions declared with addExpressionAndConditionAndAction uses
|
||||
// MutatorAndOrAccessor even though they don't declare an operator parameter.
|
||||
// Boolean operators are only used with SetMutators or SetCustomCodeGenerator.
|
||||
(instrInfos.codeExtraInformation.type == "boolean" &&
|
||||
instrInfos.codeExtraInformation.accessType ==
|
||||
gd::InstructionMetadata::ExtraInformation::AccessType::Mutators)) {
|
||||
if (instrInfos.codeExtraInformation.accessType ==
|
||||
gd::InstructionMetadata::ExtraInformation::MutatorAndOrAccessor)
|
||||
call = GenerateOperatorCall(
|
||||
@@ -932,9 +937,14 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
|
||||
|
||||
// Create call
|
||||
gd::String call;
|
||||
if ((instrInfos.codeExtraInformation.type == "number" ||
|
||||
instrInfos.codeExtraInformation.type == "string" ||
|
||||
instrInfos.codeExtraInformation.type == "boolean")) {
|
||||
if (instrInfos.codeExtraInformation.type == "number" ||
|
||||
instrInfos.codeExtraInformation.type == "string" ||
|
||||
// Boolean actions declared with addExpressionAndConditionAndAction uses
|
||||
// MutatorAndOrAccessor even though they don't declare an operator parameter.
|
||||
// Boolean operators are only used with SetMutators or SetCustomCodeGenerator.
|
||||
(instrInfos.codeExtraInformation.type == "boolean" &&
|
||||
instrInfos.codeExtraInformation.accessType ==
|
||||
gd::InstructionMetadata::ExtraInformation::AccessType::Mutators)) {
|
||||
if (instrInfos.codeExtraInformation.accessType ==
|
||||
gd::InstructionMetadata::ExtraInformation::MutatorAndOrAccessor)
|
||||
call = GenerateOperatorCall(
|
||||
|
@@ -892,22 +892,28 @@ void CommonInstructionsExtension::GenerateLocalVariableInitializationCode(
|
||||
code += variableCodeName + ".setString(" +
|
||||
EventsCodeGenerator::ConvertToStringExplicit(variable.GetString()) +
|
||||
");\n";
|
||||
} else if (variable.GetType() == gd::Variable::Structure ||
|
||||
variable.GetType() == gd::Variable::Array) {
|
||||
} else if (variable.GetType() == gd::Variable::Structure) {
|
||||
const auto &childrenNames = variable.GetAllChildrenNames();
|
||||
for (const auto& childName : variable.GetAllChildrenNames()) {
|
||||
for (const auto &childName : variable.GetAllChildrenNames()) {
|
||||
auto &child = variable.GetChild(childName);
|
||||
|
||||
code += "{\n";
|
||||
GenerateLocalVariableInitializationCode(child, code, depth + 1);
|
||||
auto childCodeName = "variable" + gd::String::From(depth + 1);
|
||||
code += variableCodeName + ".addChild(" +
|
||||
EventsCodeGenerator::ConvertToStringExplicit(childName) +
|
||||
", " + childCodeName + ");\n";
|
||||
EventsCodeGenerator::ConvertToStringExplicit(childName) + ", " +
|
||||
childCodeName + ");\n";
|
||||
code += "}\n";
|
||||
}
|
||||
if (variable.GetType() == gd::Variable::Array) {
|
||||
code += variableCodeName + ".castTo('array');\n";
|
||||
} else if (variable.GetType() == gd::Variable::Array) {
|
||||
for (std::size_t i = 0; i < variable.GetChildrenCount(); i++) {
|
||||
auto &child = variable.GetAtIndex(i);
|
||||
|
||||
code += "{\n";
|
||||
GenerateLocalVariableInitializationCode(child, code, depth + 1);
|
||||
auto childCodeName = "variable" + gd::String::From(depth + 1);
|
||||
code += variableCodeName + "._pushVariable(" + childCodeName + ");\n";
|
||||
code += "}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget id="GDJS_PACKAGENAME" version="GDJS_PROJECTVERSION" xmlns="http://www.w3.org/ns/widgets"
|
||||
xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:cdv="http://cordova.apache.org/ns/1.0"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<name>GDJS_PROJECTNAME</name>
|
||||
<content src="index.html" />
|
||||
<plugin name="cordova-plugin-whitelist" version="1" />
|
||||
@@ -13,19 +14,24 @@
|
||||
<allow-intent href="mailto:*" />
|
||||
<allow-intent href="geo:*" />
|
||||
|
||||
<!-- Allow iframes on iOS like leaderboards, including those in development that are not served via https. -->
|
||||
<!-- Allow iframes on iOS like leaderboards, including those in development that are not served
|
||||
via https. -->
|
||||
<allow-navigation href="*" />
|
||||
|
||||
<platform name="android">
|
||||
<allow-intent href="market:*" />
|
||||
|
||||
<!-- Required by admob-plus plugin > version 2 -->
|
||||
<preference name="GradlePluginKotlinEnabled" value="true" />
|
||||
|
||||
<!-- Increase timeout value for low-end android devices -->
|
||||
<preference name="loadUrlTimeoutValue" value="60000" />
|
||||
|
||||
<!-- GDJS_ICONS_ANDROID -->
|
||||
<preference name="AndroidWindowSplashScreenBackground" value="#000000" />
|
||||
|
||||
<!-- Required to get cordova-plugin-safariviewcontroller to call Chrome CustomTabs on Android. -->
|
||||
<!-- Required to get cordova-plugin-safariviewcontroller to call Chrome CustomTabs on
|
||||
Android. -->
|
||||
<config-file target="AndroidManifest.xml" parent="/manifest">
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -40,6 +46,9 @@
|
||||
|
||||
<preference name="SwiftVersion" value="5.3" />
|
||||
|
||||
<!-- Required by admob-plus plugin > version 2 -->
|
||||
<preference name="deployment-target" value="12.0" />
|
||||
|
||||
<!-- GDJS_ICONS_IOS -->
|
||||
</platform>
|
||||
|
||||
|
@@ -395,9 +395,10 @@ namespace gdjs {
|
||||
let minX = 0;
|
||||
if (this._forcedDefaultSize) {
|
||||
minX = this._forcedDefaultSize.min[0];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
} else {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
minX = this._unrotatedAABB.min[0];
|
||||
}
|
||||
const absScaleX = this.getScaleX();
|
||||
@@ -416,9 +417,10 @@ namespace gdjs {
|
||||
let minY = 0;
|
||||
if (this._forcedDefaultSize) {
|
||||
minY = this._forcedDefaultSize.min[1];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
} else {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
minY = this._unrotatedAABB.min[1];
|
||||
}
|
||||
const absScaleY = this.getScaleY();
|
||||
|
@@ -318,7 +318,10 @@ namespace gdjs {
|
||||
this.layer = '';
|
||||
this._livingOnScene = true;
|
||||
//@ts-ignore Reinitialize is like a constructor, it can overwrite the readonly property.
|
||||
this.id = runtimeScene.createNewUniqueId();
|
||||
this.id = runtimeScene
|
||||
//
|
||||
.getScene()
|
||||
.createNewUniqueId();
|
||||
this.persistentUuid = null;
|
||||
this.networkId = null;
|
||||
this.pick = false;
|
||||
@@ -2541,12 +2544,14 @@ namespace gdjs {
|
||||
* @param obj1 The first runtimeObject
|
||||
* @param obj2 The second runtimeObject
|
||||
* @param ignoreTouchingEdges If true, then edges that are touching each other, without the hitbox polygons actually overlapping, won't be considered in collision.
|
||||
* @param ignoredObject1Polygon A polygon from the `obj1` collision mask to ignore
|
||||
* @return true if obj1 and obj2 are in collision
|
||||
*/
|
||||
static collisionTest(
|
||||
obj1: gdjs.RuntimeObject,
|
||||
obj2: gdjs.RuntimeObject,
|
||||
ignoreTouchingEdges: boolean
|
||||
ignoreTouchingEdges: boolean,
|
||||
ignoredObject1Polygon: gdjs.Polygon | null = null
|
||||
): boolean {
|
||||
//First check if bounding circle are too far.
|
||||
const o1centerX = obj1.getCenterX();
|
||||
@@ -2600,6 +2605,9 @@ namespace gdjs {
|
||||
);
|
||||
|
||||
for (const hitBox1 of hitBoxes1) {
|
||||
if (hitBox1 === ignoredObject1Polygon) {
|
||||
continue;
|
||||
}
|
||||
for (const hitBox2 of hitBoxes2) {
|
||||
if (
|
||||
gdjs.Polygon.collisionTest(hitBox1, hitBox2, ignoreTouchingEdges)
|
||||
|
@@ -37,6 +37,18 @@ namespace gdjs {
|
||||
if (this._isNextLayoutLoading || this._stack.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasMadeChangeToStack = this.applyUpdateFromNetworkSyncDataIfAny();
|
||||
if (hasMadeChangeToStack) {
|
||||
debugLogger.info(
|
||||
'Scene stack has been updated from network sync data, skipping step.'
|
||||
);
|
||||
// If we have made changes to the stack as part of the network sync,
|
||||
// we trust the network to be the source of truth for the scene stack,
|
||||
// and skip the scene rendering (and so any other request to change the scene stack from it)
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentScene = this._stack[this._stack.length - 1];
|
||||
if (currentScene.renderAndStep(elapsedTime)) {
|
||||
const request = currentScene.getRequestedChange();
|
||||
@@ -58,8 +70,6 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
this.applyUpdateFromNetworkSyncDataIfAny();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -232,9 +242,10 @@ namespace gdjs {
|
||||
this._sceneStackSyncDataToApply = sceneStackSyncData;
|
||||
}
|
||||
|
||||
applyUpdateFromNetworkSyncDataIfAny(): void {
|
||||
applyUpdateFromNetworkSyncDataIfAny(): boolean {
|
||||
const sceneStackSyncData = this._sceneStackSyncDataToApply;
|
||||
if (!sceneStackSyncData) return;
|
||||
let hasMadeChangeToStack = false;
|
||||
if (!sceneStackSyncData) return hasMadeChangeToStack;
|
||||
|
||||
this._sceneStackSyncDataToApply = null;
|
||||
|
||||
@@ -251,11 +262,12 @@ namespace gdjs {
|
||||
debugLogger.info(
|
||||
`Scene at position ${i} with name ${sceneSyncData.name} is missing from the stack, adding it.`
|
||||
);
|
||||
// We have less scenes in the stack than the host, let's add the scene.
|
||||
// We have fewer scenes in the stack than the host, let's add the scene.
|
||||
const newScene = this.push(sceneSyncData.name);
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
}
|
||||
hasMadeChangeToStack = true;
|
||||
// Continue to the next scene in the stack received from the host.
|
||||
continue;
|
||||
}
|
||||
@@ -275,6 +287,7 @@ namespace gdjs {
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
}
|
||||
hasMadeChangeToStack = true;
|
||||
// Continue to the next scene in the stack received from the host.
|
||||
continue;
|
||||
}
|
||||
@@ -317,6 +330,7 @@ namespace gdjs {
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
}
|
||||
hasMadeChangeToStack = true;
|
||||
// Continue to the next scene in the stack received from the host.
|
||||
continue;
|
||||
}
|
||||
@@ -330,7 +344,10 @@ namespace gdjs {
|
||||
if (this._stack.length > sceneStackSyncData.length) {
|
||||
const popCount = this._stack.length - sceneStackSyncData.length;
|
||||
this.pop(popCount);
|
||||
hasMadeChangeToStack = true;
|
||||
}
|
||||
|
||||
return hasMadeChangeToStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -620,6 +620,16 @@ namespace gdjs {
|
||||
this._childrenArray.push(variable.clone());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a variable into the array without duplicating it first.
|
||||
* This should only be used by generated code.
|
||||
*/
|
||||
_pushVariable(variable: gdjs.Variable) {
|
||||
if (this._type !== 'array') this.castTo('array');
|
||||
|
||||
this._childrenArray.push(variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a value into the array.
|
||||
*/
|
||||
|
@@ -521,6 +521,7 @@ namespace gdjs {
|
||||
return [];
|
||||
},
|
||||
pushVariableCopy: () => {},
|
||||
_pushVariable: () => {},
|
||||
pushValue: () => {},
|
||||
removeAtIndex: function () {
|
||||
return;
|
||||
|
@@ -226,6 +226,7 @@ interface PairStringVariable {
|
||||
|
||||
enum Variable_Type {
|
||||
"Variable::Unknown",
|
||||
"Variable::MixedTypes",
|
||||
"Variable::String",
|
||||
"Variable::Number",
|
||||
"Variable::Boolean",
|
||||
@@ -264,6 +265,7 @@ interface Variable {
|
||||
double GetValue();
|
||||
void SetBool([Const] boolean val);
|
||||
boolean GetBool();
|
||||
boolean HasMixedValues();
|
||||
void SetFolded(boolean val);
|
||||
boolean IsFolded();
|
||||
|
||||
@@ -352,6 +354,16 @@ interface ObjectGroup {
|
||||
void UnserializeFrom([Const, Ref] SerializerElement element);
|
||||
};
|
||||
|
||||
interface GroupVariableHelper {
|
||||
[Value] VariablesContainer STATIC_MergeVariableContainers(
|
||||
[Const, Ref] ObjectsContainersList objectsContainersList,
|
||||
[Const, Ref] ObjectGroup objectGroup);
|
||||
void STATIC_FillAnyVariableBetweenObjects(
|
||||
[Ref] ObjectsContainer globalObjectsContainer,
|
||||
[Ref] ObjectsContainer objectsContainer,
|
||||
[Const, Ref] ObjectGroup objectGroup);
|
||||
};
|
||||
|
||||
interface ObjectGroupsContainer {
|
||||
void ObjectGroupsContainer();
|
||||
|
||||
@@ -647,6 +659,7 @@ interface ObjectsContainersList {
|
||||
[Const, Value] DOMString GetTypeOfObject([Const] DOMString objectName);
|
||||
[Const, Value] DOMString GetTypeOfBehavior([Const] DOMString name, boolean searchInGroups);
|
||||
[Value] VectorString GetBehaviorsOfObject([Const] DOMString name, boolean searchInGroups);
|
||||
[Value] VectorString GetAnimationNamesOfObject([Const] DOMString name);
|
||||
[Const, Value] DOMString GetTypeOfBehaviorInObjectOrGroup([Const] DOMString objectOrGroupName, [Const] DOMString behaviorName, boolean searchInGroups);
|
||||
boolean HasObjectOrGroupNamed([Const] DOMString name);
|
||||
|
||||
@@ -2346,6 +2359,14 @@ interface WholeProjectRefactorer {
|
||||
[Ref] VariablesContainer newVariablesContainer,
|
||||
[Const, Ref] VariablesChangeset changeset,
|
||||
[Const, Ref] SerializerElement originalSerializedVariables);
|
||||
void STATIC_ApplyRefactoringForGroupVariablesContainer(
|
||||
[Ref] Project project,
|
||||
[Ref] ObjectsContainer globalObjectsContainer,
|
||||
[Ref] ObjectsContainer objectsContainer,
|
||||
[Const, Ref] VariablesContainer groupVariablesContainer,
|
||||
[Const, Ref] ObjectGroup objectGroup,
|
||||
[Const, Ref] VariablesChangeset changeset,
|
||||
[Const, Ref] SerializerElement originalSerializedVariables);
|
||||
void STATIC_RenameEventsFunctionsExtension(
|
||||
[Ref] Project project,
|
||||
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
|
||||
@@ -2433,43 +2454,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,
|
||||
|
@@ -47,6 +47,7 @@
|
||||
#include <GDCore/IDE/Events/TextFormatting.h>
|
||||
#include <GDCore/IDE/Events/UsedExtensionsFinder.h>
|
||||
#include <GDCore/IDE/EventsFunctionTools.h>
|
||||
#include <GDCore/IDE/GroupVariableHelper.h>
|
||||
#include <GDCore/IDE/Project/ArbitraryResourceWorker.h>
|
||||
#include <GDCore/IDE/Project/ArbitraryObjectsWorker.h>
|
||||
#include <GDCore/IDE/Project/ObjectsUsingResourceCollector.h>
|
||||
@@ -646,9 +647,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 \
|
||||
@@ -723,8 +724,12 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
#define STATIC_GetBehaviorFullType GetBehaviorFullType
|
||||
#define STATIC_ApplyRefactoringForVariablesContainer \
|
||||
ApplyRefactoringForVariablesContainer
|
||||
#define STATIC_ApplyRefactoringForGroupVariablesContainer \
|
||||
ApplyRefactoringForGroupVariablesContainer
|
||||
#define STATIC_ComputeChangesetForVariablesContainer \
|
||||
ComputeChangesetForVariablesContainer
|
||||
#define STATIC_MergeVariableContainers MergeVariableContainers
|
||||
#define STATIC_FillAnyVariableBetweenObjects FillAnyVariableBetweenObjects
|
||||
#define STATIC_RenameEventsFunctionsExtension RenameEventsFunctionsExtension
|
||||
#define STATIC_UpdateExtensionNameInEventsBasedBehavior \
|
||||
UpdateExtensionNameInEventsBasedBehavior
|
||||
@@ -746,11 +751,16 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
#define STATIC_RenameLayout RenameLayout
|
||||
#define STATIC_RenameExternalLayout RenameExternalLayout
|
||||
#define STATIC_RenameExternalEvents RenameExternalEvents
|
||||
#define STATIC_RenameLayer RenameLayer
|
||||
#define STATIC_RenameLayerEffect RenameLayerEffect
|
||||
#define STATIC_RenameObjectAnimation RenameObjectAnimation
|
||||
#define STATIC_RenameObjectPoint RenameObjectPoint
|
||||
#define STATIC_RenameObjectEffect RenameObjectEffect
|
||||
#define STATIC_RenameLayerInScene RenameLayerInScene
|
||||
#define STATIC_RenameLayerEffectInScene RenameLayerEffectInScene
|
||||
#define STATIC_RenameObjectAnimationInScene RenameObjectAnimationInScene
|
||||
#define STATIC_RenameObjectPointInScene RenameObjectPointInScene
|
||||
#define STATIC_RenameObjectEffectInScene RenameObjectEffectInScene
|
||||
#define STATIC_RenameLayerInEventsBasedObject RenameLayerInEventsBasedObject
|
||||
#define STATIC_RenameLayerEffectInEventsBasedObject RenameLayerEffectInEventsBasedObject
|
||||
#define STATIC_RenameObjectAnimationInEventsBasedObject RenameObjectAnimationInEventsBasedObject
|
||||
#define STATIC_RenameObjectPointInEventsBasedObject RenameObjectPointInEventsBasedObject
|
||||
#define STATIC_RenameObjectEffectInEventsBasedObject RenameObjectEffectInEventsBasedObject
|
||||
|
||||
#define STATIC_GetBehaviorPropertyGetterName GetBehaviorPropertyGetterName
|
||||
#define STATIC_GetBehaviorPropertySetterName GetBehaviorPropertySetterName
|
||||
|
@@ -252,6 +252,13 @@ class Variable {
|
||||
this._childrenArray.push(variable.clone());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Variable} variable
|
||||
*/
|
||||
_pushVariable(variable) {
|
||||
this._childrenArray.push(variable);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
|
@@ -3643,7 +3643,7 @@ describe('libGD.js', function () {
|
||||
instance1.setObjectName('Object1');
|
||||
instance2.setObjectName('Object2');
|
||||
|
||||
gd.WholeProjectRefactorer.objectOrGroupRenamedInLayout(
|
||||
gd.WholeProjectRefactorer.objectOrGroupRenamedInScene(
|
||||
project,
|
||||
layout,
|
||||
'Object1',
|
||||
@@ -3660,7 +3660,7 @@ describe('libGD.js', function () {
|
||||
true
|
||||
);
|
||||
|
||||
gd.WholeProjectRefactorer.objectRemovedInLayout(
|
||||
gd.WholeProjectRefactorer.objectRemovedInScene(
|
||||
project,
|
||||
layout,
|
||||
'Object3',
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -420,7 +420,7 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local child-variable condition', function () {
|
||||
it('can generate a local child-variable condition on a structure', function () {
|
||||
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
const runtimeScene = generateAndRunEventsForFunction([
|
||||
{
|
||||
@@ -455,6 +455,41 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local child-variable condition on an array', function () {
|
||||
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
const runtimeScene = generateAndRunEventsForFunction([
|
||||
{
|
||||
type: 'BuiltinCommonInstructions::Standard',
|
||||
variables: [
|
||||
{
|
||||
name: 'MyLocalVariable',
|
||||
type: 'array',
|
||||
children: [{ name: '0', type: 'number', value: 123 }],
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
type: { inverted: false, value: 'NumberVariable' },
|
||||
parameters: ['MyLocalVariable[0]', '=', '123'],
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: { value: 'SetNumberVariable' },
|
||||
parameters: ['SuccessVariable', '=', '1'],
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
runtimeScene
|
||||
.getVariablesForExtension('Extension')
|
||||
.get('SuccessVariable')
|
||||
.getAsNumber()
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local variable condition giving precedence to the closest local variable', function () {
|
||||
extension.getSceneVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
|
||||
|
@@ -346,7 +346,7 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local child-variable condition', function () {
|
||||
it('can generate a local child-variable condition on a structure', function () {
|
||||
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
const runtimeScene = generateAndRunEventsForLayout([
|
||||
{
|
||||
@@ -378,6 +378,38 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local child-variable condition on an array', function () {
|
||||
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
const runtimeScene = generateAndRunEventsForLayout([
|
||||
{
|
||||
type: 'BuiltinCommonInstructions::Standard',
|
||||
variables: [
|
||||
{
|
||||
name: 'MyLocalVariable',
|
||||
type: 'array',
|
||||
children: [{ name: '0', type: 'number', value: 123 }],
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
type: { inverted: false, value: 'NumberVariable' },
|
||||
parameters: ['MyLocalVariable[0]', '=', '123'],
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: { value: 'SetNumberVariable' },
|
||||
parameters: ['SuccessVariable', '=', '1'],
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('can generate a local variable condition giving precedence to the closest local variable', function () {
|
||||
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
|
||||
|
||||
|
@@ -105,7 +105,7 @@ type EventsFunctionsContainer_FunctionOwner = 0 | 1 | 2`
|
||||
fs.writeFileSync(
|
||||
'types/variable_type.js',
|
||||
`// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5`
|
||||
type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5 | 6`
|
||||
);
|
||||
shell.sed(
|
||||
'-i',
|
||||
@@ -113,11 +113,12 @@ type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5`
|
||||
[
|
||||
'declare class gdVariable {',
|
||||
' static Unknown: 0;',
|
||||
' static String: 1;',
|
||||
' static Number: 2;',
|
||||
' static Boolean: 3;',
|
||||
' static Structure: 4;',
|
||||
' static Array: 5;',
|
||||
' static MixedTypes: 1;',
|
||||
' static String: 2;',
|
||||
' static Number: 3;',
|
||||
' static Boolean: 4;',
|
||||
' static Structure: 5;',
|
||||
' static Array: 6;',
|
||||
].join('\n'),
|
||||
'types/gdvariable.js'
|
||||
);
|
||||
|
40
GDevelop.js/types.d.ts
vendored
40
GDevelop.js/types.d.ts
vendored
@@ -21,11 +21,12 @@ declare class EmscriptenObject {
|
||||
|
||||
export enum Variable_Type {
|
||||
Unknown = 0,
|
||||
String = 1,
|
||||
Number = 2,
|
||||
Boolean = 3,
|
||||
Structure = 4,
|
||||
Array = 5,
|
||||
MixedTypes = 1,
|
||||
String = 2,
|
||||
Number = 3,
|
||||
Boolean = 4,
|
||||
Structure = 5,
|
||||
Array = 6,
|
||||
}
|
||||
|
||||
export enum VariablesContainer_SourceType {
|
||||
@@ -279,6 +280,7 @@ export class Variable extends EmscriptenObject {
|
||||
getValue(): number;
|
||||
setBool(val: boolean): void;
|
||||
getBool(): boolean;
|
||||
hasMixedValues(): boolean;
|
||||
setFolded(val: boolean): void;
|
||||
isFolded(): boolean;
|
||||
getChildrenCount(): number;
|
||||
@@ -346,6 +348,11 @@ export class ObjectGroup extends EmscriptenObject {
|
||||
unserializeFrom(element: SerializerElement): void;
|
||||
}
|
||||
|
||||
export class GroupVariableHelper extends EmscriptenObject {
|
||||
static mergeVariableContainers(objectsContainersList: ObjectsContainersList, objectGroup: ObjectGroup): VariablesContainer;
|
||||
static fillAnyVariableBetweenObjects(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer, objectGroup: ObjectGroup): void;
|
||||
}
|
||||
|
||||
export class ObjectGroupsContainer extends EmscriptenObject {
|
||||
constructor();
|
||||
has(name: string): boolean;
|
||||
@@ -583,6 +590,7 @@ export class ObjectsContainersList extends EmscriptenObject {
|
||||
getTypeOfObject(objectName: string): string;
|
||||
getTypeOfBehavior(name: string, searchInGroups: boolean): string;
|
||||
getBehaviorsOfObject(name: string, searchInGroups: boolean): VectorString;
|
||||
getAnimationNamesOfObject(name: string): VectorString;
|
||||
getTypeOfBehaviorInObjectOrGroup(objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
|
||||
hasObjectOrGroupNamed(name: string): boolean;
|
||||
hasObjectOrGroupWithVariableNamed(objectName: string, variableName: string): ObjectsContainersList_VariableExistence;
|
||||
@@ -1794,6 +1802,7 @@ export class VariablesChangeset extends EmscriptenObject {
|
||||
export class WholeProjectRefactorer extends EmscriptenObject {
|
||||
static computeChangesetForVariablesContainer(oldSerializedVariablesContainer: SerializerElement, newVariablesContainer: VariablesContainer): VariablesChangeset;
|
||||
static applyRefactoringForVariablesContainer(project: Project, newVariablesContainer: VariablesContainer, changeset: VariablesChangeset, originalSerializedVariables: SerializerElement): void;
|
||||
static applyRefactoringForGroupVariablesContainer(project: Project, globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer, groupVariablesContainer: VariablesContainer, objectGroup: ObjectGroup, changeset: VariablesChangeset, originalSerializedVariables: SerializerElement): void;
|
||||
static renameEventsFunctionsExtension(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldName: string, newName: string): void;
|
||||
static updateExtensionNameInEventsBasedBehavior(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedBehavior: EventsBasedBehavior, sourceExtensionName: string): void;
|
||||
static renameEventsFunction(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldName: string, newName: string): void;
|
||||
@@ -1810,14 +1819,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;
|
||||
|
7
GDevelop.js/types/gdgroupvariablehelper.js
Normal file
7
GDevelop.js/types/gdgroupvariablehelper.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdGroupVariableHelper {
|
||||
static mergeVariableContainers(objectsContainersList: gdObjectsContainersList, objectGroup: gdObjectGroup): gdVariablesContainer;
|
||||
static fillAnyVariableBetweenObjects(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectGroup: gdObjectGroup): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -10,6 +10,7 @@ declare class gdObjectsContainersList {
|
||||
getTypeOfObject(objectName: string): string;
|
||||
getTypeOfBehavior(name: string, searchInGroups: boolean): string;
|
||||
getBehaviorsOfObject(name: string, searchInGroups: boolean): gdVectorString;
|
||||
getAnimationNamesOfObject(name: string): gdVectorString;
|
||||
getTypeOfBehaviorInObjectOrGroup(objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
|
||||
hasObjectOrGroupNamed(name: string): boolean;
|
||||
hasObjectOrGroupWithVariableNamed(objectName: string, variableName: string): ObjectsContainersList_VariableExistence;
|
||||
|
@@ -1,11 +1,12 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdVariable {
|
||||
static Unknown: 0;
|
||||
static String: 1;
|
||||
static Number: 2;
|
||||
static Boolean: 3;
|
||||
static Structure: 4;
|
||||
static Array: 5;
|
||||
static MixedTypes: 1;
|
||||
static String: 2;
|
||||
static Number: 3;
|
||||
static Boolean: 4;
|
||||
static Structure: 5;
|
||||
static Array: 6;
|
||||
constructor(): void;
|
||||
static isPrimitive(type: Variable_Type): boolean;
|
||||
getType(): Variable_Type;
|
||||
@@ -16,6 +17,7 @@ declare class gdVariable {
|
||||
getValue(): number;
|
||||
setBool(val: boolean): void;
|
||||
getBool(): boolean;
|
||||
hasMixedValues(): boolean;
|
||||
setFolded(val: boolean): void;
|
||||
isFolded(): boolean;
|
||||
getChildrenCount(): number;
|
||||
|
@@ -2,6 +2,7 @@
|
||||
declare class gdWholeProjectRefactorer {
|
||||
static computeChangesetForVariablesContainer(oldSerializedVariablesContainer: gdSerializerElement, newVariablesContainer: gdVariablesContainer): gdVariablesChangeset;
|
||||
static applyRefactoringForVariablesContainer(project: gdProject, newVariablesContainer: gdVariablesContainer, changeset: gdVariablesChangeset, originalSerializedVariables: gdSerializerElement): void;
|
||||
static applyRefactoringForGroupVariablesContainer(project: gdProject, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, groupVariablesContainer: gdVariablesContainer, objectGroup: gdObjectGroup, changeset: gdVariablesChangeset, originalSerializedVariables: gdSerializerElement): void;
|
||||
static renameEventsFunctionsExtension(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, newName: string): void;
|
||||
static updateExtensionNameInEventsBasedBehavior(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedBehavior: gdEventsBasedBehavior, sourceExtensionName: string): void;
|
||||
static renameEventsFunction(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, newName: string): void;
|
||||
@@ -18,14 +19,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;
|
||||
|
@@ -71,6 +71,7 @@ declare class libGDevelop {
|
||||
VariablesContainer: Class<gdVariablesContainer>;
|
||||
VariablesContainersList: Class<gdVariablesContainersList>;
|
||||
ObjectGroup: Class<gdObjectGroup>;
|
||||
GroupVariableHelper: Class<gdGroupVariableHelper>;
|
||||
ObjectGroupsContainer: Class<gdObjectGroupsContainer>;
|
||||
PlatformSpecificAssets: Class<gdPlatformSpecificAssets>;
|
||||
LoadingScreen: Class<gdLoadingScreen>;
|
||||
|
@@ -1,2 +1,2 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5
|
||||
type Variable_Type = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
@@ -75,7 +75,7 @@ export namespace LDtkTileMapLoader {
|
||||
gridSize,
|
||||
dimX,
|
||||
dimY,
|
||||
tileSet
|
||||
tileSet,
|
||||
);
|
||||
const composedTileMap = new Map<string, TileDefinition>();
|
||||
let nextComposedTileId = 0xfffffff;
|
||||
@@ -89,7 +89,7 @@ export namespace LDtkTileMapLoader {
|
||||
const gridSize = layer.__gridSize;
|
||||
const tilesetId = layer.__tilesetDefUid;
|
||||
|
||||
const editableTileLayer = editableTileMap.addTileLayer(iLayer);
|
||||
const editableTileLayer = editableTileMap.addNewTileLayer(iLayer);
|
||||
editableTileLayer.setAlpha(layer.__opacity);
|
||||
editableTileLayer.setVisible(layer.visible);
|
||||
|
||||
|
@@ -117,7 +117,7 @@ export namespace TiledTileMapLoader {
|
||||
tiledTileMap.tileheight,
|
||||
tiledTileMap.width,
|
||||
tiledTileMap.height,
|
||||
definitions
|
||||
definitions,
|
||||
);
|
||||
|
||||
for (const tiledLayer of tiledTileMap.layers) {
|
||||
@@ -156,7 +156,7 @@ export namespace TiledTileMapLoader {
|
||||
layerData = tiledLayer.data as integer[];
|
||||
}
|
||||
if (layerData) {
|
||||
const collisionTileLayer = collisionTileMap.addTileLayer(
|
||||
const collisionTileLayer = collisionTileMap.addNewTileLayer(
|
||||
tiledLayer.id
|
||||
);
|
||||
collisionTileLayer.setAlpha(tiledLayer.opacity);
|
||||
|
@@ -3,3 +3,17 @@ export declare type float = number;
|
||||
export type FloatPoint = [float, float];
|
||||
|
||||
export type PolygonVertices = FloatPoint[];
|
||||
|
||||
export type EditableTileMapLayerAsJsObject = {
|
||||
id: number;
|
||||
alpha: number;
|
||||
tiles: number[][];
|
||||
};
|
||||
|
||||
export type EditableTileMapAsJsObject = {
|
||||
tileWidth: number;
|
||||
tileHeight: number;
|
||||
dimX: number;
|
||||
dimY: number;
|
||||
layers: EditableTileMapLayerAsJsObject[];
|
||||
};
|
||||
|
@@ -1,4 +1,10 @@
|
||||
import { PolygonVertices, integer, float } from "./CommonTypes";
|
||||
import {
|
||||
PolygonVertices,
|
||||
integer,
|
||||
float,
|
||||
EditableTileMapAsJsObject,
|
||||
EditableTileMapLayerAsJsObject,
|
||||
} from "./CommonTypes";
|
||||
import { FlippingHelper } from "./GID";
|
||||
|
||||
/**
|
||||
@@ -24,11 +30,15 @@ export class EditableTileMap {
|
||||
/**
|
||||
* The number of tile columns in the map.
|
||||
*/
|
||||
private readonly dimX: integer;
|
||||
private dimX: integer;
|
||||
/**
|
||||
* The number of tile rows in the map.
|
||||
*/
|
||||
private readonly dimY: integer;
|
||||
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.
|
||||
@@ -54,6 +64,70 @@ export class EditableTileMap {
|
||||
this._layers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads EditableTileMap from serialized data.
|
||||
* Uses object configuration as the source of truth as the serialized data
|
||||
* might contain expired data (if the tile set configuration has changed and
|
||||
* the serialized data was not updated).
|
||||
* @param editableTileMapAsJsObject Serialized editable tile map object
|
||||
* @param objectConfiguration
|
||||
*/
|
||||
static from(
|
||||
editableTileMapAsJsObject: EditableTileMapAsJsObject,
|
||||
{
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
}: {
|
||||
tileSize: number;
|
||||
tileSetColumnCount: number;
|
||||
tileSetRowCount: number;
|
||||
}
|
||||
): EditableTileMap {
|
||||
const tileSet = new Map<number, TileDefinition>();
|
||||
|
||||
// TODO: Actually save and load tile set when useful.
|
||||
new Array(tileSetColumnCount * tileSetRowCount)
|
||||
.fill(0)
|
||||
.forEach((_, index) => {
|
||||
tileSet.set(index, new TileDefinition(0));
|
||||
});
|
||||
|
||||
const tileMap = new EditableTileMap(
|
||||
tileSize || editableTileMapAsJsObject.tileWidth,
|
||||
tileSize || editableTileMapAsJsObject.tileHeight,
|
||||
editableTileMapAsJsObject.dimX || 1,
|
||||
editableTileMapAsJsObject.dimY || 1,
|
||||
tileSet
|
||||
);
|
||||
|
||||
if (editableTileMapAsJsObject.layers) {
|
||||
editableTileMapAsJsObject.layers.forEach((layerAsJsObject: any) => {
|
||||
tileMap.addTileLayer(
|
||||
EditableTileMapLayer.from(
|
||||
layerAsJsObject,
|
||||
tileMap,
|
||||
(tileId) => tileId < tileSetColumnCount * tileSetRowCount
|
||||
)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
tileMap.addNewTileLayer(0);
|
||||
}
|
||||
|
||||
return tileMap;
|
||||
}
|
||||
|
||||
toJSObject(): Object {
|
||||
return {
|
||||
tileWidth: this.tileWidth,
|
||||
tileHeight: this.tileHeight,
|
||||
dimX: this.dimX,
|
||||
dimY: this.dimY,
|
||||
layers: this._layers.map((layer) => layer.toJSObject()),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The tile map width in pixels.
|
||||
*/
|
||||
@@ -96,6 +170,85 @@ export class EditableTileMap {
|
||||
return this.dimY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tileId The tile identifier
|
||||
* @returns The tile definition form the tile set.
|
||||
@@ -115,12 +268,25 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param layer the new layer to set.
|
||||
*/
|
||||
addTileLayer(layer: EditableTileMapLayer): void {
|
||||
this._layers.push(layer);
|
||||
}
|
||||
|
||||
getTileLayer(id: integer): EditableTileMapLayer | null {
|
||||
const matchingLayer = this._layers.find((layer) => layer.id === id);
|
||||
if (!(matchingLayer instanceof EditableTileMapLayer)) return null;
|
||||
return matchingLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The identifier of the new layer.
|
||||
* @returns The new layer.
|
||||
@@ -182,6 +348,112 @@ export class EditableTileMap {
|
||||
setBackgroundResourceName(resourceName: string): void {
|
||||
this._backgroundResourceName = resourceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all layers contain no defined tiled.
|
||||
*/
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,12 +483,20 @@ abstract class AbstractEditableLayer {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
toJSObject(): Object {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the layer is visible.
|
||||
*/
|
||||
isVisible(): boolean {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,6 +517,10 @@ export class EditableObjectLayer extends AbstractEditableLayer {
|
||||
add(object: TileObject): void {
|
||||
this.objects.push(object);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.objects.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,7 +605,7 @@ export class TileObject {
|
||||
* A tile map layer with tile organized in grid.
|
||||
*/
|
||||
export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
private readonly _tiles: Int32Array[];
|
||||
private _tiles: Int32Array[];
|
||||
private _alpha: float;
|
||||
|
||||
/**
|
||||
@@ -330,14 +614,61 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
*/
|
||||
constructor(tileMap: EditableTileMap, id: integer) {
|
||||
super(tileMap, id);
|
||||
this._tiles = [];
|
||||
this._tiles.length = this.tileMap.getDimensionY();
|
||||
for (let index = 0; index < this._tiles.length; index++) {
|
||||
this._tiles[index] = new Int32Array(this.tileMap.getDimensionX());
|
||||
}
|
||||
this.buildEmptyLayer(
|
||||
this.tileMap.getDimensionX(),
|
||||
this.tileMap.getDimensionY()
|
||||
);
|
||||
this._alpha = 1;
|
||||
}
|
||||
|
||||
buildEmptyLayer(dimensionX: number, dimensionY: number) {
|
||||
this._tiles = [];
|
||||
this._tiles.length = dimensionY;
|
||||
for (let index = 0; index < this._tiles.length; index++) {
|
||||
this._tiles[index] = new Int32Array(dimensionX);
|
||||
}
|
||||
}
|
||||
|
||||
static from(
|
||||
editableTileMapLayerAsJsObject: EditableTileMapLayerAsJsObject,
|
||||
tileMap: EditableTileMap,
|
||||
isTileIdValid: (tileId: number) => boolean
|
||||
): EditableTileMapLayer {
|
||||
const layer = new EditableTileMapLayer(
|
||||
tileMap,
|
||||
editableTileMapLayerAsJsObject.id
|
||||
);
|
||||
layer.setAlpha(editableTileMapLayerAsJsObject.alpha);
|
||||
editableTileMapLayerAsJsObject.tiles.forEach((row: number[], y: number) =>
|
||||
row.forEach((tileGID, x) => {
|
||||
const tileId = FlippingHelper.getTileId(tileGID);
|
||||
if (isTileIdValid(tileId)) {
|
||||
layer.setTileGID(x, y, tileGID);
|
||||
}
|
||||
})
|
||||
);
|
||||
return layer;
|
||||
}
|
||||
|
||||
toJSObject(): Object {
|
||||
return {
|
||||
id: this.id,
|
||||
alpha: this._alpha,
|
||||
tiles: this._tiles.map((row, y) =>
|
||||
// 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) => {
|
||||
const tileGID = this.getTileGID(x, y);
|
||||
// -1 corresponds to null value
|
||||
if (tileGID === undefined) return -1;
|
||||
return tileGID;
|
||||
})
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The opacity (between 0-1) of the layer
|
||||
*/
|
||||
@@ -352,12 +683,84 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
this._alpha = alpha;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._tiles.every((row) => row.every((cell) => cell === 0));
|
||||
}
|
||||
|
||||
reduceDimensions(
|
||||
columnsToPop: number,
|
||||
columnsToShift: number,
|
||||
rowsToPop: number,
|
||||
rowsToShift: number
|
||||
) {
|
||||
if (rowsToPop > 0 || rowsToShift > 0) {
|
||||
this._tiles = this._tiles.slice(
|
||||
rowsToShift,
|
||||
rowsToPop ? -rowsToPop : undefined
|
||||
);
|
||||
}
|
||||
if (columnsToPop > 0 || columnsToShift > 0) {
|
||||
this._tiles.forEach((row, rowIndex) => {
|
||||
this._tiles[rowIndex] = this._tiles[rowIndex].slice(
|
||||
columnsToShift,
|
||||
columnsToPop ? -columnsToPop : undefined
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
increaseDimensions(
|
||||
columnsToAppend: number,
|
||||
columnsToUnshift: number,
|
||||
rowsToAppend: number,
|
||||
rowsToUnshift: number
|
||||
) {
|
||||
const initialRowCount = this._tiles.length;
|
||||
const initialColumnCount = this._tiles[0].length;
|
||||
if (columnsToAppend > 0 || columnsToUnshift > 0) {
|
||||
this._tiles.forEach((row, rowIndex) => {
|
||||
const newRow = new Int32Array(
|
||||
initialColumnCount + columnsToAppend + columnsToUnshift
|
||||
).fill(0);
|
||||
newRow.set(row, columnsToUnshift);
|
||||
this._tiles[rowIndex] = newRow;
|
||||
});
|
||||
}
|
||||
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)
|
||||
.map(() =>
|
||||
new Int32Array(
|
||||
initialColumnCount + columnsToAppend + columnsToUnshift
|
||||
).fill(0)
|
||||
)
|
||||
);
|
||||
|
||||
this._tiles.length = initialRowCount + rowsToAppend + rowsToUnshift;
|
||||
|
||||
for (
|
||||
let rowIndex = initialRowCount + rowsToUnshift;
|
||||
rowIndex < this._tiles.length;
|
||||
rowIndex++
|
||||
) {
|
||||
this._tiles[rowIndex] = new Int32Array(
|
||||
initialColumnCount + columnsToAppend + columnsToUnshift
|
||||
).fill(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileId The tile.
|
||||
*/
|
||||
setTile(x: integer, y: integer, tileId: integer): void {
|
||||
setTile(x: integer, y: integer, tileId: integer) {
|
||||
const definition = this.tileMap.getTileDefinition(tileId);
|
||||
if (!definition) {
|
||||
console.error(`Invalid tile definition index: ${tileId}`);
|
||||
@@ -369,10 +772,83 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
// +1 because 0 mean null
|
||||
// +1 because 0 means null
|
||||
tilesRow[x] = tileId + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
* @param tileGID The tile GID.
|
||||
*/
|
||||
setTileGID(x: integer, y: integer, tileGID: integer): void {
|
||||
const tilesRow = this._tiles[y];
|
||||
if (!tilesRow || x >= tilesRow.length) {
|
||||
// Coordinates are out of bounds, don't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
// +1 because 0 means null
|
||||
tilesRow[x] = tileGID + 1;
|
||||
}
|
||||
|
||||
getTrimmingData(): {
|
||||
rowsToShift: number;
|
||||
columnsToShift: number;
|
||||
rowsToPop: number;
|
||||
columnsToPop: number;
|
||||
} {
|
||||
let rowsToShift = 0,
|
||||
rowsToPop = 0;
|
||||
const initialDimensionX = this.getDimensionX();
|
||||
const initialDimensionY = this.getDimensionY();
|
||||
const columnsToShiftByRow = new Array(this._tiles.length).fill(
|
||||
this._tiles[0].length
|
||||
);
|
||||
const columnsToPopByRow = new Array(this._tiles.length).fill(
|
||||
this._tiles[0].length
|
||||
);
|
||||
let isFirstNonEmptyRowFound = false;
|
||||
for (let y = 0; y < this._tiles.length; y++) {
|
||||
const row = this._tiles[y];
|
||||
let isFirstNonEmptyColumnFound = false;
|
||||
for (let x = 0; x < row.length; x++) {
|
||||
const cell = row[x];
|
||||
if (cell !== 0) {
|
||||
columnsToPopByRow[y] = row.length - 1 - x;
|
||||
if (!isFirstNonEmptyColumnFound) {
|
||||
columnsToShiftByRow[y] = x;
|
||||
isFirstNonEmptyColumnFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const isRowEmpty = !isFirstNonEmptyColumnFound;
|
||||
if (!isRowEmpty) {
|
||||
rowsToPop = this._tiles.length - 1 - y;
|
||||
if (!isFirstNonEmptyRowFound) {
|
||||
rowsToShift = y;
|
||||
isFirstNonEmptyRowFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isFirstNonEmptyRowFound) {
|
||||
return {
|
||||
columnsToShift: 0,
|
||||
rowsToShift: 0,
|
||||
columnsToPop: initialDimensionX - 1,
|
||||
rowsToPop: initialDimensionY - 1,
|
||||
};
|
||||
}
|
||||
const columnsToShift = Math.min(...columnsToShiftByRow);
|
||||
const columnsToPop = Math.min(...columnsToPopByRow);
|
||||
return {
|
||||
rowsToShift,
|
||||
columnsToShift,
|
||||
rowsToPop,
|
||||
columnsToPop,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x The layer column.
|
||||
* @param y The layer row.
|
||||
@@ -384,7 +860,7 @@ export class EditableTileMapLayer extends AbstractEditableLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
// 0 mean null
|
||||
// 0 means null
|
||||
tilesRow[x] = 0;
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { TileTextureCache } from "./TileTextureCache";
|
||||
import { PixiTileMapHelper } from "./TileMapPixiHelper";
|
||||
import { TileMapLoader } from "../load/TileMapLoader";
|
||||
import { TileMapFileContent } from "../load/TileMapFileContent";
|
||||
import { EditableTileMapAsJsObject } from "../model/CommonTypes";
|
||||
|
||||
/**
|
||||
* A holder to share tile maps across the 2 extension objects.
|
||||
@@ -117,6 +118,30 @@ export class TileMapManager {
|
||||
);
|
||||
}
|
||||
|
||||
getOrLoadSimpleTileMap(
|
||||
tileMapAsJsObject: EditableTileMapAsJsObject,
|
||||
objectName: string,
|
||||
tileSize: number,
|
||||
tileSetColumnCount: number,
|
||||
tileSetRowCount: number,
|
||||
// Logic using callback has been set up to mimic what has been done for other
|
||||
// loading methods. But it could be refactored to directly return the tile map.
|
||||
callback: (tileMap: EditableTileMap) => void
|
||||
): void {
|
||||
// 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,
|
||||
},
|
||||
);
|
||||
callback(editableTileMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loadTileMap The method that loads the Tiled JSON file in memory.
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
@@ -178,6 +203,42 @@ export class TileMapManager {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param getTexture The method that loads the atlas image file in memory.
|
||||
* @param atlasImageResourceName The resource name of the atlas image.
|
||||
* @param tileSize
|
||||
* @param columnCount
|
||||
* @param rowCount
|
||||
* @param callback A function called when the tiles textures are split.
|
||||
*/
|
||||
getOrLoadSimpleTileMapTextureCache(
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
atlasImageResourceName: string,
|
||||
tileSize: number,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
callback: (textureCache: TileTextureCache | null) => void
|
||||
): void {
|
||||
const key = `${atlasImageResourceName}|${tileSize}|${columnCount}|${rowCount}`;
|
||||
|
||||
this._textureCacheCaches.getOrLoad(
|
||||
key,
|
||||
(textureCacheLoadingCallback) => {
|
||||
const atlasTexture = atlasImageResourceName
|
||||
? getTexture(atlasImageResourceName)
|
||||
: null;
|
||||
const textureCache = PixiTileMapHelper.parseSimpleTileMapAtlas(
|
||||
atlasTexture,
|
||||
columnCount,
|
||||
rowCount,
|
||||
tileSize
|
||||
);
|
||||
textureCacheLoadingCallback(textureCache);
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
clearCaches(): void {
|
||||
this._tileMapCache = new ResourceCache<EditableTileMap>();
|
||||
this._textureCacheCaches = new ResourceCache<TileTextureCache>();
|
||||
|
@@ -14,7 +14,7 @@ export namespace PixiTileMapHelper {
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
*
|
||||
* @param tiledMap A tile map exported from Tiled.
|
||||
* @param tiledMap A tile map exported from Tiled or LDtk.
|
||||
* @param levelIndex The level of the tile map to load from.
|
||||
* @param atlasTexture The texture containing the whole tile set.
|
||||
* @param getTexture A getter to load a texture. Used if atlasTexture is not specified.
|
||||
@@ -50,6 +50,43 @@ export namespace PixiTileMapHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
*
|
||||
* @param atlasTexture The texture containing the whole tile set.
|
||||
* @param columnCount The number of columns.
|
||||
* @param rowCount The number of rows.
|
||||
* @param tileSize The squared tile size.
|
||||
* @returns A textures cache.
|
||||
*/
|
||||
export function parseSimpleTileMapAtlas(
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource>,
|
||||
columnCount: number,
|
||||
rowCount: number,
|
||||
tileSize: number
|
||||
): TileTextureCache {
|
||||
const textureCache = new TileTextureCache();
|
||||
for (let x = 0; x < columnCount; x++) {
|
||||
for (let y = 0; y < rowCount; y++) {
|
||||
const rect = new PIXI.Rectangle(
|
||||
x * tileSize,
|
||||
y * tileSize,
|
||||
tileSize,
|
||||
tileSize
|
||||
);
|
||||
|
||||
const texture = new PIXI.Texture(atlasTexture, rect);
|
||||
|
||||
textureCache.setTexture(
|
||||
// Id of the tile
|
||||
columnCount * y + x,
|
||||
texture
|
||||
);
|
||||
}
|
||||
}
|
||||
return textureCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-renders the tile map whenever its rendering settings have been changed
|
||||
*
|
||||
|
7
newIDE/app/package-lock.json
generated
7
newIDE/app/package-lock.json
generated
@@ -86,7 +86,7 @@
|
||||
"flow-bin": "0.131.0",
|
||||
"flow-coverage-report": "^0.4.0",
|
||||
"folder-hash": "^3.0.0",
|
||||
"iso-639-1": "^2.0.3",
|
||||
"iso-639-1": "^3.1.2",
|
||||
"minimist": "1.2.5",
|
||||
"patch-package": "^6.4.7",
|
||||
"prettier": "1.15.3",
|
||||
@@ -19848,9 +19848,10 @@
|
||||
"integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="
|
||||
},
|
||||
"node_modules/iso-639-1": {
|
||||
"version": "2.1.4",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.2.tgz",
|
||||
"integrity": "sha512-Le7BRl3Jt9URvaiEHJCDEdvPZCfhiQoXnFgLAWNRhzFMwRFdWO7/5tLRQbiPzE394I9xd7KdRCM7S6qdOhwG5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@
|
||||
"flow-bin": "0.131.0",
|
||||
"flow-coverage-report": "^0.4.0",
|
||||
"folder-hash": "^3.0.0",
|
||||
"iso-639-1": "^2.0.3",
|
||||
"iso-639-1": "^3.1.2",
|
||||
"minimist": "1.2.5",
|
||||
"patch-package": "^6.4.7",
|
||||
"prettier": "1.15.3",
|
||||
@@ -152,6 +152,13 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "MemberExpression[object.property.name='constructor'][property.name='name']",
|
||||
"message": "'constructor.name' is not reliable (can become 'E', 'P' and etc.) in production build."
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
|
@@ -34,7 +34,7 @@ const getLocaleSourceCatalogFiles = localeName => {
|
||||
if (localeName === 'pseudo_LOCALE') return ['ide-messages.pot'];
|
||||
|
||||
return ['ide-messages.po', 'gdcore-gdcpp-gdjs-extensions-messages.po'];
|
||||
}
|
||||
};
|
||||
|
||||
const getLocalePath = localeName => {
|
||||
return path.join(localesBasePath, localeName);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user