mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e9b4de2ca9 | ||
![]() |
75a4114ce8 | ||
![]() |
20abb9b45a | ||
![]() |
56436fd44a | ||
![]() |
0dd5fc55c9 | ||
![]() |
93db4cb508 | ||
![]() |
f7888abf45 | ||
![]() |
c8144da704 | ||
![]() |
832e8cd593 | ||
![]() |
ef66a9f1a4 | ||
![]() |
5efbaa8c58 | ||
![]() |
ecbf38ccda | ||
![]() |
b3fcfc3f55 | ||
![]() |
a515836add | ||
![]() |
a7c81b47b2 | ||
![]() |
0f22e462ad | ||
![]() |
e6e4d9048f | ||
![]() |
12f5f95d0c | ||
![]() |
c52168a967 | ||
![]() |
1e33a13cc5 | ||
![]() |
505debd60c | ||
![]() |
e3b7109154 | ||
![]() |
9e25899d3e | ||
![]() |
87cb8f0d47 | ||
![]() |
481c6da992 | ||
![]() |
7cbebbb82f | ||
![]() |
fcf668788b | ||
![]() |
0cc844a77f | ||
![]() |
a234d9bd35 | ||
![]() |
465a6ce2ab | ||
![]() |
7e2e19eb33 | ||
![]() |
95101763f7 | ||
![]() |
d4bd5fc671 | ||
![]() |
c7fcf48ba5 | ||
![]() |
8926d4406f | ||
![]() |
9ed2173038 | ||
![]() |
2fc3bc337f | ||
![]() |
0b7cac79ef | ||
![]() |
8721c0099e | ||
![]() |
4453eee3b9 | ||
![]() |
0215ab7dbb | ||
![]() |
87f6d5b99f | ||
![]() |
a440b16f84 | ||
![]() |
f3822ba0df | ||
![]() |
6c5813affd | ||
![]() |
be4fe62bb6 | ||
![]() |
0a29999894 |
2
.vscode/GDevelopExtensions.code-snippets
vendored
2
.vscode/GDevelopExtensions.code-snippets
vendored
@@ -107,7 +107,7 @@
|
||||
"description": "Define a parameter in a GDevelop extension definition.",
|
||||
"prefix": "gdparam",
|
||||
"body": [
|
||||
".addParameter('${1|string,expression,object,behavior,yesorno,stringWithSelector,scenevar,globalvar,objectvar,objectList,objectListWithoutPicking,color,key,sceneName,file,layer,relationalOperator,operator,trueorfalse,musicfile,soundfile,police,mouse,passwordjoyaxis,camera,objectPtr,forceMultiplier|}', '${2:Parameter description}', '${3:Optional parameter data}', /*parameterIsOptional=*/${4|false,true|})"
|
||||
".addParameter('${1|string,expression,object,behavior,yesorno,stringWithSelector,scenevar,globalvar,objectvar,objectList,objectListWithoutPicking,color,key,sceneName,file,layer,relationalOperator,operator,trueorfalse,musicfile,soundfile,mouse,passwordjoyaxis,camera,objectPtr,forceMultiplier|}', '${2:Parameter description}', '${3:Optional parameter data}', /*parameterIsOptional=*/${4|false,true|})"
|
||||
]
|
||||
},
|
||||
"Add code only parameter": {
|
||||
|
@@ -77,11 +77,11 @@ gd::String EventsCodeGenerator::GenerateRelationalOperatorCall(
|
||||
|
||||
/**
|
||||
* @brief Generate a relational operation
|
||||
*
|
||||
*
|
||||
* @param relationalOperator the operator
|
||||
* @param lhs the left hand operand
|
||||
* @param rhs the right hand operand
|
||||
* @return gd::String
|
||||
* @return gd::String
|
||||
*/
|
||||
gd::String EventsCodeGenerator::GenerateRelationalOperation(
|
||||
const gd::String& relationalOperator,
|
||||
@@ -828,7 +828,7 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
|
||||
metadata.GetType() == "spineResource" ||
|
||||
// Deprecated, old parameter names:
|
||||
metadata.GetType() == "password" || metadata.GetType() == "musicfile" ||
|
||||
metadata.GetType() == "soundfile" || metadata.GetType() == "police") {
|
||||
metadata.GetType() == "soundfile") {
|
||||
argOutput = "\"" + ConvertToString(parameter.GetPlainString()) + "\"";
|
||||
} else if (metadata.GetType() == "mouse") {
|
||||
argOutput = "\"" + ConvertToString(parameter.GetPlainString()) + "\"";
|
||||
@@ -1007,7 +1007,7 @@ gd::String EventsCodeGenerator::GenerateEventsListCode(
|
||||
|
||||
output += "\n" + scopeBegin + "\n" + declarationsCode + "\n" +
|
||||
eventCoreCode + "\n" + scopeEnd + "\n";
|
||||
|
||||
|
||||
if (event.HasVariables()) {
|
||||
GetProjectScopedContainers().GetVariablesContainersList().Pop();
|
||||
}
|
||||
|
@@ -528,7 +528,7 @@ protected:
|
||||
parameter -> string
|
||||
* - operator : Used to update a value using a setter and a getter -> string
|
||||
* - key, mouse, objectvar, scenevar, globalvar, password, musicfile,
|
||||
soundfile, police -> string
|
||||
soundfile -> string
|
||||
* - trueorfalse, yesorno -> boolean ( See GenerateTrue/GenerateFalse ).
|
||||
*
|
||||
* <br><br>
|
||||
@@ -849,7 +849,7 @@ protected:
|
||||
instructionUniqueIds; ///< The unique ids generated for instructions.
|
||||
size_t eventsListNextUniqueId; ///< The next identifier to use for an events
|
||||
///< list function name.
|
||||
|
||||
|
||||
gd::DiagnosticReport* diagnosticReport;
|
||||
};
|
||||
|
||||
|
@@ -225,9 +225,7 @@ bool ResourceWorkerInEventsWorker::DoVisitInstruction(gd::Instruction& instructi
|
||||
size_t parameterIndex,
|
||||
const gd::String& lastObjectName) {
|
||||
const String& parameterValue = parameterExpression.GetPlainString();
|
||||
if (parameterMetadata.GetType() ==
|
||||
"police" || // Should be renamed fontResource
|
||||
parameterMetadata.GetType() == "fontResource") {
|
||||
if (parameterMetadata.GetType() == "fontResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeFont(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
|
@@ -39,16 +39,16 @@ void CustomConfigurationHelper::InitializeContent(
|
||||
std::map<gd::String, gd::PropertyDescriptor> CustomConfigurationHelper::GetProperties(
|
||||
const gd::PropertiesContainer &properties,
|
||||
const gd::SerializerElement &configurationContent) {
|
||||
auto behaviorProperties = std::map<gd::String, gd::PropertyDescriptor>();
|
||||
auto objectProperties = std::map<gd::String, gd::PropertyDescriptor>();
|
||||
|
||||
for (auto &property : properties.GetInternalVector()) {
|
||||
const auto &propertyName = property->GetName();
|
||||
const auto &propertyType = property->GetType();
|
||||
|
||||
// Copy the property
|
||||
behaviorProperties[propertyName] = *property;
|
||||
objectProperties[propertyName] = *property;
|
||||
|
||||
auto &newProperty = behaviorProperties[propertyName];
|
||||
auto &newProperty = objectProperties[propertyName];
|
||||
|
||||
if (configurationContent.HasChild(propertyName)) {
|
||||
if (propertyType == "String" || propertyType == "Choice" ||
|
||||
@@ -71,7 +71,7 @@ std::map<gd::String, gd::PropertyDescriptor> CustomConfigurationHelper::GetPrope
|
||||
}
|
||||
}
|
||||
|
||||
return behaviorProperties;
|
||||
return objectProperties;
|
||||
}
|
||||
|
||||
bool CustomConfigurationHelper::UpdateProperty(
|
||||
|
@@ -21,6 +21,9 @@ void CustomObjectConfiguration::Init(const gd::CustomObjectConfiguration& object
|
||||
project = objectConfiguration.project;
|
||||
objectContent = objectConfiguration.objectContent;
|
||||
animations = objectConfiguration.animations;
|
||||
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration =
|
||||
objectConfiguration
|
||||
.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration;
|
||||
|
||||
// There is no default copy for a map of unique_ptr like childObjectConfigurations.
|
||||
childObjectConfigurations.clear();
|
||||
@@ -42,6 +45,26 @@ const gd::EventsBasedObject* CustomObjectConfiguration::GetEventsBasedObject() c
|
||||
return &project->GetEventsBasedObject(GetType());
|
||||
}
|
||||
|
||||
bool CustomObjectConfiguration::
|
||||
IsForcedToOverrideEventsBasedObjectChildrenConfiguration() const {
|
||||
const auto *eventsBasedObject = GetEventsBasedObject();
|
||||
if (!eventsBasedObject) {
|
||||
// True is safer because nothing will be lost when serializing.
|
||||
return true;
|
||||
}
|
||||
return eventsBasedObject->GetInitialInstances().GetInstancesCount() == 0;
|
||||
}
|
||||
|
||||
bool CustomObjectConfiguration::
|
||||
IsOverridingEventsBasedObjectChildrenConfiguration() const {
|
||||
return isMarkedAsOverridingEventsBasedObjectChildrenConfiguration ||
|
||||
IsForcedToOverrideEventsBasedObjectChildrenConfiguration();
|
||||
}
|
||||
|
||||
void CustomObjectConfiguration::ClearChildrenConfiguration() {
|
||||
childObjectConfigurations.clear();
|
||||
}
|
||||
|
||||
gd::ObjectConfiguration &CustomObjectConfiguration::GetChildObjectConfiguration(const gd::String &objectName) {
|
||||
const auto *eventsBasedObject = GetEventsBasedObject();
|
||||
if (!eventsBasedObject) {
|
||||
@@ -55,6 +78,18 @@ gd::ObjectConfiguration &CustomObjectConfiguration::GetChildObjectConfiguration(
|
||||
}
|
||||
|
||||
auto &childObject = eventsBasedObject->GetObjects().GetObject(objectName);
|
||||
|
||||
if (!IsOverridingEventsBasedObjectChildrenConfiguration()) {
|
||||
// It should be fine because the editor doesn't allow to edit values when
|
||||
// the default values from the events-based object is used.
|
||||
//
|
||||
// Resource refactor operations may modify it but they will do the same
|
||||
// thing on the custom object as on the event-based object children so it
|
||||
// shouldn't have any side effect.
|
||||
return const_cast<gd::ObjectConfiguration &>(
|
||||
childObject.GetConfiguration());
|
||||
}
|
||||
|
||||
auto configurationPosition = childObjectConfigurations.find(objectName);
|
||||
if (configurationPosition == childObjectConfigurations.end()) {
|
||||
childObjectConfigurations.insert(std::make_pair(
|
||||
@@ -67,7 +102,7 @@ gd::ObjectConfiguration &CustomObjectConfiguration::GetChildObjectConfiguration(
|
||||
auto &configuration = pair.second;
|
||||
return *configuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor> CustomObjectConfiguration::GetProperties() const {
|
||||
auto objectProperties = std::map<gd::String, gd::PropertyDescriptor>();
|
||||
@@ -128,26 +163,14 @@ void CustomObjectConfiguration::DoSerializeTo(SerializerElement& element) const
|
||||
animations.SerializeTo(animatableElement);
|
||||
}
|
||||
|
||||
auto &childrenContentElement = element.AddChild("childrenContent");
|
||||
for (auto &pair : childObjectConfigurations) {
|
||||
auto &childName = pair.first;
|
||||
auto &childConfiguration = pair.second;
|
||||
auto &childElement = childrenContentElement.AddChild(childName);
|
||||
childConfiguration->SerializeTo(childElement);
|
||||
}
|
||||
|
||||
const auto *eventsBasedObject = GetEventsBasedObject();
|
||||
if (eventsBasedObject) {
|
||||
eventsBasedObject->GetInitialInstances().SerializeTo(
|
||||
element.AddChild("instances"));
|
||||
eventsBasedObject->GetLayers().SerializeLayersTo(
|
||||
element.AddChild("layers"));
|
||||
element.SetIntAttribute("areaMinX", eventsBasedObject->GetAreaMinX());
|
||||
element.SetIntAttribute("areaMinY", eventsBasedObject->GetAreaMinY());
|
||||
element.SetIntAttribute("areaMinZ", eventsBasedObject->GetAreaMinZ());
|
||||
element.SetIntAttribute("areaMaxX", eventsBasedObject->GetAreaMaxX());
|
||||
element.SetIntAttribute("areaMaxY", eventsBasedObject->GetAreaMaxY());
|
||||
element.SetIntAttribute("areaMaxZ", eventsBasedObject->GetAreaMaxZ());
|
||||
if (IsOverridingEventsBasedObjectChildrenConfiguration()) {
|
||||
auto &childrenContentElement = element.AddChild("childrenContent");
|
||||
for (auto &pair : childObjectConfigurations) {
|
||||
auto &childName = pair.first;
|
||||
auto &childConfiguration = pair.second;
|
||||
auto &childElement = childrenContentElement.AddChild(childName);
|
||||
childConfiguration->SerializeTo(childElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
void CustomObjectConfiguration::DoUnserializeFrom(Project& project,
|
||||
@@ -159,12 +182,16 @@ void CustomObjectConfiguration::DoUnserializeFrom(Project& project,
|
||||
animations.UnserializeFrom(animatableElement);
|
||||
}
|
||||
|
||||
auto &childrenContentElement = element.GetChild("childrenContent");
|
||||
for (auto &pair : childrenContentElement.GetAllChildren()) {
|
||||
auto &childName = pair.first;
|
||||
auto &childElement = pair.second;
|
||||
auto &childConfiguration = GetChildObjectConfiguration(childName);
|
||||
childConfiguration.UnserializeFrom(project, *childElement);
|
||||
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration =
|
||||
element.HasChild("childrenContent");
|
||||
if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) {
|
||||
auto &childrenContentElement = element.GetChild("childrenContent");
|
||||
for (auto &pair : childrenContentElement.GetAllChildren()) {
|
||||
auto &childName = pair.first;
|
||||
auto &childElement = pair.second;
|
||||
auto &childConfiguration = GetChildObjectConfiguration(childName);
|
||||
childConfiguration.UnserializeFrom(project, *childElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -30,7 +30,7 @@ namespace gd {
|
||||
class CustomObjectConfiguration : public gd::ObjectConfiguration {
|
||||
public:
|
||||
CustomObjectConfiguration(const Project& project_, const String& type_)
|
||||
: project(&project_) {
|
||||
: project(&project_), isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(false) {
|
||||
SetType(type_);
|
||||
}
|
||||
std::unique_ptr<gd::ObjectConfiguration> Clone() const override;
|
||||
@@ -65,7 +65,22 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
|
||||
|
||||
void ExposeResources(gd::ArbitraryResourceWorker& worker) override;
|
||||
|
||||
gd::ObjectConfiguration &GetChildObjectConfiguration(const gd::String& objectName);
|
||||
bool IsForcedToOverrideEventsBasedObjectChildrenConfiguration() const;
|
||||
|
||||
bool IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration() const {
|
||||
return isMarkedAsOverridingEventsBasedObjectChildrenConfiguration;
|
||||
}
|
||||
|
||||
void SetMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
|
||||
bool isOverridingEventsBasedObjectChildrenConfiguration_) {
|
||||
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration =
|
||||
isOverridingEventsBasedObjectChildrenConfiguration_;
|
||||
}
|
||||
|
||||
void ClearChildrenConfiguration();
|
||||
|
||||
gd::ObjectConfiguration &
|
||||
GetChildObjectConfiguration(const gd::String &objectName);
|
||||
|
||||
std::size_t GetAnimationsCount() const override;
|
||||
|
||||
@@ -90,10 +105,14 @@ protected:
|
||||
private:
|
||||
const gd::EventsBasedObject* GetEventsBasedObject() const;
|
||||
|
||||
bool IsOverridingEventsBasedObjectChildrenConfiguration() const;
|
||||
|
||||
const Project* project; ///< The project is used to get the
|
||||
///< EventBasedObject from the fullType.
|
||||
gd::SerializerElement objectContent;
|
||||
std::map<gd::String, std::unique_ptr<gd::ObjectConfiguration>> childObjectConfigurations;
|
||||
|
||||
bool isMarkedAsOverridingEventsBasedObjectChildrenConfiguration;
|
||||
mutable std::map<gd::String, std::unique_ptr<gd::ObjectConfiguration>> childObjectConfigurations;
|
||||
|
||||
static gd::ObjectConfiguration badObjectConfiguration;
|
||||
|
||||
|
@@ -16,6 +16,7 @@ EventsBasedObject::EventsBasedObject()
|
||||
isRenderedIn3D(false),
|
||||
isAnimatable(false),
|
||||
isTextContainer(false),
|
||||
isInnerAreaFollowingParentSize(false),
|
||||
areaMinX(0),
|
||||
areaMinY(0),
|
||||
areaMinZ(0),
|
||||
@@ -37,6 +38,9 @@ void EventsBasedObject::SerializeTo(SerializerElement& element) const {
|
||||
if (isTextContainer) {
|
||||
element.SetBoolAttribute("isTextContainer", true);
|
||||
}
|
||||
if (isInnerAreaFollowingParentSize) {
|
||||
element.SetBoolAttribute("isInnerAreaFollowingParentSize", true);
|
||||
}
|
||||
element.SetIntAttribute("areaMinX", areaMinX);
|
||||
element.SetIntAttribute("areaMinY", areaMinY);
|
||||
element.SetIntAttribute("areaMinZ", areaMinZ);
|
||||
@@ -59,6 +63,8 @@ void EventsBasedObject::UnserializeFrom(gd::Project& project,
|
||||
isRenderedIn3D = element.GetBoolAttribute("is3D", false);
|
||||
isAnimatable = element.GetBoolAttribute("isAnimatable", false);
|
||||
isTextContainer = element.GetBoolAttribute("isTextContainer", false);
|
||||
isInnerAreaFollowingParentSize =
|
||||
element.GetBoolAttribute("isInnerAreaFollowingParentSize", false);
|
||||
areaMinX = element.GetIntAttribute("areaMinX", 0);
|
||||
areaMinY = element.GetIntAttribute("areaMinY", 0);
|
||||
areaMinZ = element.GetIntAttribute("areaMinZ", 0);
|
||||
|
@@ -101,11 +101,35 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
|
||||
/**
|
||||
* \brief Declare a TextContainer capability.
|
||||
*/
|
||||
EventsBasedObject& MarkAsTextContainer(bool isTextContainer_) {
|
||||
EventsBasedObject &MarkAsTextContainer(bool isTextContainer_) {
|
||||
isTextContainer = isTextContainer_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Declare that the parent scale will always be 1 and children will
|
||||
* adapt there size. This is removing the ScalableCapability.
|
||||
*/
|
||||
EventsBasedObject &
|
||||
MarkAsInnerAreaExpandingWithParent(bool isInnerAreaExpandingWithParent_) {
|
||||
isInnerAreaFollowingParentSize = isInnerAreaExpandingWithParent_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Return true if objects handle size changes on their own and
|
||||
* don't have the ScalableCapability.
|
||||
*
|
||||
* When the parent dimensions change:
|
||||
* - if `false`, the object is stretch proportionally while children local
|
||||
* positions stay the same.
|
||||
* - if `true`, the children local positions need to be adapted by events
|
||||
* to follow their parent size.
|
||||
*/
|
||||
bool IsInnerAreaFollowingParentSize() const {
|
||||
return isInnerAreaFollowingParentSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Return true if the object needs a TextContainer capability.
|
||||
*/
|
||||
@@ -279,6 +303,7 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
|
||||
bool isRenderedIn3D;
|
||||
bool isAnimatable;
|
||||
bool isTextContainer;
|
||||
bool isInnerAreaFollowingParentSize;
|
||||
gd::InitialInstancesContainer initialInstances;
|
||||
gd::LayersContainer layers;
|
||||
gd::ObjectsContainer objectsContainer;
|
||||
|
@@ -589,4 +589,13 @@ std::vector<gd::String> ObjectsContainersList::GetAnimationNamesOfObject(
|
||||
return animationNames;
|
||||
}
|
||||
|
||||
const gd::ObjectsContainer &
|
||||
ObjectsContainersList::GetObjectsContainer(std::size_t index) const {
|
||||
return *objectsContainers[index];
|
||||
}
|
||||
|
||||
std::size_t ObjectsContainersList::GetObjectsContainersCount() const {
|
||||
return objectsContainers.size();
|
||||
}
|
||||
|
||||
} // namespace gd
|
@@ -173,6 +173,16 @@ class GD_CORE_API ObjectsContainersList {
|
||||
std::function<void(const gd::String& variableName,
|
||||
const gd::Variable& variable)> fn) const;
|
||||
|
||||
/**
|
||||
* \brief Return a the objects container at position \a index.
|
||||
*/
|
||||
const gd::ObjectsContainer &GetObjectsContainer(std::size_t index) const;
|
||||
|
||||
/**
|
||||
* \brief Return the number of objects containers.
|
||||
*/
|
||||
std::size_t GetObjectsContainersCount() const;
|
||||
|
||||
/** Do not use - should be private but accessible to let Emscripten create a
|
||||
* temporary. */
|
||||
ObjectsContainersList(){};
|
||||
|
@@ -304,8 +304,15 @@ namespace gdjs {
|
||||
*/
|
||||
setDepth(depth: float): void {
|
||||
const unscaledDepth = this.getUnscaledDepth();
|
||||
if (unscaledDepth !== 0) {
|
||||
this.setScaleZ(depth / unscaledDepth);
|
||||
if (unscaledDepth === 0) {
|
||||
return;
|
||||
}
|
||||
const scaleZ = depth / unscaledDepth;
|
||||
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
|
||||
this._innerArea.min[2] *= scaleZ;
|
||||
this._innerArea.max[2] *= scaleZ;
|
||||
} else {
|
||||
this.setScaleZ(scaleZ);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +332,10 @@ namespace gdjs {
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleZ(newScale: number): void {
|
||||
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
|
||||
// The scale is always 1;
|
||||
return;
|
||||
}
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
|
@@ -333,6 +333,7 @@ namespace gdjs {
|
||||
adUnitId,
|
||||
position: atTop ? 'top' : 'bottom',
|
||||
size: bannerRequestedAdSizeType,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
banner.on('load', () => {
|
||||
|
@@ -25,7 +25,6 @@ void AnchorBehavior::InitializeContent(gd::SerializerElement& content) {
|
||||
content.SetAttribute("useLegacyBottomAndRightAnchors", false);
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
namespace {
|
||||
gd::String GetAnchorAsString(AnchorBehavior::HorizontalAnchor anchor) {
|
||||
if (anchor == AnchorBehavior::ANCHOR_HORIZONTAL_WINDOW_LEFT)
|
||||
@@ -34,6 +33,8 @@ gd::String GetAnchorAsString(AnchorBehavior::HorizontalAnchor anchor) {
|
||||
return _("Window right");
|
||||
else if (anchor == AnchorBehavior::ANCHOR_HORIZONTAL_PROPORTIONAL)
|
||||
return _("Proportional");
|
||||
else if (anchor == AnchorBehavior::ANCHOR_HORIZONTAL_WINDOW_CENTER)
|
||||
return _("Window center");
|
||||
else
|
||||
return _("No anchor");
|
||||
}
|
||||
@@ -45,6 +46,8 @@ gd::String GetAnchorAsString(AnchorBehavior::VerticalAnchor anchor) {
|
||||
return _("Window bottom");
|
||||
else if (anchor == AnchorBehavior::ANCHOR_VERTICAL_PROPORTIONAL)
|
||||
return _("Proportional");
|
||||
else if (anchor == AnchorBehavior::ANCHOR_VERTICAL_WINDOW_CENTER)
|
||||
return _("Window center");
|
||||
else
|
||||
return _("No anchor");
|
||||
}
|
||||
@@ -69,6 +72,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
|
||||
.SetType("Choice")
|
||||
.AddExtraInfo(_("No anchor"))
|
||||
.AddExtraInfo(_("Window left"))
|
||||
.AddExtraInfo(_("Window center"))
|
||||
.AddExtraInfo(_("Window right"))
|
||||
.AddExtraInfo(_("Proportional"))
|
||||
.SetDescription(_("Anchor the left edge of the object on X axis."));
|
||||
@@ -79,6 +83,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
|
||||
.SetType("Choice")
|
||||
.AddExtraInfo(_("No anchor"))
|
||||
.AddExtraInfo(_("Window left"))
|
||||
.AddExtraInfo(_("Window center"))
|
||||
.AddExtraInfo(_("Window right"))
|
||||
.AddExtraInfo(_("Proportional"))
|
||||
.SetDescription(_("Anchor the right edge of the object on X axis."));
|
||||
@@ -89,6 +94,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
|
||||
.SetType("Choice")
|
||||
.AddExtraInfo(_("No anchor"))
|
||||
.AddExtraInfo(_("Window top"))
|
||||
.AddExtraInfo(_("Window center"))
|
||||
.AddExtraInfo(_("Window bottom"))
|
||||
.AddExtraInfo(_("Proportional"))
|
||||
.SetDescription(_("Anchor the top edge of the object on Y axis."));
|
||||
@@ -99,6 +105,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
|
||||
.SetType("Choice")
|
||||
.AddExtraInfo(_("No anchor"))
|
||||
.AddExtraInfo(_("Window top"))
|
||||
.AddExtraInfo(_("Window center"))
|
||||
.AddExtraInfo(_("Window bottom"))
|
||||
.AddExtraInfo(_("Proportional"))
|
||||
.SetDescription(_("Anchor the bottom edge of the object on Y axis."));
|
||||
@@ -127,6 +134,8 @@ AnchorBehavior::HorizontalAnchor GetHorizontalAnchorFromString(
|
||||
return AnchorBehavior::ANCHOR_HORIZONTAL_WINDOW_RIGHT;
|
||||
else if (value == _("Proportional"))
|
||||
return AnchorBehavior::ANCHOR_HORIZONTAL_PROPORTIONAL;
|
||||
else if (value == _("Window center"))
|
||||
return AnchorBehavior::ANCHOR_HORIZONTAL_WINDOW_CENTER;
|
||||
else
|
||||
return AnchorBehavior::ANCHOR_HORIZONTAL_NONE;
|
||||
}
|
||||
@@ -139,6 +148,8 @@ AnchorBehavior::VerticalAnchor GetVerticalAnchorFromString(
|
||||
return AnchorBehavior::ANCHOR_VERTICAL_WINDOW_BOTTOM;
|
||||
else if (value == _("Proportional"))
|
||||
return AnchorBehavior::ANCHOR_VERTICAL_PROPORTIONAL;
|
||||
else if (value == _("Window center"))
|
||||
return AnchorBehavior::ANCHOR_VERTICAL_WINDOW_CENTER;
|
||||
else
|
||||
return AnchorBehavior::ANCHOR_VERTICAL_NONE;
|
||||
}
|
||||
@@ -172,4 +183,3 @@ bool AnchorBehavior::UpdateProperty(gd::SerializerElement& behaviorContent,
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
@@ -3,8 +3,8 @@ GDevelop - Anchor Behavior Extension
|
||||
Copyright (c) 2016 Victor Levasseur (victorlevasseur52@gmail.com)
|
||||
This project is released under the MIT License.
|
||||
*/
|
||||
#ifndef ANCHORBEHAVIOR_H
|
||||
#define ANCHORBEHAVIOR_H
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "GDCore/Project/Behavior.h"
|
||||
#include "GDCore/Project/Object.h"
|
||||
@@ -22,14 +22,16 @@ class GD_EXTENSION_API AnchorBehavior : public gd::Behavior {
|
||||
ANCHOR_HORIZONTAL_NONE = 0,
|
||||
ANCHOR_HORIZONTAL_WINDOW_LEFT = 1,
|
||||
ANCHOR_HORIZONTAL_WINDOW_RIGHT = 2,
|
||||
ANCHOR_HORIZONTAL_PROPORTIONAL = 3
|
||||
ANCHOR_HORIZONTAL_PROPORTIONAL = 3,
|
||||
ANCHOR_HORIZONTAL_WINDOW_CENTER = 4
|
||||
};
|
||||
|
||||
enum VerticalAnchor {
|
||||
ANCHOR_VERTICAL_NONE = 0,
|
||||
ANCHOR_VERTICAL_WINDOW_TOP = 1,
|
||||
ANCHOR_VERTICAL_WINDOW_BOTTOM = 2,
|
||||
ANCHOR_VERTICAL_PROPORTIONAL = 3
|
||||
ANCHOR_VERTICAL_PROPORTIONAL = 3,
|
||||
ANCHOR_VERTICAL_WINDOW_CENTER = 4
|
||||
};
|
||||
|
||||
AnchorBehavior() {};
|
||||
@@ -47,5 +49,3 @@ class GD_EXTENSION_API AnchorBehavior : public gd::Behavior {
|
||||
virtual void InitializeContent(
|
||||
gd::SerializerElement& behaviorContent) override;
|
||||
};
|
||||
|
||||
#endif // ANCHORBEHAVIOR_H
|
||||
|
@@ -4,10 +4,25 @@ Copyright (c) 2013-2016 Florian Rival (Florian.Rival@gmail.com)
|
||||
*/
|
||||
|
||||
namespace gdjs {
|
||||
const enum HorizontalAnchor {
|
||||
None = 0,
|
||||
WindowLeft,
|
||||
WindowRight,
|
||||
Proportional,
|
||||
WindowCenter,
|
||||
}
|
||||
const enum VerticalAnchor {
|
||||
None = 0,
|
||||
WindowTop,
|
||||
WindowBottom,
|
||||
Proportional,
|
||||
WindowCenter,
|
||||
}
|
||||
|
||||
export class AnchorRuntimeBehavior extends gdjs.RuntimeBehavior {
|
||||
_relativeToOriginalWindowSize: any;
|
||||
_leftEdgeAnchor: any;
|
||||
_rightEdgeAnchor: any;
|
||||
_leftEdgeAnchor: HorizontalAnchor;
|
||||
_rightEdgeAnchor: HorizontalAnchor;
|
||||
_topEdgeAnchor: any;
|
||||
_bottomEdgeAnchor: any;
|
||||
_invalidDistances: boolean = true;
|
||||
@@ -74,14 +89,25 @@ namespace gdjs {
|
||||
gdjs.AnchorRuntimeBehavior.prototype.doStepPreEvents
|
||||
) as FloatPoint;
|
||||
// TODO EBO Make it work with event based objects or hide this behavior for them.
|
||||
const game = instanceContainer.getGame();
|
||||
let rendererWidth = game.getGameResolutionWidth();
|
||||
let rendererHeight = game.getGameResolutionHeight();
|
||||
let parentMinX = instanceContainer.getUnrotatedViewportMinX();
|
||||
let parentMinY = instanceContainer.getUnrotatedViewportMinY();
|
||||
let parentMaxX = instanceContainer.getUnrotatedViewportMaxX();
|
||||
let parentMaxY = instanceContainer.getUnrotatedViewportMaxY();
|
||||
let parentCenterX = (parentMaxX + parentMinX) / 2;
|
||||
let parentCenterY = (parentMaxY + parentMinY) / 2;
|
||||
let parentWidth = parentMaxX - parentMinX;
|
||||
let parentHeight = parentMaxY - parentMinY;
|
||||
const layer = instanceContainer.getLayer(this.owner.getLayer());
|
||||
if (this._invalidDistances) {
|
||||
if (this._relativeToOriginalWindowSize) {
|
||||
rendererWidth = game.getOriginalWidth();
|
||||
rendererHeight = game.getOriginalHeight();
|
||||
parentMinX = instanceContainer.getInitialUnrotatedViewportMinX();
|
||||
parentMinY = instanceContainer.getInitialUnrotatedViewportMinY();
|
||||
parentMaxX = instanceContainer.getInitialUnrotatedViewportMaxX();
|
||||
parentMaxY = instanceContainer.getInitialUnrotatedViewportMaxY();
|
||||
parentCenterX = (parentMaxX + parentMinX) / 2;
|
||||
parentCenterY = (parentMaxY + parentMinY) / 2;
|
||||
parentWidth = parentMaxX - parentMinX;
|
||||
parentHeight = parentMaxY - parentMinY;
|
||||
}
|
||||
|
||||
//Calculate the distances from the window's bounds.
|
||||
@@ -92,49 +118,28 @@ namespace gdjs {
|
||||
workingPoint
|
||||
);
|
||||
|
||||
//Left edge
|
||||
if (
|
||||
this._leftEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_LEFT
|
||||
) {
|
||||
this._leftEdgeDistance = topLeftPixel[0];
|
||||
} else {
|
||||
if (
|
||||
this._leftEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_RIGHT
|
||||
) {
|
||||
this._leftEdgeDistance = rendererWidth - topLeftPixel[0];
|
||||
} else {
|
||||
if (
|
||||
this._leftEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.PROPORTIONAL
|
||||
) {
|
||||
this._leftEdgeDistance = topLeftPixel[0] / rendererWidth;
|
||||
}
|
||||
}
|
||||
// Left edge
|
||||
if (this._leftEdgeAnchor === HorizontalAnchor.WindowLeft) {
|
||||
this._leftEdgeDistance = topLeftPixel[0] - parentMinX;
|
||||
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowRight) {
|
||||
this._leftEdgeDistance = topLeftPixel[0] - parentMaxX;
|
||||
} else if (this._leftEdgeAnchor === HorizontalAnchor.Proportional) {
|
||||
this._leftEdgeDistance = (topLeftPixel[0] - parentMinX) / parentWidth;
|
||||
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowCenter) {
|
||||
this._leftEdgeDistance = topLeftPixel[0] - parentCenterX;
|
||||
}
|
||||
|
||||
//Top edge
|
||||
if (
|
||||
this._topEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_TOP
|
||||
) {
|
||||
this._topEdgeDistance = topLeftPixel[1];
|
||||
} else {
|
||||
if (
|
||||
this._topEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_BOTTOM
|
||||
) {
|
||||
this._topEdgeDistance = rendererHeight - topLeftPixel[1];
|
||||
} else {
|
||||
if (
|
||||
this._topEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.PROPORTIONAL
|
||||
) {
|
||||
this._topEdgeDistance = topLeftPixel[1] / rendererHeight;
|
||||
}
|
||||
}
|
||||
// Top edge
|
||||
if (this._topEdgeAnchor === VerticalAnchor.WindowTop) {
|
||||
this._topEdgeDistance = topLeftPixel[1] - parentMinY;
|
||||
} else if (this._topEdgeAnchor === VerticalAnchor.WindowBottom) {
|
||||
this._topEdgeDistance = topLeftPixel[1] - parentMaxY;
|
||||
} else if (this._topEdgeAnchor === VerticalAnchor.Proportional) {
|
||||
this._topEdgeDistance = (topLeftPixel[1] - parentMinY) / parentHeight;
|
||||
} else if (this._topEdgeAnchor === VerticalAnchor.WindowCenter) {
|
||||
this._topEdgeDistance = topLeftPixel[1] - parentCenterY;
|
||||
}
|
||||
|
||||
// It's fine to reuse workingPoint as topLeftPixel is no longer used.
|
||||
const bottomRightPixel = layer.convertCoords(
|
||||
this.owner.getDrawableX() + this.owner.getWidth(),
|
||||
@@ -143,49 +148,30 @@ namespace gdjs {
|
||||
workingPoint
|
||||
);
|
||||
|
||||
//Right edge
|
||||
if (
|
||||
this._rightEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_LEFT
|
||||
) {
|
||||
this._rightEdgeDistance = bottomRightPixel[0];
|
||||
} else {
|
||||
if (
|
||||
this._rightEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_RIGHT
|
||||
) {
|
||||
this._rightEdgeDistance = rendererWidth - bottomRightPixel[0];
|
||||
} else {
|
||||
if (
|
||||
this._rightEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.PROPORTIONAL
|
||||
) {
|
||||
this._rightEdgeDistance = bottomRightPixel[0] / rendererWidth;
|
||||
}
|
||||
}
|
||||
// Right edge
|
||||
if (this._rightEdgeAnchor === HorizontalAnchor.WindowLeft) {
|
||||
this._rightEdgeDistance = bottomRightPixel[0] - parentMinX;
|
||||
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowRight) {
|
||||
this._rightEdgeDistance = bottomRightPixel[0] - parentMaxX;
|
||||
} else if (this._rightEdgeAnchor === HorizontalAnchor.Proportional) {
|
||||
this._rightEdgeDistance =
|
||||
(bottomRightPixel[0] - parentMinX) / parentWidth;
|
||||
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowCenter) {
|
||||
this._rightEdgeDistance = bottomRightPixel[0] - parentCenterX;
|
||||
}
|
||||
|
||||
//Bottom edge
|
||||
if (
|
||||
this._bottomEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_TOP
|
||||
) {
|
||||
this._bottomEdgeDistance = bottomRightPixel[1];
|
||||
} else {
|
||||
if (
|
||||
this._bottomEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_BOTTOM
|
||||
) {
|
||||
this._bottomEdgeDistance = rendererHeight - bottomRightPixel[1];
|
||||
} else {
|
||||
if (
|
||||
this._bottomEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.PROPORTIONAL
|
||||
) {
|
||||
this._bottomEdgeDistance = bottomRightPixel[1] / rendererHeight;
|
||||
}
|
||||
}
|
||||
// Bottom edge
|
||||
if (this._bottomEdgeAnchor === VerticalAnchor.WindowTop) {
|
||||
this._bottomEdgeDistance = bottomRightPixel[1] - parentMinY;
|
||||
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowBottom) {
|
||||
this._bottomEdgeDistance = bottomRightPixel[1] - parentMaxY;
|
||||
} else if (this._bottomEdgeAnchor === VerticalAnchor.Proportional) {
|
||||
this._bottomEdgeDistance =
|
||||
(bottomRightPixel[1] - parentMinY) / parentHeight;
|
||||
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowCenter) {
|
||||
this._bottomEdgeDistance = bottomRightPixel[1] - parentCenterY;
|
||||
}
|
||||
|
||||
this._invalidDistances = false;
|
||||
} else {
|
||||
//Move and resize the object if needed
|
||||
@@ -194,93 +180,50 @@ namespace gdjs {
|
||||
let rightPixel = 0;
|
||||
let bottomPixel = 0;
|
||||
|
||||
//Left edge
|
||||
if (
|
||||
this._leftEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_LEFT
|
||||
) {
|
||||
leftPixel = this._leftEdgeDistance;
|
||||
} else {
|
||||
if (
|
||||
this._leftEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_RIGHT
|
||||
) {
|
||||
leftPixel = rendererWidth - this._leftEdgeDistance;
|
||||
} else {
|
||||
if (
|
||||
this._leftEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.PROPORTIONAL
|
||||
) {
|
||||
leftPixel = this._leftEdgeDistance * rendererWidth;
|
||||
}
|
||||
}
|
||||
// Left edge
|
||||
if (this._leftEdgeAnchor === HorizontalAnchor.WindowLeft) {
|
||||
leftPixel = parentMinX + this._leftEdgeDistance;
|
||||
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowRight) {
|
||||
leftPixel = parentMaxX + this._leftEdgeDistance;
|
||||
} else if (this._leftEdgeAnchor === HorizontalAnchor.Proportional) {
|
||||
leftPixel = parentMinX + this._leftEdgeDistance * parentWidth;
|
||||
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowCenter) {
|
||||
leftPixel = parentCenterX + this._leftEdgeDistance;
|
||||
}
|
||||
|
||||
//Top edge
|
||||
if (
|
||||
this._topEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_TOP
|
||||
) {
|
||||
topPixel = this._topEdgeDistance;
|
||||
} else {
|
||||
if (
|
||||
this._topEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_BOTTOM
|
||||
) {
|
||||
topPixel = rendererHeight - this._topEdgeDistance;
|
||||
} else {
|
||||
if (
|
||||
this._topEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.PROPORTIONAL
|
||||
) {
|
||||
topPixel = this._topEdgeDistance * rendererHeight;
|
||||
}
|
||||
}
|
||||
// Top edge
|
||||
if (this._topEdgeAnchor === VerticalAnchor.WindowTop) {
|
||||
topPixel = parentMinY + this._topEdgeDistance;
|
||||
} else if (this._topEdgeAnchor === VerticalAnchor.WindowBottom) {
|
||||
topPixel = parentMaxY + this._topEdgeDistance;
|
||||
} else if (this._topEdgeAnchor === VerticalAnchor.Proportional) {
|
||||
topPixel = parentMinY + this._topEdgeDistance * parentHeight;
|
||||
} else if (this._topEdgeAnchor === VerticalAnchor.WindowCenter) {
|
||||
topPixel = parentCenterY + this._topEdgeDistance;
|
||||
}
|
||||
|
||||
//Right edge
|
||||
if (
|
||||
this._rightEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_LEFT
|
||||
) {
|
||||
rightPixel = this._rightEdgeDistance;
|
||||
} else {
|
||||
if (
|
||||
this._rightEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.WINDOW_RIGHT
|
||||
) {
|
||||
rightPixel = rendererWidth - this._rightEdgeDistance;
|
||||
} else {
|
||||
if (
|
||||
this._rightEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.PROPORTIONAL
|
||||
) {
|
||||
rightPixel = this._rightEdgeDistance * rendererWidth;
|
||||
}
|
||||
}
|
||||
// Right edge
|
||||
if (this._rightEdgeAnchor === HorizontalAnchor.WindowLeft) {
|
||||
rightPixel = parentMinX + this._rightEdgeDistance;
|
||||
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowRight) {
|
||||
rightPixel = parentMaxX + this._rightEdgeDistance;
|
||||
} else if (this._rightEdgeAnchor === HorizontalAnchor.Proportional) {
|
||||
rightPixel = parentMinX + this._rightEdgeDistance * parentWidth;
|
||||
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowCenter) {
|
||||
rightPixel = parentCenterX + this._rightEdgeDistance;
|
||||
}
|
||||
|
||||
//Bottom edge
|
||||
if (
|
||||
this._bottomEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_TOP
|
||||
) {
|
||||
bottomPixel = this._bottomEdgeDistance;
|
||||
} else {
|
||||
if (
|
||||
this._bottomEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.WINDOW_BOTTOM
|
||||
) {
|
||||
bottomPixel = rendererHeight - this._bottomEdgeDistance;
|
||||
} else {
|
||||
if (
|
||||
this._bottomEdgeAnchor ===
|
||||
AnchorRuntimeBehavior.VerticalAnchor.PROPORTIONAL
|
||||
) {
|
||||
bottomPixel = this._bottomEdgeDistance * rendererHeight;
|
||||
}
|
||||
}
|
||||
// Bottom edge
|
||||
if (this._bottomEdgeAnchor === VerticalAnchor.WindowTop) {
|
||||
bottomPixel = parentMinY + this._bottomEdgeDistance;
|
||||
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowBottom) {
|
||||
bottomPixel = parentMaxY + this._bottomEdgeDistance;
|
||||
} else if (this._bottomEdgeAnchor === VerticalAnchor.Proportional) {
|
||||
bottomPixel = parentMinY + this._bottomEdgeDistance * parentHeight;
|
||||
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowCenter) {
|
||||
bottomPixel = parentCenterY + this._bottomEdgeDistance;
|
||||
}
|
||||
|
||||
// It's fine to reuse workingPoint as topLeftPixel is no longer used.
|
||||
const topLeftCoord = layer.convertInverseCoords(
|
||||
leftPixel,
|
||||
@@ -303,27 +246,18 @@ namespace gdjs {
|
||||
// Compatibility with GD <= 5.0.133
|
||||
if (this._useLegacyBottomAndRightAnchors) {
|
||||
//Move and resize the object according to the anchors
|
||||
if (
|
||||
this._rightEdgeAnchor !==
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.NONE
|
||||
) {
|
||||
if (this._rightEdgeAnchor !== HorizontalAnchor.None) {
|
||||
this.owner.setWidth(right - left);
|
||||
}
|
||||
if (
|
||||
this._bottomEdgeAnchor !== AnchorRuntimeBehavior.VerticalAnchor.NONE
|
||||
) {
|
||||
if (this._bottomEdgeAnchor !== VerticalAnchor.None) {
|
||||
this.owner.setHeight(bottom - top);
|
||||
}
|
||||
if (
|
||||
this._leftEdgeAnchor !== AnchorRuntimeBehavior.HorizontalAnchor.NONE
|
||||
) {
|
||||
if (this._leftEdgeAnchor !== HorizontalAnchor.None) {
|
||||
this.owner.setX(
|
||||
left + this.owner.getX() - this.owner.getDrawableX()
|
||||
);
|
||||
}
|
||||
if (
|
||||
this._topEdgeAnchor !== AnchorRuntimeBehavior.VerticalAnchor.NONE
|
||||
) {
|
||||
if (this._topEdgeAnchor !== VerticalAnchor.None) {
|
||||
this.owner.setY(
|
||||
top + this.owner.getY() - this.owner.getDrawableY()
|
||||
);
|
||||
@@ -333,25 +267,18 @@ namespace gdjs {
|
||||
else {
|
||||
// Resize if right and left anchors are set
|
||||
if (
|
||||
this._rightEdgeAnchor !==
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.NONE &&
|
||||
this._leftEdgeAnchor !== AnchorRuntimeBehavior.HorizontalAnchor.NONE
|
||||
this._rightEdgeAnchor !== HorizontalAnchor.None &&
|
||||
this._leftEdgeAnchor !== HorizontalAnchor.None
|
||||
) {
|
||||
this.owner.setWidth(right - left);
|
||||
this.owner.setX(left);
|
||||
} else {
|
||||
if (
|
||||
this._leftEdgeAnchor !==
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.NONE
|
||||
) {
|
||||
if (this._leftEdgeAnchor !== HorizontalAnchor.None) {
|
||||
this.owner.setX(
|
||||
left + this.owner.getX() - this.owner.getDrawableX()
|
||||
);
|
||||
}
|
||||
if (
|
||||
this._rightEdgeAnchor !==
|
||||
AnchorRuntimeBehavior.HorizontalAnchor.NONE
|
||||
) {
|
||||
if (this._rightEdgeAnchor !== HorizontalAnchor.None) {
|
||||
this.owner.setX(
|
||||
right +
|
||||
this.owner.getX() -
|
||||
@@ -362,24 +289,18 @@ namespace gdjs {
|
||||
}
|
||||
// Resize if top and bottom anchors are set
|
||||
if (
|
||||
this._bottomEdgeAnchor !==
|
||||
AnchorRuntimeBehavior.VerticalAnchor.NONE &&
|
||||
this._topEdgeAnchor !== AnchorRuntimeBehavior.VerticalAnchor.NONE
|
||||
this._bottomEdgeAnchor !== VerticalAnchor.None &&
|
||||
this._topEdgeAnchor !== VerticalAnchor.None
|
||||
) {
|
||||
this.owner.setHeight(bottom - top);
|
||||
this.owner.setY(top);
|
||||
} else {
|
||||
if (
|
||||
this._topEdgeAnchor !== AnchorRuntimeBehavior.VerticalAnchor.NONE
|
||||
) {
|
||||
if (this._topEdgeAnchor !== VerticalAnchor.None) {
|
||||
this.owner.setY(
|
||||
top + this.owner.getY() - this.owner.getDrawableY()
|
||||
);
|
||||
}
|
||||
if (
|
||||
this._bottomEdgeAnchor !==
|
||||
AnchorRuntimeBehavior.VerticalAnchor.NONE
|
||||
) {
|
||||
if (this._bottomEdgeAnchor !== VerticalAnchor.None) {
|
||||
this.owner.setY(
|
||||
bottom +
|
||||
this.owner.getY() -
|
||||
@@ -393,19 +314,6 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {}
|
||||
|
||||
static HorizontalAnchor = {
|
||||
NONE: 0,
|
||||
WINDOW_LEFT: 1,
|
||||
WINDOW_RIGHT: 2,
|
||||
PROPORTIONAL: 3,
|
||||
};
|
||||
static VerticalAnchor = {
|
||||
NONE: 0,
|
||||
WINDOW_TOP: 1,
|
||||
WINDOW_BOTTOM: 2,
|
||||
PROPORTIONAL: 3,
|
||||
};
|
||||
}
|
||||
gdjs.registerBehavior(
|
||||
'AnchorBehavior::AnchorBehavior',
|
||||
|
@@ -36,6 +36,13 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
||||
const setGameResolutionSizeAndStep = (width, height) => {
|
||||
runtimeGame.setGameResolutionSize(width, height);
|
||||
// This method is called by the main loop:
|
||||
runtimeScene.onGameResolutionResized();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
};
|
||||
|
||||
function createObject(behaviorProperties) {
|
||||
const object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
@@ -67,13 +74,10 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
['rightEdgeAnchor', 'leftEdgeAnchor'].forEach((objectEdge) => {
|
||||
it(`anchors the ${objectEdge} edge of object to window left (fixed)`, function () {
|
||||
const object = createObject({ [objectEdge]: 1 });
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
object.setPosition(500, 500);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
runtimeGame.setGameResolutionSize(2000, 2000);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(500);
|
||||
expect(object.getY()).to.equal(500);
|
||||
@@ -83,29 +87,36 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
['rightEdgeAnchor', 'leftEdgeAnchor'].forEach((objectEdge) => {
|
||||
it(`anchors the ${objectEdge} edge of object to window right (fixed)`, function () {
|
||||
const object = createObject({ [objectEdge]: 2 });
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
object.setPosition(500, 500);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
runtimeGame.setGameResolutionSize(2000, 2000);
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getX()).to.equal(1500);
|
||||
expect(object.getY()).to.equal(500);
|
||||
expect(object.getWidth()).to.equal(10);
|
||||
});
|
||||
});
|
||||
['rightEdgeAnchor', 'leftEdgeAnchor'].forEach((objectEdge) => {
|
||||
it(`anchors the ${objectEdge} edge of object to window center (fixed)`, function () {
|
||||
const object = createObject({ [objectEdge]: 4 });
|
||||
object.setPosition(500, 500);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(1000);
|
||||
expect(object.getY()).to.equal(500);
|
||||
expect(object.getWidth()).to.equal(10);
|
||||
});
|
||||
});
|
||||
|
||||
it('anchors the right and left edge of object (fixed)', function () {
|
||||
const object = createObject({ leftEdgeAnchor: 1, rightEdgeAnchor: 2 });
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
object.setPosition(500, 500);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
runtimeGame.setGameResolutionSize(2000, 2000);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(500);
|
||||
expect(object.getY()).to.equal(500);
|
||||
@@ -114,13 +125,10 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
|
||||
it('anchors the left edge of object (proportional)', function () {
|
||||
const object = createObject({ leftEdgeAnchor: 3 });
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
object.setPosition(500, 500);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
runtimeGame.setGameResolutionSize(2000, 2000);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(1000);
|
||||
expect(object.getY()).to.equal(500);
|
||||
@@ -132,13 +140,10 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
['topEdgeAnchor', 'bottomEdgeAnchor'].forEach((objectEdge) => {
|
||||
it(`anchors the ${objectEdge} edge of object to window top (fixed)`, function () {
|
||||
const object = createObject({ [objectEdge]: 1 });
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
object.setPosition(500, 500);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
runtimeGame.setGameResolutionSize(2000, 2000);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(500);
|
||||
expect(object.getY()).to.equal(500);
|
||||
@@ -148,29 +153,36 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
['topEdgeAnchor', 'bottomEdgeAnchor'].forEach((objectEdge) => {
|
||||
it(`anchors the ${objectEdge} edge of object to window bottom (fixed)`, function () {
|
||||
const object = createObject({ [objectEdge]: 2 });
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
object.setPosition(500, 500);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
runtimeGame.setGameResolutionSize(2000, 2000);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(500);
|
||||
expect(object.getY()).to.equal(1500);
|
||||
expect(object.getWidth()).to.equal(10);
|
||||
});
|
||||
});
|
||||
['topEdgeAnchor', 'bottomEdgeAnchor'].forEach((objectEdge) => {
|
||||
it(`anchors the ${objectEdge} edge of object to window center (fixed)`, function () {
|
||||
const object = createObject({ [objectEdge]: 4 });
|
||||
object.setPosition(500, 500);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(500);
|
||||
expect(object.getY()).to.equal(1000);
|
||||
expect(object.getWidth()).to.equal(10);
|
||||
});
|
||||
});
|
||||
|
||||
it('anchors the top and bottom edge of object (fixed)', function () {
|
||||
const object = createObject({ topEdgeAnchor: 1, bottomEdgeAnchor: 2 });
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
object.setPosition(500, 500);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
runtimeGame.setGameResolutionSize(2000, 2000);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(500);
|
||||
expect(object.getY()).to.equal(500);
|
||||
@@ -179,13 +191,10 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
|
||||
it('anchors the top edge of object (proportional)', function () {
|
||||
const object = createObject({ topEdgeAnchor: 3 });
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
object.setPosition(500, 500);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
runtimeGame.setGameResolutionSize(2000, 2000);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
setGameResolutionSizeAndStep(2000, 2000);
|
||||
|
||||
expect(object.getX()).to.equal(500);
|
||||
expect(object.getY()).to.equal(1000);
|
||||
|
@@ -52,7 +52,7 @@ module.exports = {
|
||||
.setType('number');
|
||||
adjustmentProperties
|
||||
.getOrCreate('saturation')
|
||||
.setValue('2')
|
||||
.setValue('1')
|
||||
.setLabel(_('Saturation (between 0 and 5)'))
|
||||
.setType('number');
|
||||
adjustmentProperties
|
||||
@@ -77,7 +77,7 @@ module.exports = {
|
||||
.setType('number');
|
||||
adjustmentProperties
|
||||
.getOrCreate('blue')
|
||||
.setValue('0.6')
|
||||
.setValue('1')
|
||||
.setLabel(_('Blue (between 0 and 5)'))
|
||||
.setType('number');
|
||||
adjustmentProperties
|
||||
|
@@ -369,6 +369,36 @@ module.exports = {
|
||||
'gdjs.multiplayerMessageManager.hasCustomMessageBeenReceived'
|
||||
);
|
||||
|
||||
extension
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'ObjectsSynchronizationRate',
|
||||
_('Objects synchronization rate'),
|
||||
_(
|
||||
'objects synchronization rate (between 1 and 60, default is 30 times per second)'
|
||||
),
|
||||
_('objects synchronization rate'),
|
||||
_('Advanced'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.useStandardParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions().setDescription(_('Sync rate'))
|
||||
)
|
||||
.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.setObjectsSynchronizationRate')
|
||||
.setGetter('gdjs.multiplayer.getObjectsSynchronizationRate');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'IsPlayerHost',
|
||||
@@ -392,13 +422,13 @@ module.exports = {
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.isPlayerHost');
|
||||
.setFunctionName('gdjs.multiplayer.isCurrentPlayerHost');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'HasAnyPlayerLeft',
|
||||
_('Any player has left'),
|
||||
_('Check if any player has left the lobby.'),
|
||||
_('Check if any player has left the lobby game.'),
|
||||
_('Any player has left'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
@@ -423,7 +453,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'HasPlayerLeft',
|
||||
_('Player has left'),
|
||||
_('Check if the player has left the lobby.'),
|
||||
_('Check if the player has left the lobby game.'),
|
||||
_('Player _PARAM0_ has left'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
@@ -448,8 +478,10 @@ module.exports = {
|
||||
extension
|
||||
.addExpression(
|
||||
'LastLeftPlayerNumber',
|
||||
_('Last left player number'),
|
||||
_('Returns the number of the player that has just left the lobby.'),
|
||||
_('Player number that just left'),
|
||||
_(
|
||||
'Returns the player number of the player that has just left the lobby.'
|
||||
),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
@@ -524,8 +556,10 @@ module.exports = {
|
||||
extension
|
||||
.addExpression(
|
||||
'LastJoinedPlayerNumber',
|
||||
_('Last joined player number'),
|
||||
_('Returns the number of the player that has just joined the lobby.'),
|
||||
_('Player number that just joined'),
|
||||
_(
|
||||
'Returns the player number of the player that has just joined the lobby.'
|
||||
),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
@@ -546,6 +580,61 @@ module.exports = {
|
||||
'gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined'
|
||||
);
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'IsMigratingHost',
|
||||
_('Host is migrating'),
|
||||
_(
|
||||
'Check if the host is migrating, in order to adapt the game state (like pausing the game).'
|
||||
),
|
||||
_('Host is migrating'),
|
||||
_('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.multiplayer.isMigratingHost');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'EndLobbyWhenHostLeaves',
|
||||
_('Configure lobby game to end when host leaves'),
|
||||
_(
|
||||
'Configure the lobby game to end when the host leaves. This will trigger the "Lobby game has just ended" condition. (Default behavior is to migrate the host)'
|
||||
),
|
||||
_('Configure lobby game to end when host leaves'),
|
||||
_('Advanced'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.addParameter('yesorno', _('End lobby game when host leaves'), '', 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.endLobbyWhenHostLeaves');
|
||||
|
||||
extension
|
||||
.addStrExpression(
|
||||
'MessageData',
|
||||
|
@@ -145,7 +145,7 @@ namespace gdjs {
|
||||
} = {};
|
||||
|
||||
// The number of times per second the scene data should be synchronized.
|
||||
const sceneSyncDataTickRate = 1;
|
||||
const sceneSyncDataSyncRate = 1;
|
||||
let lastSceneSyncTimestamp = 0;
|
||||
let lastSentSceneSyncData: LayoutNetworkSyncData | null = null;
|
||||
let numberOfForcedSceneUpdates = 0;
|
||||
@@ -154,7 +154,7 @@ namespace gdjs {
|
||||
>();
|
||||
|
||||
// The number of times per second the game data should be synchronized.
|
||||
const gameSyncDataTickRate = 1;
|
||||
const gameSyncDataSyncRate = 1;
|
||||
let lastGameSyncTimestamp = 0;
|
||||
let lastSentGameSyncData: GameNetworkSyncData | null = null;
|
||||
let numberOfForcedGameUpdates = 0;
|
||||
@@ -164,8 +164,8 @@ namespace gdjs {
|
||||
|
||||
// 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;
|
||||
const heartbeatSyncRate = 1;
|
||||
let lastHeartbeatSentTimestamp = 0;
|
||||
let _playersLastRoundTripTimes: {
|
||||
[playerNumber: number]: number[];
|
||||
} = {};
|
||||
@@ -531,7 +531,10 @@ namespace gdjs {
|
||||
currentPlayerObjectOwnership === previousOwner ||
|
||||
// the object is already owned by the new owner. (may have been changed by another player faster)
|
||||
currentPlayerObjectOwnership === newOwner;
|
||||
if (gdjs.multiplayer.isPlayerHost() && !ownershipChangeIsCoherent) {
|
||||
if (
|
||||
gdjs.multiplayer.isCurrentPlayerHost() &&
|
||||
!ownershipChangeIsCoherent
|
||||
) {
|
||||
// We received an ownership change message for an object which is in an unexpected state.
|
||||
// There may be some lag, and multiple ownership changes may have been sent by the other players.
|
||||
// As the host, let's not change the ownership and let the player revert it.
|
||||
@@ -560,7 +563,7 @@ namespace gdjs {
|
||||
// If we are the host,
|
||||
// so we need to relay the ownership change to others,
|
||||
// and expect an acknowledgment from them.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
// We don't need to send the message to the player who sent the ownership change message.
|
||||
const otherPeerIds = connectedPeerIds.filter(
|
||||
@@ -738,7 +741,7 @@ namespace gdjs {
|
||||
|
||||
// If we are are the host,
|
||||
// we need to relay the position to others except the player who sent the update message.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
const otherPeerIds = connectedPeerIds.filter(
|
||||
(peerId) => peerId !== messageSender
|
||||
@@ -863,7 +866,10 @@ namespace gdjs {
|
||||
currentPlayerVariableOwnership === previousOwner ||
|
||||
// the variable is already owned by the new owner. (may have been changed by another player faster)
|
||||
currentPlayerVariableOwnership === newOwner;
|
||||
if (gdjs.multiplayer.isPlayerHost() && !ownershipChangeIsCoherent) {
|
||||
if (
|
||||
gdjs.multiplayer.isCurrentPlayerHost() &&
|
||||
!ownershipChangeIsCoherent
|
||||
) {
|
||||
// We received an ownership change message for a variable which is in an unexpected state.
|
||||
// There may be some lag, and multiple ownership changes may have been sent by the other players.
|
||||
// As the host, let's not change the ownership and let the player revert it.
|
||||
@@ -892,7 +898,7 @@ namespace gdjs {
|
||||
// If we are the host,
|
||||
// we need to relay the ownership change to others,
|
||||
// and expect an acknowledgment from them.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
// We don't need to send the message to the player who sent the ownership change message.
|
||||
const otherPeerIds = connectedPeerIds.filter(
|
||||
@@ -1336,7 +1342,7 @@ namespace gdjs {
|
||||
|
||||
// If we are the host, we need to relay the destruction to others.
|
||||
// And expect an acknowledgment from everyone else as well.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
// We don't need to send the message to the player who sent the destroy message.
|
||||
const otherPeerIds = connectedPeerIds.filter(
|
||||
@@ -1429,7 +1435,7 @@ namespace gdjs {
|
||||
|
||||
// If we are the host, we can consider this messaged as received
|
||||
// and add it to the list of custom messages to process on top of the messages received.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const messagesList = gdjs.multiplayerPeerJsHelper.getOrCreateMessagesList(
|
||||
messageName
|
||||
);
|
||||
@@ -1592,7 +1598,7 @@ namespace gdjs {
|
||||
|
||||
// If we are the host,
|
||||
// so we need to relay the message to others.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
// In the case of custom messages, we relay the message to all players, including the sender.
|
||||
// This allows the sender to process it the same way others would, when they receive the event.
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
@@ -1650,7 +1656,7 @@ namespace gdjs {
|
||||
|
||||
const hasSceneBeenSyncedRecently = () => {
|
||||
return (
|
||||
getTimeNow() - lastSceneSyncTimestamp < 1000 / sceneSyncDataTickRate
|
||||
getTimeNow() - lastSceneSyncTimestamp < 1000 / sceneSyncDataSyncRate
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1664,6 +1670,7 @@ namespace gdjs {
|
||||
|
||||
const sceneNetworkSyncData = runtimeScene.getNetworkSyncData({
|
||||
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
|
||||
isHost: gdjs.multiplayer.isCurrentPlayerHost(),
|
||||
});
|
||||
if (!sceneNetworkSyncData) {
|
||||
return;
|
||||
@@ -1737,7 +1744,7 @@ namespace gdjs {
|
||||
|
||||
// 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()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
// We don't need to send the message to the player who sent the update message.
|
||||
const otherPeerIds = connectedPeerIds.filter(
|
||||
@@ -1814,7 +1821,7 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const hasGameBeenSyncedRecently = () => {
|
||||
return getTimeNow() - lastGameSyncTimestamp < 1000 / gameSyncDataTickRate;
|
||||
return getTimeNow() - lastGameSyncTimestamp < 1000 / gameSyncDataSyncRate;
|
||||
};
|
||||
|
||||
const handleUpdateGameMessagesToSend = (
|
||||
@@ -1827,6 +1834,7 @@ namespace gdjs {
|
||||
|
||||
const gameNetworkSyncData = runtimeScene.getGame().getNetworkSyncData({
|
||||
playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),
|
||||
isHost: gdjs.multiplayer.isCurrentPlayerHost(),
|
||||
});
|
||||
if (!gameNetworkSyncData) {
|
||||
return;
|
||||
@@ -1888,7 +1896,7 @@ namespace gdjs {
|
||||
|
||||
// If we are are the host,
|
||||
// we need to relay the game update to others except the player who sent the update message.
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
// We don't need to send the message to the player who sent the update message.
|
||||
const otherPeerIds = connectedPeerIds.filter(
|
||||
@@ -1937,9 +1945,10 @@ namespace gdjs {
|
||||
messageName: string;
|
||||
messageData: any;
|
||||
} => {
|
||||
// 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.
|
||||
// If we create the heartbeat meassage, we are the host,
|
||||
// Ensure our player number is correctly set when the first heartbeat is sent.
|
||||
_playersInfo[gdjs.multiplayer.getCurrentPlayerNumber()] = {
|
||||
ping: 0, // we are the host, so we don't need to compute the ping.
|
||||
playerId: gdjs.playerAuthentication.getUserId(),
|
||||
username: gdjs.playerAuthentication.getUsername(),
|
||||
};
|
||||
@@ -1976,15 +1985,15 @@ namespace gdjs {
|
||||
};
|
||||
const hasSentHeartbeatRecently = () => {
|
||||
return (
|
||||
!!lastHeartbeatTimestamp &&
|
||||
getTimeNow() - lastHeartbeatTimestamp < 1000 / heartbeatTickRate
|
||||
!!lastHeartbeatSentTimestamp &&
|
||||
getTimeNow() - lastHeartbeatSentTimestamp < 1000 / heartbeatSyncRate
|
||||
);
|
||||
};
|
||||
const handleHeartbeatsToSend = () => {
|
||||
// Only host sends heartbeats to all players regularly:
|
||||
// - it allows them to send a heartbeat back immediately so that the host can compute the ping.
|
||||
// - it allows to pass along the pings of all players to all players.
|
||||
if (!gdjs.multiplayer.isPlayerHost()) {
|
||||
if (!gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1997,7 +2006,7 @@ namespace gdjs {
|
||||
const { messageName, messageData } = createHeartbeatMessage();
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
|
||||
lastHeartbeatTimestamp = getTimeNow();
|
||||
lastHeartbeatSentTimestamp = getTimeNow();
|
||||
};
|
||||
|
||||
const handleHeartbeatsReceived = () => {
|
||||
@@ -2024,7 +2033,7 @@ namespace gdjs {
|
||||
|
||||
// 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()) {
|
||||
if (!gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
|
||||
const currentlyKnownPlayerNumbers = Object.keys(
|
||||
_playersInfo
|
||||
@@ -2134,12 +2143,20 @@ namespace gdjs {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
const { messageName, messageData } = createHeartbeatMessage();
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
lastHeartbeatTimestamp = getTimeNow();
|
||||
lastHeartbeatSentTimestamp = getTimeNow();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const hasReceivedHeartbeatFromPlayer = (playerNumber: number) => {
|
||||
// Consider that a player has sent a heartbeat if we have been able to calculate
|
||||
// at least one round trip time for them.
|
||||
const playerLastRoundTripTimes =
|
||||
_playersLastRoundTripTimes[playerNumber] || [];
|
||||
return playerLastRoundTripTimes.length > 0;
|
||||
};
|
||||
|
||||
const getPlayerPing = (playerNumber: number) => {
|
||||
const playerInfo = _playersInfo[playerNumber];
|
||||
if (!playerInfo) {
|
||||
@@ -2153,7 +2170,15 @@ namespace gdjs {
|
||||
return getPlayerPing(currentPlayerNumber);
|
||||
};
|
||||
|
||||
const markPlayerAsDisconnected = (playerNumber: number) => {
|
||||
const markPlayerAsDisconnected = ({
|
||||
runtimeScene,
|
||||
playerNumber,
|
||||
peerId,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
playerNumber: number;
|
||||
peerId?: string;
|
||||
}) => {
|
||||
logger.info(`Marking player ${playerNumber} as disconnected.`);
|
||||
_playerNumbersWhoJustLeft.push(playerNumber);
|
||||
// Temporarily save the username in another variable to be used for the notification,
|
||||
@@ -2161,23 +2186,31 @@ namespace gdjs {
|
||||
_temporaryPlayerNumberToUsername[playerNumber] = getPlayerUsername(
|
||||
playerNumber
|
||||
);
|
||||
clearPlayerTempData(playerNumber);
|
||||
|
||||
// If Player 1 has disconnected, just end the game.
|
||||
if (playerNumber === 1) {
|
||||
logger.info('Host has disconnected, ending the game.');
|
||||
clearAllMessagesTempData();
|
||||
gdjs.multiplayer.handleLobbyGameEnded();
|
||||
return;
|
||||
// If Host has disconnected, either switch host or stop the game.
|
||||
if (peerId && peerId === gdjs.multiplayer.hostPeerId) {
|
||||
const shouldEndLobbyGame = gdjs.multiplayer.shouldEndLobbyWhenHostLeaves();
|
||||
if (shouldEndLobbyGame) {
|
||||
logger.info('Host has disconnected, ending the game.');
|
||||
|
||||
clearAllMessagesTempData();
|
||||
gdjs.multiplayer.handleLobbyGameEnded();
|
||||
} else {
|
||||
logger.info('Host has disconnected, switching host.');
|
||||
|
||||
gdjs.multiplayer.handleHostDisconnected({ runtimeScene });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
const { messageName, messageData } = createHeartbeatMessage();
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
lastHeartbeatTimestamp = getTimeNow();
|
||||
lastHeartbeatSentTimestamp = getTimeNow();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2200,7 +2233,10 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// We rely on the p2p helper to know who has disconnected.
|
||||
const justDisconnectedPlayerNumbers: number[] = [];
|
||||
const justDisconnectedPlayers: {
|
||||
playerNumber: number;
|
||||
peerId: string;
|
||||
}[] = [];
|
||||
|
||||
const justDisconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();
|
||||
if (justDisconnectedPeers.length) {
|
||||
@@ -2212,14 +2248,17 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
logger.info(`Player ${disconnectedPlayerNumber} has disconnected.`);
|
||||
justDisconnectedPlayerNumbers.push(disconnectedPlayerNumber);
|
||||
justDisconnectedPlayers.push({
|
||||
playerNumber: disconnectedPlayerNumber,
|
||||
peerId: disconnectedPeer,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const playerNumber of justDisconnectedPlayerNumbers) {
|
||||
for (const { playerNumber, peerId } of justDisconnectedPlayers) {
|
||||
// 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()) {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
const instances = runtimeScene.getAdhocListOfAllInstances();
|
||||
for (const instance of instances) {
|
||||
const behavior = instance.getBehavior(
|
||||
@@ -2243,7 +2282,7 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
markPlayerAsDisconnected(playerNumber);
|
||||
markPlayerAsDisconnected({ runtimeScene, playerNumber, peerId });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2303,6 +2342,10 @@ namespace gdjs {
|
||||
return _playersInfo[playerNumber] !== undefined;
|
||||
};
|
||||
|
||||
const getPlayersInfo = () => {
|
||||
return _playersInfo;
|
||||
};
|
||||
|
||||
const endGameMessageName = '#endGame';
|
||||
const createEndGameMessage = (): {
|
||||
messageName: string;
|
||||
@@ -2315,7 +2358,7 @@ namespace gdjs {
|
||||
};
|
||||
const sendEndGameMessage = () => {
|
||||
// Only the host can end the game.
|
||||
if (!gdjs.multiplayer.isPlayerHost()) {
|
||||
if (!gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2327,8 +2370,8 @@ namespace gdjs {
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
};
|
||||
|
||||
const handleEndGameMessages = () => {
|
||||
if (gdjs.multiplayer.isPlayerHost()) {
|
||||
const handleEndGameMessagesReceived = () => {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
// Only other players need to react to the end game message.
|
||||
return;
|
||||
}
|
||||
@@ -2348,6 +2391,50 @@ namespace gdjs {
|
||||
gdjs.multiplayer.handleLobbyGameEnded();
|
||||
};
|
||||
|
||||
const resumeGameMessageName = '#resumeGame';
|
||||
const createResumeGameMessage = (): {
|
||||
messageName: string;
|
||||
messageData: any;
|
||||
} => {
|
||||
return {
|
||||
messageName: resumeGameMessageName,
|
||||
messageData: {},
|
||||
};
|
||||
};
|
||||
const sendResumeGameMessage = () => {
|
||||
// Only the host can inform others that the game is resuming.
|
||||
if (!gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugLogger.info(`Sending resumeGame message.`);
|
||||
|
||||
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
const { messageName, messageData } = createResumeGameMessage();
|
||||
sendDataTo(connectedPeerIds, messageName, messageData);
|
||||
};
|
||||
|
||||
const handleResumeGameMessagesReceived = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (gdjs.multiplayer.isCurrentPlayerHost()) {
|
||||
// Only other players need to react to resume game message.
|
||||
return;
|
||||
}
|
||||
|
||||
const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();
|
||||
const resumeGameMessagesList = p2pMessagesMap.get(resumeGameMessageName);
|
||||
if (!resumeGameMessagesList) {
|
||||
return; // No resume game message received.
|
||||
}
|
||||
const messages = resumeGameMessagesList.getMessages();
|
||||
if (!messages.length) return; // No messages to process.
|
||||
|
||||
logger.info(`Received resumeGame message.`);
|
||||
|
||||
gdjs.multiplayer.resumeGame(runtimeScene);
|
||||
};
|
||||
|
||||
const clearAllMessagesTempData = () => {
|
||||
_playersLastRoundTripTimes = {};
|
||||
_playersInfo = {};
|
||||
@@ -2409,6 +2496,7 @@ namespace gdjs {
|
||||
// Heartbeats.
|
||||
handleHeartbeatsToSend,
|
||||
handleHeartbeatsReceived,
|
||||
hasReceivedHeartbeatFromPlayer,
|
||||
// Pings & usernames.
|
||||
getPlayerPing,
|
||||
getCurrentPlayerPing,
|
||||
@@ -2419,12 +2507,14 @@ namespace gdjs {
|
||||
getConnectedPlayers,
|
||||
getNumberOfConnectedPlayers,
|
||||
isPlayerConnected,
|
||||
getPlayersInfo,
|
||||
// Leaving players.
|
||||
hasAnyPlayerJustLeft,
|
||||
hasPlayerJustLeft,
|
||||
getPlayersWhoJustLeft,
|
||||
getLatestPlayerWhoJustLeft,
|
||||
removePlayerWhoJustLeft,
|
||||
markPlayerAsDisconnected,
|
||||
// Joining players.
|
||||
hasAnyPlayerJustJoined,
|
||||
hasPlayerJustJoined,
|
||||
@@ -2433,8 +2523,11 @@ namespace gdjs {
|
||||
removePlayerWhoJustJoined,
|
||||
// End game.
|
||||
sendEndGameMessage,
|
||||
handleEndGameMessages,
|
||||
handleEndGameMessagesReceived,
|
||||
clearAllMessagesTempData,
|
||||
// Resume game after migration.
|
||||
sendResumeGameMessage,
|
||||
handleResumeGameMessagesReceived,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -398,11 +398,12 @@ namespace gdjs {
|
||||
export const displayErrorNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
showNotification(
|
||||
showNotification({
|
||||
runtimeScene,
|
||||
'An error occurred while displaying the game lobbies, please try again.',
|
||||
'error'
|
||||
);
|
||||
content:
|
||||
'An error occurred while displaying the game lobbies, please try again.',
|
||||
type: 'error',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -412,7 +413,11 @@ namespace gdjs {
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
playerName: string
|
||||
) {
|
||||
showNotification(runtimeScene, `${playerName} left.`, 'warning');
|
||||
showNotification({
|
||||
runtimeScene,
|
||||
content: `${playerName} left.`,
|
||||
type: 'warning',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -422,7 +427,11 @@ namespace gdjs {
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
playerName: string
|
||||
) {
|
||||
showNotification(runtimeScene, `${playerName} joined.`, 'success');
|
||||
showNotification({
|
||||
runtimeScene,
|
||||
content: `${playerName} joined.`,
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -431,11 +440,48 @@ namespace gdjs {
|
||||
export const displayConnectionErrorNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
showNotification(
|
||||
showNotification({
|
||||
runtimeScene,
|
||||
'Could not connect to other players.',
|
||||
'error'
|
||||
);
|
||||
content: 'Could not connect to other players.',
|
||||
type: 'error',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create, display, and hide a notification when a player leaves the game.
|
||||
*/
|
||||
export const displayHostMigrationNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
showNotification({
|
||||
runtimeScene,
|
||||
content: `Migrating host...`,
|
||||
type: 'warning',
|
||||
id: 'migrating-host',
|
||||
persist: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const showHostMigrationFinishedNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
removeNotificationAndShiftOthers('migrating-host');
|
||||
showNotification({
|
||||
runtimeScene,
|
||||
content: `Host migrated!`,
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
export const showHostMigrationFailedNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
removeNotificationAndShiftOthers('migrating-host');
|
||||
showNotification({
|
||||
runtimeScene,
|
||||
content: `Host migration failed.`,
|
||||
type: 'error',
|
||||
});
|
||||
};
|
||||
|
||||
const removeNotificationAndShiftOthers = function (
|
||||
@@ -443,7 +489,9 @@ namespace gdjs {
|
||||
) {
|
||||
const notification = document.getElementById(notificationContainerId);
|
||||
if (!notification) {
|
||||
logger.error('Notification not found.');
|
||||
logger.warn(
|
||||
`Notification ${notificationContainerId} not found. skipping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const index = notificationContainerIds.indexOf(notificationContainerId);
|
||||
@@ -452,8 +500,8 @@ namespace gdjs {
|
||||
}
|
||||
notification.remove();
|
||||
|
||||
// Shift the other notifications up.
|
||||
for (let i = 0; i < notificationContainerIds.length; i++) {
|
||||
// Shift the notifications that are below the one that was removed up.
|
||||
for (let i = index; i < notificationContainerIds.length; i++) {
|
||||
const notification = document.getElementById(
|
||||
notificationContainerIds[i]
|
||||
);
|
||||
@@ -468,11 +516,19 @@ namespace gdjs {
|
||||
/**
|
||||
* Helper to show a notification to the user, that disappears automatically.
|
||||
*/
|
||||
export const showNotification = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
content: string,
|
||||
type: 'success' | 'warning' | 'error'
|
||||
) {
|
||||
export const showNotification = function ({
|
||||
runtimeScene,
|
||||
content,
|
||||
type,
|
||||
id,
|
||||
persist,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
content: string;
|
||||
type: 'success' | 'warning' | 'error';
|
||||
id?: string;
|
||||
persist?: boolean;
|
||||
}) {
|
||||
// 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) {
|
||||
@@ -486,7 +542,8 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// We generate a random ID for the notification, so they can stack.
|
||||
const id = `notification-${Math.random().toString(36).substring(7)}`;
|
||||
const notificationId =
|
||||
id || `notification-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
const domContainer = runtimeScene
|
||||
.getGame()
|
||||
@@ -498,7 +555,7 @@ namespace gdjs {
|
||||
}
|
||||
const divContainer = document.createElement('div');
|
||||
|
||||
divContainer.id = id;
|
||||
divContainer.id = notificationId;
|
||||
divContainer.style.position = 'absolute';
|
||||
divContainer.style.pointerEvents = 'all';
|
||||
divContainer.style.backgroundColor =
|
||||
@@ -544,10 +601,14 @@ namespace gdjs {
|
||||
|
||||
divContainer.appendChild(loggedText);
|
||||
domContainer.appendChild(divContainer);
|
||||
notificationContainerIds.push(id);
|
||||
notificationContainerIds.push(notificationId);
|
||||
|
||||
if (persist) {
|
||||
return;
|
||||
}
|
||||
|
||||
const animationTime = 700;
|
||||
const notificationTime = 5000;
|
||||
const notificationTime = 3000;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
divContainer.animate(
|
||||
@@ -566,7 +627,7 @@ namespace gdjs {
|
||||
}, notificationTime);
|
||||
// Use timeout because onanimationend listener does not work.
|
||||
setTimeout(() => {
|
||||
removeNotificationAndShiftOthers(id);
|
||||
removeNotificationAndShiftOthers(notificationId);
|
||||
}, notificationTime + animationTime);
|
||||
};
|
||||
|
||||
|
@@ -24,48 +24,46 @@ namespace gdjs {
|
||||
actionOnPlayerDisconnect: string;
|
||||
|
||||
// The last time the object has been synchronized.
|
||||
// This is to avoid synchronizing the object too often, see _objectMaxTickRate.
|
||||
// This is to avoid synchronizing the object too often, see _objectMaxSyncRate.
|
||||
_lastObjectSyncTimestamp: number = 0;
|
||||
// The number of times per second the object should be synchronized if it keeps changing.
|
||||
_objectMaxTickRate: number = 60;
|
||||
|
||||
// The last time the basic object info has been synchronized.
|
||||
_lastBasicObjectSyncTimestamp: number = 0;
|
||||
// The number of times per second the object basic info should be synchronized when it doesn't change.
|
||||
_objectBasicInfoTickRate: number = 5;
|
||||
_objectBasicInfoSyncRate: number = 5;
|
||||
// The last data sent to synchronize the basic info of the object.
|
||||
_lastSentBasicObjectSyncData: BasicObjectNetworkSyncData | undefined;
|
||||
// When we know that the basic info of the object has been updated, we can force sending them
|
||||
// on the max tickrate for a number of times to ensure they are received, without the need of an acknowledgment.
|
||||
// on the max SyncRate for a number of times to ensure they are received, without the need of an acknowledgment.
|
||||
_numberOfForcedBasicObjectUpdates: number = 0;
|
||||
|
||||
// The last time the variables have been synchronized.
|
||||
_lastVariablesSyncTimestamp: number = 0;
|
||||
// The number of times per second the variables should be synchronized.
|
||||
_variablesTickRate: number = 1;
|
||||
_variablesSyncRate: number = 1;
|
||||
// The last data sent to synchronize the variables.
|
||||
_lastSentVariableSyncData: VariableNetworkSyncData[] | undefined;
|
||||
// When we know that the variables have been updated, we can force sending them
|
||||
// on the same tickrate as the object update for a number of times
|
||||
// on the same syncRate as the object update for a number of times
|
||||
// to ensure they are received, without the need of an acknowledgment.
|
||||
_numberOfForcedVariablesUpdates: number = 0;
|
||||
|
||||
// The last time the effects have been synchronized.
|
||||
_lastEffectsSyncTimestamp: number = 0;
|
||||
// The number of times per second the effects should be synchronized.
|
||||
_effectsTickRate: number = 1;
|
||||
_effectsSyncRate: number = 1;
|
||||
// The last data sent to synchronize the effects.
|
||||
_lastSentEffectSyncData:
|
||||
| { [effectName: string]: EffectNetworkSyncData }
|
||||
| undefined;
|
||||
// When we know that the effects have been updated, we can force sending them
|
||||
// on the same tickrate as the object update for a number of times
|
||||
// on the same syncRate as the object update for a number of times
|
||||
// to ensure they are received, without the need of an acknowledgment.
|
||||
_numberOfForcedEffectsUpdates: number = 0;
|
||||
|
||||
// To avoid seeing too many logs.
|
||||
_lastLogTimestamp: number = 0;
|
||||
_logTickRate: number = 1;
|
||||
_logSyncRate: number = 1;
|
||||
// Clock to be incremented every time we send a message, to ensure they are ordered
|
||||
// and old messages are ignored.
|
||||
_clock: number = 0;
|
||||
@@ -131,35 +129,35 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
private _hasObjectBeenSyncedWithinMaxRate() {
|
||||
const objectMaxSyncRate = gdjs.multiplayer.getObjectsSynchronizationRate();
|
||||
return (
|
||||
getTimeNow() - this._lastObjectSyncTimestamp <
|
||||
1000 / this._objectMaxTickRate
|
||||
getTimeNow() - this._lastObjectSyncTimestamp < 1000 / objectMaxSyncRate
|
||||
);
|
||||
}
|
||||
|
||||
private _hasObjectBasicInfoBeenSyncedRecently() {
|
||||
return (
|
||||
getTimeNow() - this._lastBasicObjectSyncTimestamp <
|
||||
1000 / this._objectBasicInfoTickRate
|
||||
1000 / this._objectBasicInfoSyncRate
|
||||
);
|
||||
}
|
||||
|
||||
private _haveVariablesBeenSyncedRecently() {
|
||||
return (
|
||||
getTimeNow() - this._lastVariablesSyncTimestamp <
|
||||
1000 / this._variablesTickRate
|
||||
1000 / this._variablesSyncRate
|
||||
);
|
||||
}
|
||||
|
||||
private _haveEffectsBeenSyncedRecently() {
|
||||
return (
|
||||
getTimeNow() - this._lastEffectsSyncTimestamp <
|
||||
1000 / this._effectsTickRate
|
||||
1000 / this._effectsSyncRate
|
||||
);
|
||||
}
|
||||
|
||||
// private _logToConsoleWithThrottle(message: string) {
|
||||
// if (getTimeNow() - this._lastLogTimestamp > 1000 / this._logTickRate) {
|
||||
// if (getTimeNow() - this._lastLogTimestamp > 1000 / this._logSyncRate) {
|
||||
// logger.info(message);
|
||||
// this._lastLogTimestamp = getTimeNow();
|
||||
// }
|
||||
@@ -415,7 +413,10 @@ namespace gdjs {
|
||||
|
||||
// For destruction of objects, we allow the host to destroy the object even if it is not the owner.
|
||||
// This is particularly helpful when a player disconnects, so the host can destroy the object they were owning.
|
||||
if (!this._isOwnerAsPlayerOrHost() && !gdjs.multiplayer.isPlayerHost()) {
|
||||
if (
|
||||
!this._isOwnerAsPlayerOrHost() &&
|
||||
!gdjs.multiplayer.isCurrentPlayerHost()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,82 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Multiplayer');
|
||||
|
||||
type LobbyChangeHostRequest = {
|
||||
lobbyId: string;
|
||||
gameId: string;
|
||||
peerId: string;
|
||||
playerId: string;
|
||||
ping: number;
|
||||
createdAt: number;
|
||||
ttl: number;
|
||||
newLobbyId?: string;
|
||||
newHostPeerId?: string;
|
||||
newPlayers?: {
|
||||
playerNumber: number;
|
||||
playerId: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const getTimeNow =
|
||||
window.performance && typeof window.performance.now === 'function'
|
||||
? window.performance.now.bind(window.performance)
|
||||
: Date.now;
|
||||
|
||||
const fetchAsPlayer = async ({
|
||||
relativeUrl,
|
||||
method,
|
||||
body,
|
||||
dev,
|
||||
}: {
|
||||
relativeUrl: string;
|
||||
method: 'GET' | 'POST';
|
||||
body?: string;
|
||||
dev: boolean;
|
||||
}) => {
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
if (!playerId || !playerToken) {
|
||||
logger.warn('Cannot fetch as a player if the player is not connected.');
|
||||
throw new Error(
|
||||
'Cannot fetch as a player if the player is not connected.'
|
||||
);
|
||||
}
|
||||
|
||||
const rootApi = dev
|
||||
? 'https://api-dev.gdevelop.io'
|
||||
: 'https://api.gdevelop.io';
|
||||
const url = new URL(`${rootApi}${relativeUrl}`);
|
||||
url.searchParams.set('playerId', playerId);
|
||||
const formattedUrl = url.toString();
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `player-game-token ${playerToken}`,
|
||||
};
|
||||
const response = await fetch(formattedUrl, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Error while fetching as a player: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
// Response can either be 'OK' or a JSON object. Get the content before trying to parse it.
|
||||
const responseText = await response.text();
|
||||
if (responseText === 'OK') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(responseText);
|
||||
} catch (error) {
|
||||
throw new Error(`Error while parsing the response: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
export namespace multiplayer {
|
||||
/** Set to true in testing to avoid relying on the multiplayer extension. */
|
||||
export let disableMultiplayerForTesting = false;
|
||||
@@ -17,20 +93,38 @@ namespace gdjs {
|
||||
let _lobbyId: string | null = null;
|
||||
let _connectionId: string | null = null;
|
||||
|
||||
let _shouldEndLobbyWhenHostLeaves = false;
|
||||
let _lobbyChangeHostRequest: LobbyChangeHostRequest | null = null;
|
||||
let _lobbyChangeHostRequestInitiatedAt: number | null = null;
|
||||
let _isChangingHost = false;
|
||||
let _lobbyNewHostPickedAt: number | null = null;
|
||||
|
||||
// Communication methods.
|
||||
let _lobbiesMessageCallback: ((event: MessageEvent) => void) | null = null;
|
||||
let _websocket: WebSocket | null = null;
|
||||
let _websocketHeartbeatInterval: NodeJS.Timeout | null = null;
|
||||
let _lobbyHeartbeatInterval: NodeJS.Timeout | null = null;
|
||||
let _websocketHeartbeatIntervalFunction: NodeJS.Timeout | null = null;
|
||||
let _lobbyHeartbeatIntervalFunction: NodeJS.Timeout | null = null;
|
||||
|
||||
const DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL = 10000;
|
||||
const DEFAULT_LOBBY_HEARTBEAT_INTERVAL = 30000;
|
||||
const DEFAULT_COUNTDOWN_SECONDS_TO_START = 5;
|
||||
let currentLobbyHeartbeatInterval = DEFAULT_LOBBY_HEARTBEAT_INTERVAL;
|
||||
const DEFAULT_LOBBY_CHANGE_HOST_REQUEST_CHECK_INTERVAL = 1000;
|
||||
// 10 seconds to be safe, but the backend will answer in less.
|
||||
const DEFAULT_LOBBY_CHANGE_HOST_REQUEST_TIMEOUT = 10000;
|
||||
const DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_CHECK_INTERVAL = 1000;
|
||||
const DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_TIMEOUT = 10000;
|
||||
let _resumeTimeout: NodeJS.Timeout | null = null;
|
||||
const DEFAULT_LOBBY_EXPECTED_RESUME_TIMEOUT = 12000;
|
||||
|
||||
export const DEFAULT_OBJECT_MAX_SYNC_RATE = 30;
|
||||
// The number of times per second an object should be synchronized if it keeps changing.
|
||||
export let _objectMaxSyncRate = DEFAULT_OBJECT_MAX_SYNC_RATE;
|
||||
|
||||
// Save if we are on dev environment so we don't need to use the runtimeGame every time.
|
||||
let isUsingGDevelopDevelopmentEnvironment = false;
|
||||
|
||||
export let playerNumber: number | null = null;
|
||||
export let hostPeerId: string | null = null;
|
||||
|
||||
gdjs.registerRuntimeScenePreEventsCallback(
|
||||
(runtimeScene: gdjs.RuntimeScene) => {
|
||||
@@ -88,6 +182,11 @@ namespace gdjs {
|
||||
// Then look at the heartbeats received to know if a new player has joined/left.
|
||||
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
|
||||
|
||||
gdjs.multiplayerMessageManager.handleEndGameMessagesReceived();
|
||||
gdjs.multiplayerMessageManager.handleResumeGameMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
|
||||
gdjs.multiplayerMessageManager.handleDestroyInstanceMessagesReceived(
|
||||
runtimeScene
|
||||
);
|
||||
@@ -161,6 +260,19 @@ namespace gdjs {
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const setObjectsSynchronizationRate = (rate: number) => {
|
||||
if (rate < 1 || rate > 60) {
|
||||
logger.warn(
|
||||
`Invalid rate ${rate} for object synchronization. Defaulting to ${DEFAULT_OBJECT_MAX_SYNC_RATE}.`
|
||||
);
|
||||
_objectMaxSyncRate = DEFAULT_OBJECT_MAX_SYNC_RATE;
|
||||
} else {
|
||||
_objectMaxSyncRate = rate;
|
||||
}
|
||||
};
|
||||
|
||||
export const getObjectsSynchronizationRate = () => _objectMaxSyncRate;
|
||||
|
||||
/**
|
||||
* Returns true if the game has just started,
|
||||
* useful to switch to the game scene.
|
||||
@@ -181,7 +293,7 @@ namespace gdjs {
|
||||
/**
|
||||
* Returns the number of players in the lobby.
|
||||
*/
|
||||
export const getPlayersInLobbyCount = () => {
|
||||
export const getPlayersInLobbyCount = (): number => {
|
||||
// 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();
|
||||
@@ -190,7 +302,7 @@ namespace gdjs {
|
||||
/**
|
||||
* Returns true if the player at this position is connected to the lobby.
|
||||
*/
|
||||
export const isPlayerConnected = (playerNumber: number) => {
|
||||
export const isPlayerConnected = (playerNumber: number): boolean => {
|
||||
return gdjs.multiplayerMessageManager.isPlayerConnected(playerNumber);
|
||||
};
|
||||
|
||||
@@ -199,29 +311,52 @@ namespace gdjs {
|
||||
* Return 0 if the player is not in the lobby.
|
||||
* Returns 1, 2, 3, ... if the player is in the lobby.
|
||||
*/
|
||||
export const getCurrentPlayerNumber = () => {
|
||||
export const getCurrentPlayerNumber = (): number => {
|
||||
return playerNumber || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the player is the host in the lobby. Here, player 1.
|
||||
* Returns true if the player is the host in the lobby.
|
||||
* This can change during the game.
|
||||
*/
|
||||
export const isPlayerHost = () => {
|
||||
return playerNumber === 1;
|
||||
export const isCurrentPlayerHost = (): boolean => {
|
||||
return (
|
||||
!!hostPeerId &&
|
||||
hostPeerId === gdjs.multiplayerPeerJsHelper.getCurrentId()
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the host left and the game is either:
|
||||
* - picking a new host
|
||||
* - waiting for everyone to connect to the new host
|
||||
*/
|
||||
export const isMigratingHost = (): boolean => {
|
||||
return !!_isChangingHost;
|
||||
};
|
||||
|
||||
/**
|
||||
* If this is set, instead of migrating the host, the lobby will end when the host leaves.
|
||||
*/
|
||||
export const endLobbyWhenHostLeaves = (enable: boolean) => {
|
||||
_shouldEndLobbyWhenHostLeaves = enable;
|
||||
};
|
||||
|
||||
export const shouldEndLobbyWhenHostLeaves = () =>
|
||||
_shouldEndLobbyWhenHostLeaves;
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
export const getPlayerUsername = (playerNumber: number): string => {
|
||||
return gdjs.multiplayerMessageManager.getPlayerUsername(playerNumber);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the player username of the current player in the lobby.
|
||||
*/
|
||||
export const getCurrentPlayerUsername = () => {
|
||||
export const getCurrentPlayerUsername = (): string => {
|
||||
const currentPlayerNumber = getCurrentPlayerNumber();
|
||||
return getPlayerUsername(currentPlayerNumber);
|
||||
};
|
||||
@@ -241,7 +376,12 @@ namespace gdjs {
|
||||
|
||||
// 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();
|
||||
if (
|
||||
isCurrentPlayerHost() &&
|
||||
isReadyToSendOrReceiveGameUpdateMessages()
|
||||
) {
|
||||
sendHeartbeatToBackend();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -253,6 +393,15 @@ namespace gdjs {
|
||||
runtimeScene,
|
||||
playerUsername
|
||||
);
|
||||
|
||||
// We also send a heartbeat to the backend right away, 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.
|
||||
if (
|
||||
isCurrentPlayerHost() &&
|
||||
isReadyToSendOrReceiveGameUpdateMessages()
|
||||
) {
|
||||
sendHeartbeatToBackend();
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
@@ -310,6 +459,7 @@ namespace gdjs {
|
||||
_websocket.close();
|
||||
_connectionId = null;
|
||||
playerNumber = null;
|
||||
hostPeerId = null;
|
||||
_lobbyId = null;
|
||||
_websocket = null;
|
||||
}
|
||||
@@ -339,7 +489,7 @@ namespace gdjs {
|
||||
_websocket.onopen = () => {
|
||||
logger.info('Connected to the lobby.');
|
||||
// Register a heartbeat to keep the connection alive.
|
||||
_websocketHeartbeatInterval = setInterval(() => {
|
||||
_websocketHeartbeatIntervalFunction = setInterval(() => {
|
||||
if (_websocket) {
|
||||
_websocket.send(
|
||||
JSON.stringify({
|
||||
@@ -413,23 +563,21 @@ namespace gdjs {
|
||||
case 'gameCountdownStarted': {
|
||||
const messageData = messageContent.data;
|
||||
const compressionMethod = messageData.compressionMethod || 'none';
|
||||
const secondsToStart =
|
||||
messageData.secondsToStart ||
|
||||
DEFAULT_COUNTDOWN_SECONDS_TO_START;
|
||||
handleGameCountdownStartedEvent({
|
||||
runtimeScene,
|
||||
compressionMethod,
|
||||
secondsToStart,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'gameStarted': {
|
||||
const messageData = messageContent.data;
|
||||
const heartbeatInterval =
|
||||
currentLobbyHeartbeatInterval =
|
||||
messageData.heartbeatInterval ||
|
||||
DEFAULT_LOBBY_HEARTBEAT_INTERVAL;
|
||||
|
||||
handleGameStartedEvent({ runtimeScene, heartbeatInterval });
|
||||
handleGameStartedEvent({
|
||||
runtimeScene,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'peerId': {
|
||||
@@ -452,14 +600,14 @@ namespace gdjs {
|
||||
}
|
||||
};
|
||||
_websocket.onclose = () => {
|
||||
logger.info(
|
||||
'Disconnected from the lobby. Either manually or game started.'
|
||||
);
|
||||
if (!_isLobbyGameRunning) {
|
||||
logger.info('Disconnected from the lobby.');
|
||||
}
|
||||
|
||||
_connectionId = null;
|
||||
_websocket = null;
|
||||
if (_websocketHeartbeatInterval) {
|
||||
clearInterval(_websocketHeartbeatInterval);
|
||||
if (_websocketHeartbeatIntervalFunction) {
|
||||
clearInterval(_websocketHeartbeatIntervalFunction);
|
||||
}
|
||||
|
||||
// If the game is running, then all good.
|
||||
@@ -576,6 +724,7 @@ namespace gdjs {
|
||||
}
|
||||
_connectionId = null;
|
||||
playerNumber = null;
|
||||
hostPeerId = null;
|
||||
_lobbyId = null;
|
||||
_websocket = null;
|
||||
};
|
||||
@@ -613,15 +762,14 @@ namespace gdjs {
|
||||
const handleGameCountdownStartedEvent = function ({
|
||||
runtimeScene,
|
||||
compressionMethod,
|
||||
secondsToStart,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;
|
||||
secondsToStart: number;
|
||||
}) {
|
||||
gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);
|
||||
|
||||
// When the countdown starts, if we are player number 1, then send the peerId to others so they can connect via P2P.
|
||||
// When the countdown starts, if we are player number 1, we are chosen as the host.
|
||||
// We then send the peerId to others so they can connect via P2P.
|
||||
if (getCurrentPlayerNumber() === 1) {
|
||||
sendPeerId();
|
||||
}
|
||||
@@ -639,7 +787,6 @@ namespace gdjs {
|
||||
lobbiesIframe.contentWindow.postMessage(
|
||||
{
|
||||
id: 'gameCountdownStarted',
|
||||
secondsToStart,
|
||||
},
|
||||
'*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.
|
||||
);
|
||||
@@ -652,43 +799,34 @@ 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) {
|
||||
if (!gameId || !_lobbyId) {
|
||||
logger.error(
|
||||
'Cannot keep the lobby playing without the game ID or player ID.'
|
||||
'Cannot keep the lobby playing without the game ID or lobby 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 heartbeatRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/action/heartbeat`;
|
||||
const players = gdjs.multiplayerMessageManager.getConnectedPlayers();
|
||||
try {
|
||||
await fetch(heartbeatUrl, {
|
||||
await fetchAsPlayer({
|
||||
relativeUrl: heartbeatRelativeUrl,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
players,
|
||||
}),
|
||||
dev: isUsingGDevelopDevelopmentEnvironment,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error while sending heartbeat, retrying:', error);
|
||||
try {
|
||||
await fetch(heartbeatUrl, {
|
||||
await fetchAsPlayer({
|
||||
relativeUrl: heartbeatRelativeUrl,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
players,
|
||||
}),
|
||||
dev: isUsingGDevelopDevelopmentEnvironment,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -705,16 +843,14 @@ namespace gdjs {
|
||||
*/
|
||||
const handleGameStartedEvent = function ({
|
||||
runtimeScene,
|
||||
heartbeatInterval,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
heartbeatInterval: number;
|
||||
}) {
|
||||
// It is possible the connection to other players didn't work.
|
||||
// If that's the case, show an error message and leave the lobby.
|
||||
// If we are the host, still start the game, as this allows a player to test the game alone.
|
||||
const allConnectedPeers = gdjs.multiplayerPeerJsHelper.getAllPeers();
|
||||
if (!isPlayerHost() && allConnectedPeers.length === 0) {
|
||||
if (!isCurrentPlayerHost() && allConnectedPeers.length === 0) {
|
||||
gdjs.multiplayerComponents.displayConnectionErrorNotification(
|
||||
runtimeScene
|
||||
);
|
||||
@@ -726,10 +862,10 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// If we are the host, start pinging the backend to let it know the lobby is running.
|
||||
if (isPlayerHost()) {
|
||||
_lobbyHeartbeatInterval = setInterval(async () => {
|
||||
if (isCurrentPlayerHost()) {
|
||||
_lobbyHeartbeatIntervalFunction = setInterval(async () => {
|
||||
await sendHeartbeatToBackend();
|
||||
}, heartbeatInterval);
|
||||
}, currentLobbyHeartbeatInterval);
|
||||
}
|
||||
|
||||
// If we are connected to players, then the game can start.
|
||||
@@ -757,9 +893,11 @@ namespace gdjs {
|
||||
_isLobbyGameRunning = false;
|
||||
_lobbyId = null;
|
||||
playerNumber = null;
|
||||
hostPeerId = null;
|
||||
_isReadyToSendOrReceiveGameUpdateMessages = false;
|
||||
if (_lobbyHeartbeatInterval) {
|
||||
clearInterval(_lobbyHeartbeatInterval);
|
||||
if (_lobbyHeartbeatIntervalFunction) {
|
||||
clearInterval(_lobbyHeartbeatIntervalFunction);
|
||||
_lobbyHeartbeatIntervalFunction = null;
|
||||
}
|
||||
|
||||
// Disconnect from any P2P connections.
|
||||
@@ -795,6 +933,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
hostPeerId = peerId;
|
||||
gdjs.multiplayerPeerJsHelper.connect(peerId);
|
||||
};
|
||||
|
||||
@@ -878,10 +1017,315 @@ namespace gdjs {
|
||||
action: 'updateConnection',
|
||||
connectionType: 'lobby',
|
||||
status: 'connected',
|
||||
peerId: gdjs.multiplayerPeerJsHelper.getCurrentId(),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const clearChangeHostRequestData = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
_lobbyChangeHostRequest = null;
|
||||
_lobbyChangeHostRequestInitiatedAt = null;
|
||||
_lobbyNewHostPickedAt = null;
|
||||
if (_resumeTimeout) {
|
||||
clearTimeout(_resumeTimeout);
|
||||
_resumeTimeout = null;
|
||||
}
|
||||
_isChangingHost = false;
|
||||
if (hostPeerId) {
|
||||
gdjs.multiplayerComponents.showHostMigrationFinishedNotification(
|
||||
runtimeScene
|
||||
);
|
||||
} else {
|
||||
gdjs.multiplayerComponents.showHostMigrationFailedNotification(
|
||||
runtimeScene
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const resumeGame = async function (runtimeScene: gdjs.RuntimeScene) {
|
||||
if (isCurrentPlayerHost()) {
|
||||
// Send message to other players to indicate the game is resuming.
|
||||
gdjs.multiplayerMessageManager.sendResumeGameMessage();
|
||||
|
||||
// Start sending heartbeats to the backend.
|
||||
await sendHeartbeatToBackend();
|
||||
_lobbyHeartbeatIntervalFunction = setInterval(async () => {
|
||||
await sendHeartbeatToBackend();
|
||||
}, currentLobbyHeartbeatInterval);
|
||||
}
|
||||
|
||||
// Migration is finished.
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
};
|
||||
|
||||
/**
|
||||
* When a host is being changed, multiple cases can happen:
|
||||
* - We are the new host and the only one in the lobby. Unpause the game right away.
|
||||
* - We are the new host and there are other players in the new lobby. Wait for them to connect:
|
||||
* - if they are all connected, unpause the game.
|
||||
* - if we reach a timeout, a player may have disconnected at the same time, unpause the game.
|
||||
* - We are not the new host. Connect to the new host peerId.
|
||||
* - If we cannot connect, leave the lobby.
|
||||
* - when we receive a message to unpause the game, unpause it.
|
||||
* - if we reach a timeout without the message, leave the lobby, something wrong happened.
|
||||
*/
|
||||
const checkHostChangeRequestRegularly = async function ({
|
||||
runtimeScene,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
}) {
|
||||
if (!_lobbyChangeHostRequest || !_lobbyChangeHostRequestInitiatedAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh the request to get the latest information.
|
||||
try {
|
||||
const changeHostRelativeUrl = `/play/game/${
|
||||
_lobbyChangeHostRequest.gameId
|
||||
}/public-lobby/${
|
||||
_lobbyChangeHostRequest.lobbyId
|
||||
}/lobby-change-host-request?peerId=${gdjs.multiplayerPeerJsHelper.getCurrentId()}`;
|
||||
|
||||
const lobbyChangeHostRequest = await fetchAsPlayer({
|
||||
relativeUrl: changeHostRelativeUrl,
|
||||
method: 'GET',
|
||||
dev: isUsingGDevelopDevelopmentEnvironment,
|
||||
});
|
||||
_lobbyChangeHostRequest = lobbyChangeHostRequest;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error while trying to retrieve the lobby change host request:',
|
||||
error
|
||||
);
|
||||
handleLobbyGameEnded();
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_lobbyChangeHostRequest) {
|
||||
throw new Error('No lobby change host request received.');
|
||||
}
|
||||
|
||||
const newHostPeerId = _lobbyChangeHostRequest.newHostPeerId;
|
||||
if (!newHostPeerId) {
|
||||
logger.info('No new host picked yet.');
|
||||
if (
|
||||
getTimeNow() - _lobbyChangeHostRequestInitiatedAt >
|
||||
DEFAULT_LOBBY_CHANGE_HOST_REQUEST_TIMEOUT
|
||||
) {
|
||||
logger.error(
|
||||
'Timeout while waiting for the lobby host change. Giving up.'
|
||||
);
|
||||
handleLobbyGameEnded();
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Retrying...');
|
||||
setTimeout(() => {
|
||||
checkHostChangeRequestRegularly({ runtimeScene });
|
||||
}, DEFAULT_LOBBY_CHANGE_HOST_REQUEST_CHECK_INTERVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newLobbyId = _lobbyChangeHostRequest.newLobbyId;
|
||||
const newPlayers = _lobbyChangeHostRequest.newPlayers;
|
||||
if (!newLobbyId || !newPlayers) {
|
||||
logger.error(
|
||||
'Change host request is incomplete. Cannot change host.'
|
||||
);
|
||||
handleLobbyGameEnded();
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
return;
|
||||
}
|
||||
hostPeerId = newHostPeerId;
|
||||
_lobbyNewHostPickedAt = getTimeNow();
|
||||
_lobbyId = newLobbyId;
|
||||
|
||||
if (newHostPeerId === gdjs.multiplayerPeerJsHelper.getCurrentId()) {
|
||||
logger.info(
|
||||
`We are the new host. Switching to lobby ${newLobbyId} and awaiting for ${
|
||||
newPlayers.length - 1
|
||||
} player(s) to connect.`
|
||||
);
|
||||
await checkExpectedConnectedPlayersRegularly({
|
||||
runtimeScene,
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`Connecting to new host and switching lobby to ${newLobbyId}.`
|
||||
);
|
||||
gdjs.multiplayerPeerJsHelper.connect(newHostPeerId);
|
||||
_resumeTimeout = setTimeout(() => {
|
||||
logger.error(
|
||||
'Timeout while waiting for the game to resume. Leaving the lobby.'
|
||||
);
|
||||
handleLobbyGameEnded();
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
}, DEFAULT_LOBBY_EXPECTED_RESUME_TIMEOUT);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error while trying to change host:', error);
|
||||
handleLobbyGameEnded();
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper for the new host, to check if they have all the expected players connected.
|
||||
*/
|
||||
const checkExpectedConnectedPlayersRegularly = async function ({
|
||||
runtimeScene,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
}) {
|
||||
if (!_lobbyChangeHostRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedNewPlayers = _lobbyChangeHostRequest.newPlayers;
|
||||
if (!expectedNewPlayers) {
|
||||
logger.error('No expected players in the lobby change host request.');
|
||||
handleLobbyGameEnded();
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
return;
|
||||
}
|
||||
const expectedNewOtherPlayerNumbers = expectedNewPlayers.map(
|
||||
(player) => player.playerNumber
|
||||
);
|
||||
|
||||
// First look for players who left during the migration.
|
||||
const playerNumbersConnectedBeforeMigration = gdjs.multiplayerMessageManager
|
||||
.getConnectedPlayers()
|
||||
.map((player) => player.playerNumber);
|
||||
const playerNumbersWhoLeftDuringMigration = playerNumbersConnectedBeforeMigration.filter(
|
||||
(playerNumberBeforeMigration) =>
|
||||
!expectedNewOtherPlayerNumbers.includes(playerNumberBeforeMigration)
|
||||
);
|
||||
playerNumbersWhoLeftDuringMigration.map((playerNumberWhoLeft) => {
|
||||
logger.info(
|
||||
`Player ${playerNumberWhoLeft} left during the host migration. Marking as disconnected.`
|
||||
);
|
||||
gdjs.multiplayerMessageManager.markPlayerAsDisconnected({
|
||||
runtimeScene,
|
||||
playerNumber: playerNumberWhoLeft,
|
||||
});
|
||||
});
|
||||
|
||||
// Then check if all expected players are connected.
|
||||
const playerNumbersWhoDidNotConnect = expectedNewOtherPlayerNumbers.filter(
|
||||
(otherPlayerNumber) =>
|
||||
otherPlayerNumber !== playerNumber && // We don't look for ourselves
|
||||
!gdjs.multiplayerMessageManager.hasReceivedHeartbeatFromPlayer(
|
||||
otherPlayerNumber
|
||||
)
|
||||
);
|
||||
|
||||
if (playerNumbersWhoDidNotConnect.length === 0) {
|
||||
logger.info('All expected players are connected. Resuming the game.');
|
||||
await resumeGame(runtimeScene);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
_lobbyNewHostPickedAt &&
|
||||
getTimeNow() - _lobbyNewHostPickedAt >
|
||||
DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_TIMEOUT &&
|
||||
playerNumbersWhoDidNotConnect.length > 0
|
||||
) {
|
||||
logger.error(
|
||||
`Timeout while waiting for players ${playerNumbersWhoDidNotConnect.join(
|
||||
', '
|
||||
)} to connect. Assume they disconnected.`
|
||||
);
|
||||
playerNumbersWhoDidNotConnect.map((missingPlayerNumber) => {
|
||||
gdjs.multiplayerMessageManager.markPlayerAsDisconnected({
|
||||
runtimeScene,
|
||||
playerNumber: missingPlayerNumber,
|
||||
});
|
||||
});
|
||||
await resumeGame(runtimeScene);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
checkExpectedConnectedPlayersRegularly({
|
||||
runtimeScene,
|
||||
});
|
||||
}, DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_CHECK_INTERVAL);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the host disconnects, we inform the backend we lost the connection and we need a new lobby/host.
|
||||
*/
|
||||
export const handleHostDisconnected = async function ({
|
||||
runtimeScene,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
}) {
|
||||
if (!_isLobbyGameRunning) {
|
||||
// This can happen when the game ends. Nothing to do here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lobbyChangeHostRequest) {
|
||||
// The new host disconnected while we are already changing host.
|
||||
// Let's end the lobby game to avoid weird situations.
|
||||
handleLobbyGameEnded();
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
}
|
||||
|
||||
const gameId = gdjs.projectData.properties.projectUuid;
|
||||
|
||||
if (!gameId || !_lobbyId) {
|
||||
logger.error(
|
||||
'Cannot ask for a host change without the game ID or lobby ID.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_isChangingHost = true;
|
||||
gdjs.multiplayerComponents.displayHostMigrationNotification(
|
||||
runtimeScene
|
||||
);
|
||||
|
||||
const changeHostRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/lobby-change-host-request`;
|
||||
const playersInfo = gdjs.multiplayerMessageManager.getPlayersInfo();
|
||||
const playersInfoForHostChange = Object.keys(playersInfo).map(
|
||||
(playerNumber) => {
|
||||
return {
|
||||
playerNumber: parseInt(playerNumber, 10),
|
||||
playerId: playersInfo[playerNumber].playerId,
|
||||
ping: playersInfo[playerNumber].ping,
|
||||
};
|
||||
}
|
||||
);
|
||||
const body = JSON.stringify({
|
||||
playersInfo: playersInfoForHostChange,
|
||||
peerId: gdjs.multiplayerPeerJsHelper.getCurrentId(),
|
||||
});
|
||||
const lobbyChangeHostRequest = await fetchAsPlayer({
|
||||
relativeUrl: changeHostRelativeUrl,
|
||||
method: 'POST',
|
||||
body,
|
||||
dev: isUsingGDevelopDevelopmentEnvironment,
|
||||
});
|
||||
|
||||
_lobbyChangeHostRequest = lobbyChangeHostRequest;
|
||||
_lobbyChangeHostRequestInitiatedAt = getTimeNow();
|
||||
|
||||
await checkHostChangeRequestRegularly({ runtimeScene });
|
||||
} catch (error) {
|
||||
logger.error('Error while trying to change host:', error);
|
||||
handleLobbyGameEnded();
|
||||
clearChangeHostRequestData(runtimeScene);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to end the lobby game.
|
||||
* This will update the lobby status and inform everyone in the lobby that the game has ended.
|
||||
@@ -891,7 +1335,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPlayerHost()) {
|
||||
if (!isCurrentPlayerHost()) {
|
||||
logger.error('Only the host can end the game.');
|
||||
return;
|
||||
}
|
||||
@@ -906,31 +1350,18 @@ namespace gdjs {
|
||||
|
||||
// Also call backend to end the game.
|
||||
const gameId = gdjs.projectData.properties.projectUuid;
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
|
||||
if (!gameId || !playerId || !playerToken || !_lobbyId) {
|
||||
logger.error('Cannot end the lobby without the game ID or player ID.');
|
||||
if (!gameId || !_lobbyId) {
|
||||
logger.error('Cannot end the lobby without the game ID or lobby ID.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rootApi = isUsingGDevelopDevelopmentEnvironment
|
||||
? 'https://api-dev.gdevelop.io'
|
||||
: 'https://api.gdevelop.io';
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
let endGameUrl = `${rootApi}/play/game/${gameId}/public-lobby/${_lobbyId}/action/end`;
|
||||
headers['Authorization'] = `player-game-token ${playerToken}`;
|
||||
endGameUrl += `?playerId=${playerId}`;
|
||||
const endGameRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/action/end`;
|
||||
try {
|
||||
await fetch(endGameUrl, {
|
||||
await fetchAsPlayer({
|
||||
relativeUrl: endGameRelativeUrl,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
gameId,
|
||||
lobbyId: _lobbyId,
|
||||
}),
|
||||
body: JSON.stringify({}),
|
||||
dev: isUsingGDevelopDevelopmentEnvironment,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error while ending the game:', error);
|
||||
@@ -966,6 +1397,8 @@ namespace gdjs {
|
||||
peerId,
|
||||
})
|
||||
);
|
||||
// We are the host.
|
||||
hostPeerId = peerId;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -313,9 +313,14 @@ describe('Multiplayer', () => {
|
||||
/**
|
||||
* Helper to fast forward a bit of time in players games, so that heartbeats
|
||||
* are sent and all players are aware of each other.
|
||||
* @param {{ playerNumber: number, peerId: string}[]} players
|
||||
* @param {{ playerNumber: number, peerId: string, isHost?: boolean }[]} players
|
||||
*/
|
||||
const initiateGameWithPlayers = (players) => {
|
||||
// Find the host.
|
||||
const host = players.find((player) => player.isHost);
|
||||
if (!host)
|
||||
throw new Error('No host defined in players, cannot initiate game.');
|
||||
|
||||
// Create the instances of the MultiplayerMessageManager and MultiplayerVariablesManager
|
||||
// for each player.
|
||||
for (const player of players) {
|
||||
@@ -325,6 +330,9 @@ describe('Multiplayer', () => {
|
||||
peerMultiplayerVariablesManager[
|
||||
player.peerId
|
||||
] = gdjs.makeMultiplayerVariablesManager();
|
||||
|
||||
// Define the host for everyone.
|
||||
gdjs.multiplayer.hostPeerId = host.peerId;
|
||||
}
|
||||
|
||||
// Use a scene to simulate the game loop moving forward.
|
||||
@@ -395,12 +403,16 @@ describe('Multiplayer', () => {
|
||||
gdjs.multiplayer.disableMultiplayerForTesting = false;
|
||||
gdjs.multiplayer._isLobbyGameRunning = true;
|
||||
gdjs.multiplayer._isReadyToSendOrReceiveGameUpdateMessages = true;
|
||||
// Sync as fast as possible for tests.
|
||||
gdjs.multiplayer._objectMaxSyncRate = Infinity;
|
||||
});
|
||||
afterEach(() => {
|
||||
gdjs.multiplayerPeerJsHelper = _originalP2pIfAny;
|
||||
gdjs.multiplayer.disableMultiplayerForTesting = true;
|
||||
gdjs.multiplayer._isLobbyGameRunning = false;
|
||||
gdjs.multiplayer._isReadyToSendOrReceiveGameUpdateMessages = false;
|
||||
gdjs.multiplayer._objectMaxSyncRate =
|
||||
gdjs.multiplayer.DEFAULT_OBJECT_MAX_SYNC_RATE;
|
||||
});
|
||||
|
||||
describe('Single scene tests', () => {
|
||||
@@ -411,7 +423,7 @@ describe('Multiplayer', () => {
|
||||
initiateGameWithPlayers,
|
||||
} = createMultiplayerManagersMock();
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -607,7 +619,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -693,7 +705,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
@@ -830,7 +842,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
@@ -1026,7 +1038,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -1081,13 +1093,11 @@ describe('Multiplayer', () => {
|
||||
|
||||
const {
|
||||
object: p1SpriteObject,
|
||||
behavior: p1SpriteObjectBehavior,
|
||||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||||
p1RuntimeScene,
|
||||
'MySpriteObject'
|
||||
)[0];
|
||||
|
||||
p1SpriteObjectBehavior._objectMaxTickRate = Infinity;
|
||||
p1SpriteObject.setX(242);
|
||||
p1SpriteObject.setY(243);
|
||||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||||
@@ -1155,7 +1165,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
@@ -1237,13 +1247,11 @@ describe('Multiplayer', () => {
|
||||
|
||||
const {
|
||||
object: p2SpriteObject,
|
||||
behavior: p2SpriteObjectBehavior,
|
||||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||||
p2RuntimeScene,
|
||||
'MySpriteObject'
|
||||
)[0];
|
||||
|
||||
p2SpriteObjectBehavior._objectMaxTickRate = Infinity;
|
||||
p2SpriteObject.setX(242);
|
||||
p2SpriteObject.setY(243);
|
||||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||||
@@ -1358,7 +1366,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
@@ -1630,7 +1638,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
@@ -1840,7 +1848,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
@@ -1899,7 +1907,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -1941,7 +1949,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
@@ -2140,12 +2148,10 @@ describe('Multiplayer', () => {
|
||||
|
||||
const {
|
||||
object: p2SpriteObject,
|
||||
behavior: p2SpriteMultiplayerObjectBehavior,
|
||||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||||
p2RuntimeScene,
|
||||
'MySpriteObject'
|
||||
)[0];
|
||||
p2SpriteMultiplayerObjectBehavior._objectMaxTickRate = Infinity;
|
||||
p2SpriteObject.setX(242);
|
||||
p2SpriteObject.setY(243);
|
||||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||||
@@ -2202,7 +2208,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -2365,7 +2371,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -2450,7 +2456,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -2582,7 +2588,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -2676,7 +2682,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
@@ -2685,7 +2691,7 @@ describe('Multiplayer', () => {
|
||||
|
||||
// Player 2 leaves.
|
||||
const newConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
// Host sees the player 2 leaving.
|
||||
@@ -2712,7 +2718,7 @@ describe('Multiplayer', () => {
|
||||
} = createMultiplayerManagersMock();
|
||||
|
||||
const allConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
initiateGameWithPlayers(allConnectedPlayers);
|
||||
@@ -2723,7 +2729,7 @@ describe('Multiplayer', () => {
|
||||
|
||||
// Player 2 joins.
|
||||
const newConnectedPlayers = [
|
||||
{ playerNumber: 1, peerId: 'player-1' },
|
||||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||||
{ playerNumber: 2, peerId: 'player-2' },
|
||||
{ playerNumber: 3, peerId: 'player-3' },
|
||||
];
|
||||
|
@@ -394,16 +394,15 @@ module.exports = {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (propertyName === 'scaleX') {
|
||||
if (propertyName === 'worldScale') {
|
||||
const newValueAsNumber = parseInt(newValue, 10);
|
||||
if (newValueAsNumber !== newValueAsNumber) return false;
|
||||
if (!sharedContent.hasChild('worldScale')) {
|
||||
sharedContent.addChild('worldScale');
|
||||
}
|
||||
sharedContent.getChild('worldScale').setDoubleValue(newValueAsNumber);
|
||||
// Set deprecated properties for compatibility with 5.4.209-
|
||||
sharedContent.getChild('scaleX').setDoubleValue(newValueAsNumber);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (propertyName === 'scaleY') {
|
||||
const newValueAsNumber = parseInt(newValue, 10);
|
||||
if (newValueAsNumber !== newValueAsNumber) return false;
|
||||
sharedContent.getChild('scaleY').setDoubleValue(newValueAsNumber);
|
||||
return true;
|
||||
}
|
||||
@@ -427,16 +426,22 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getNewton());
|
||||
|
||||
if (!sharedContent.hasChild('worldScale')) {
|
||||
sharedContent.addChild('worldScale');
|
||||
sharedContent
|
||||
.getChild('worldScale')
|
||||
.setDoubleValue(
|
||||
Math.sqrt(
|
||||
sharedContent.getChild('scaleX').getDoubleValue() *
|
||||
sharedContent.getChild('scaleY').getDoubleValue()
|
||||
)
|
||||
);
|
||||
}
|
||||
sharedProperties
|
||||
.getOrCreate('scaleX')
|
||||
.getOrCreate('worldScale')
|
||||
.setValue(
|
||||
sharedContent.getChild('scaleX').getDoubleValue().toString(10)
|
||||
)
|
||||
.setType('Number');
|
||||
sharedProperties
|
||||
.getOrCreate('scaleY')
|
||||
.setValue(
|
||||
sharedContent.getChild('scaleY').getDoubleValue().toString(10)
|
||||
sharedContent.getChild('worldScale').getDoubleValue().toString(10)
|
||||
)
|
||||
.setType('Number');
|
||||
|
||||
@@ -445,6 +450,8 @@ module.exports = {
|
||||
sharedData.initializeContent = function (behaviorContent) {
|
||||
behaviorContent.addChild('gravityX').setDoubleValue(0);
|
||||
behaviorContent.addChild('gravityY').setDoubleValue(9.8);
|
||||
behaviorContent.addChild('worldScale').setDoubleValue(100);
|
||||
// Set deprecated properties for compatibility with 5.4.209-
|
||||
behaviorContent.addChild('scaleX').setDoubleValue(100);
|
||||
behaviorContent.addChild('scaleY').setDoubleValue(100);
|
||||
};
|
||||
@@ -472,6 +479,19 @@ module.exports = {
|
||||
);
|
||||
|
||||
// Global
|
||||
aut
|
||||
.addExpression(
|
||||
'WorldScale',
|
||||
_('World scale'),
|
||||
_('Return the world scale.'),
|
||||
_('Global'),
|
||||
'res/physics32.png'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('getWorldScale');
|
||||
|
||||
aut
|
||||
.addCondition(
|
||||
'GravityX',
|
||||
@@ -1757,7 +1777,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Angular impulse (N·m·s'))
|
||||
.addParameter('expression', _('Angular impulse (N·m·s)'))
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'An impulse is like a rotation speed addition but depends on the mass.'
|
||||
@@ -1784,7 +1804,7 @@ module.exports = {
|
||||
'Inertia',
|
||||
_('Inertia'),
|
||||
_(
|
||||
'Return the rotational inertia of the object (in kilograms * meters * meters)'
|
||||
'Return the rotational inertia of the object (in kilograms · meters²)'
|
||||
),
|
||||
'',
|
||||
'res/physics32.png'
|
||||
|
@@ -15,6 +15,8 @@ namespace gdjs {
|
||||
lvy: number | undefined;
|
||||
av: number | undefined;
|
||||
aw: boolean | undefined;
|
||||
layers: number;
|
||||
masks: number;
|
||||
}
|
||||
|
||||
export interface Physics2NetworkSyncData extends BehaviorNetworkSyncData {
|
||||
@@ -23,9 +25,15 @@ namespace gdjs {
|
||||
export class Physics2SharedData {
|
||||
gravityX: float;
|
||||
gravityY: float;
|
||||
worldScale: float;
|
||||
worldInvScale: float;
|
||||
/** @deprecated Use `worldScale` instead */
|
||||
scaleX: float;
|
||||
/** @deprecated Use `worldScale` instead */
|
||||
scaleY: float;
|
||||
/** @deprecated Use `worldInvScale` instead */
|
||||
invScaleX: float;
|
||||
/** @deprecated Use `worldInvScale` instead */
|
||||
invScaleY: float;
|
||||
timeStep: float;
|
||||
frameTime: float = 0;
|
||||
@@ -52,10 +60,13 @@ namespace gdjs {
|
||||
this._registeredBehaviors = new Set();
|
||||
this.gravityX = sharedData.gravityX;
|
||||
this.gravityY = sharedData.gravityY;
|
||||
this.scaleX = sharedData.scaleX === 0 ? 100 : sharedData.scaleX;
|
||||
this.scaleY = sharedData.scaleY === 0 ? 100 : sharedData.scaleY;
|
||||
this.scaleX = sharedData.scaleX || 100;
|
||||
this.scaleY = sharedData.scaleY || 100;
|
||||
this.invScaleX = 1 / this.scaleX;
|
||||
this.invScaleY = 1 / this.scaleY;
|
||||
this.worldScale =
|
||||
sharedData.worldScale || Math.sqrt(this.scaleX * this.scaleY);
|
||||
this.worldInvScale = 1 / this.worldScale;
|
||||
this.timeStep = 1 / 60;
|
||||
this.world = new Box2D.b2World(
|
||||
new Box2D.b2Vec2(this.gravityX, this.gravityY)
|
||||
@@ -513,6 +524,8 @@ namespace gdjs {
|
||||
...super.getNetworkSyncData(),
|
||||
props: {
|
||||
...bodyProps,
|
||||
layers: this.layers,
|
||||
masks: this.masks,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -556,6 +569,14 @@ namespace gdjs {
|
||||
this._body.SetAwake(behaviorSpecificProps.aw);
|
||||
}
|
||||
}
|
||||
|
||||
if (behaviorSpecificProps.layers !== undefined) {
|
||||
this.layers = behaviorSpecificProps.layers;
|
||||
}
|
||||
|
||||
if (behaviorSpecificProps.masks !== undefined) {
|
||||
this.masks = behaviorSpecificProps.masks;
|
||||
}
|
||||
}
|
||||
|
||||
onDeActivate() {
|
||||
@@ -642,10 +663,10 @@ namespace gdjs {
|
||||
createShape(): Box2D.b2FixtureDef {
|
||||
// Get the scaled offset
|
||||
const offsetX = this.shapeOffsetX
|
||||
? this.shapeOffsetX * this.shapeScale * this._sharedData.invScaleX
|
||||
? this.shapeOffsetX * this.shapeScale * this._sharedData.worldInvScale
|
||||
: 0;
|
||||
const offsetY = this.shapeOffsetY
|
||||
? this.shapeOffsetY * this.shapeScale * this._sharedData.invScaleY
|
||||
? this.shapeOffsetY * this.shapeScale * this._sharedData.worldInvScale
|
||||
: 0;
|
||||
|
||||
// Generate the base shape
|
||||
@@ -657,12 +678,14 @@ namespace gdjs {
|
||||
// Average radius from width and height
|
||||
if (this.shapeDimensionA > 0) {
|
||||
shape.set_m_radius(
|
||||
this.shapeDimensionA * this.shapeScale * this._sharedData.invScaleX
|
||||
this.shapeDimensionA *
|
||||
this.shapeScale *
|
||||
this._sharedData.worldInvScale
|
||||
);
|
||||
} else {
|
||||
const radius =
|
||||
(this.owner.getWidth() * this._sharedData.invScaleX +
|
||||
this.owner.getHeight() * this._sharedData.invScaleY) /
|
||||
(this.owner.getWidth() * this._sharedData.worldInvScale +
|
||||
this.owner.getHeight() * this._sharedData.worldInvScale) /
|
||||
4;
|
||||
shape.set_m_radius(radius > 0 ? radius : 1);
|
||||
}
|
||||
@@ -680,10 +703,10 @@ namespace gdjs {
|
||||
) {
|
||||
let width =
|
||||
(this.owner.getWidth() > 0 ? this.owner.getWidth() : 1) *
|
||||
this._sharedData.invScaleX;
|
||||
this._sharedData.worldInvScale;
|
||||
let height =
|
||||
(this.owner.getHeight() > 0 ? this.owner.getHeight() : 1) *
|
||||
this._sharedData.invScaleY;
|
||||
this._sharedData.worldInvScale;
|
||||
|
||||
// Set the shape box
|
||||
shape.SetAsBox(
|
||||
@@ -728,12 +751,12 @@ namespace gdjs {
|
||||
Box2D.HEAPF32[(this._verticesBuffer + offset) >> 2] =
|
||||
(this.polygon.vertices[i][0] * this.shapeScale +
|
||||
originOffsetX) *
|
||||
this._sharedData.invScaleX +
|
||||
this._sharedData.worldInvScale +
|
||||
offsetX;
|
||||
Box2D.HEAPF32[(this._verticesBuffer + (offset + 4)) >> 2] =
|
||||
(this.polygon.vertices[i][1] * this.shapeScale +
|
||||
originOffsetY) *
|
||||
this._sharedData.invScaleY +
|
||||
this._sharedData.worldInvScale +
|
||||
offsetY;
|
||||
offset += 8;
|
||||
}
|
||||
@@ -755,10 +778,10 @@ namespace gdjs {
|
||||
? this.shapeDimensionA * this.shapeScale
|
||||
: this.owner.getWidth() > 0
|
||||
? this.owner.getWidth()
|
||||
: 1) * this._sharedData.invScaleX;
|
||||
: 1) * this._sharedData.worldInvScale;
|
||||
let height =
|
||||
this.owner.getHeight() > 0
|
||||
? this.owner.getHeight() * this._sharedData.invScaleY
|
||||
? this.owner.getHeight() * this._sharedData.worldInvScale
|
||||
: 0;
|
||||
|
||||
// Angle from custom dimension, otherwise is 0
|
||||
@@ -787,13 +810,13 @@ namespace gdjs {
|
||||
? this.shapeDimensionA * this.shapeScale
|
||||
: this.owner.getWidth() > 0
|
||||
? this.owner.getWidth()
|
||||
: 1) * this._sharedData.invScaleX;
|
||||
: 1) * this._sharedData.worldInvScale;
|
||||
let height =
|
||||
(this.shapeDimensionB > 0
|
||||
? this.shapeDimensionB * this.shapeScale
|
||||
: this.owner.getHeight() > 0
|
||||
? this.owner.getHeight()
|
||||
: 1) * this._sharedData.invScaleY;
|
||||
: 1) * this._sharedData.worldInvScale;
|
||||
|
||||
// Set the shape box, the offset must be added here too
|
||||
shape.SetAsBox(
|
||||
@@ -878,9 +901,9 @@ namespace gdjs {
|
||||
bodyDef.set_position(
|
||||
this.b2Vec2(
|
||||
(this.owner.getDrawableX() + this.owner.getWidth() / 2) *
|
||||
this._sharedData.invScaleX,
|
||||
this._sharedData.worldInvScale,
|
||||
(this.owner.getDrawableY() + this.owner.getHeight() / 2) *
|
||||
this._sharedData.invScaleY
|
||||
this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
bodyDef.set_angle(gdjs.toRad(this.owner.getAngle()));
|
||||
@@ -934,13 +957,13 @@ namespace gdjs {
|
||||
// don't do anything (but still run the physics simulation - this is independent).
|
||||
if (this._body !== null) {
|
||||
this.owner.setX(
|
||||
this._body.GetPosition().get_x() * this._sharedData.scaleX -
|
||||
this._body.GetPosition().get_x() * this._sharedData.worldScale -
|
||||
this.owner.getWidth() / 2 +
|
||||
this.owner.getX() -
|
||||
this.owner.getDrawableX()
|
||||
);
|
||||
this.owner.setY(
|
||||
this._body.GetPosition().get_y() * this._sharedData.scaleY -
|
||||
this._body.GetPosition().get_y() * this._sharedData.worldScale -
|
||||
this.owner.getHeight() / 2 +
|
||||
this.owner.getY() -
|
||||
this.owner.getDrawableY()
|
||||
@@ -993,15 +1016,19 @@ namespace gdjs {
|
||||
) {
|
||||
const pos = this.b2Vec2(
|
||||
(this.owner.getDrawableX() + this.owner.getWidth() / 2) *
|
||||
this._sharedData.invScaleX,
|
||||
this._sharedData.worldInvScale,
|
||||
(this.owner.getDrawableY() + this.owner.getHeight() / 2) *
|
||||
this._sharedData.invScaleY
|
||||
this._sharedData.worldInvScale
|
||||
);
|
||||
body.SetTransform(pos, gdjs.toRad(this.owner.getAngle()));
|
||||
body.SetAwake(true);
|
||||
}
|
||||
}
|
||||
|
||||
getWorldScale(): float {
|
||||
return this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getGravityX(): float {
|
||||
return this._sharedData.gravityX;
|
||||
}
|
||||
@@ -1435,7 +1462,7 @@ namespace gdjs {
|
||||
const body = this._body!;
|
||||
|
||||
// Get the linear velocity on X
|
||||
return body.GetLinearVelocity().get_x() * this._sharedData.scaleX;
|
||||
return body.GetLinearVelocity().get_x() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
setLinearVelocityX(linearVelocityX: float): void {
|
||||
@@ -1448,7 +1475,7 @@ namespace gdjs {
|
||||
// Set the linear velocity on X
|
||||
body.SetLinearVelocity(
|
||||
this.b2Vec2(
|
||||
linearVelocityX * this._sharedData.invScaleX,
|
||||
linearVelocityX * this._sharedData.worldInvScale,
|
||||
body.GetLinearVelocity().get_y()
|
||||
)
|
||||
);
|
||||
@@ -1462,7 +1489,7 @@ namespace gdjs {
|
||||
const body = this._body!;
|
||||
|
||||
// Get the linear velocity on Y
|
||||
return body.GetLinearVelocity().get_y() * this._sharedData.scaleY;
|
||||
return body.GetLinearVelocity().get_y() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
setLinearVelocityY(linearVelocityY: float): void {
|
||||
@@ -1476,7 +1503,7 @@ namespace gdjs {
|
||||
body.SetLinearVelocity(
|
||||
this.b2Vec2(
|
||||
body.GetLinearVelocity().get_x(),
|
||||
linearVelocityY * this._sharedData.invScaleY
|
||||
linearVelocityY * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1490,8 +1517,8 @@ namespace gdjs {
|
||||
|
||||
// Get the linear velocity length
|
||||
return this.b2Vec2(
|
||||
body.GetLinearVelocity().get_x() * this._sharedData.scaleX,
|
||||
body.GetLinearVelocity().get_y() * this._sharedData.scaleY
|
||||
body.GetLinearVelocity().get_x() * this._sharedData.worldScale,
|
||||
body.GetLinearVelocity().get_y() * this._sharedData.worldScale
|
||||
).Length();
|
||||
}
|
||||
|
||||
@@ -1505,8 +1532,8 @@ namespace gdjs {
|
||||
// Get the linear velocity angle
|
||||
return gdjs.toDegrees(
|
||||
Math.atan2(
|
||||
body.GetLinearVelocity().get_y() * this._sharedData.scaleY,
|
||||
body.GetLinearVelocity().get_x() * this._sharedData.scaleX
|
||||
body.GetLinearVelocity().get_y() * this._sharedData.worldScale,
|
||||
body.GetLinearVelocity().get_x() * this._sharedData.worldScale
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1522,8 +1549,8 @@ namespace gdjs {
|
||||
angle = gdjs.toRad(angle);
|
||||
body.SetLinearVelocity(
|
||||
this.b2Vec2(
|
||||
linearVelocity * Math.cos(angle) * this._sharedData.invScaleX,
|
||||
linearVelocity * Math.sin(angle) * this._sharedData.invScaleY
|
||||
linearVelocity * Math.cos(angle) * this._sharedData.worldInvScale,
|
||||
linearVelocity * Math.sin(angle) * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1580,8 +1607,8 @@ namespace gdjs {
|
||||
body.ApplyForce(
|
||||
this.b2Vec2(forceX, forceY),
|
||||
this.b2Vec2Sec(
|
||||
positionX * this._sharedData.invScaleX,
|
||||
positionY * this._sharedData.invScaleY
|
||||
positionX * this._sharedData.worldInvScale,
|
||||
positionY * this._sharedData.worldInvScale
|
||||
),
|
||||
// TODO Should let Box2d awake the object itself.
|
||||
false
|
||||
@@ -1608,8 +1635,8 @@ namespace gdjs {
|
||||
body.ApplyForce(
|
||||
this.b2Vec2(length * Math.cos(angle), length * Math.sin(angle)),
|
||||
this.b2Vec2Sec(
|
||||
positionX * this._sharedData.invScaleX,
|
||||
positionY * this._sharedData.invScaleY
|
||||
positionX * this._sharedData.worldInvScale,
|
||||
positionY * this._sharedData.worldInvScale
|
||||
),
|
||||
// TODO Should let Box2d awake the object itself.
|
||||
false
|
||||
@@ -1632,16 +1659,17 @@ namespace gdjs {
|
||||
// Wake up the object
|
||||
body.SetAwake(true);
|
||||
|
||||
// TODO Optimize this using a unit vector instead of trigonometry.
|
||||
// Apply the force
|
||||
const angle = Math.atan2(
|
||||
towardY * this._sharedData.invScaleY - body.GetPosition().get_y(),
|
||||
towardX * this._sharedData.invScaleX - body.GetPosition().get_x()
|
||||
towardY * this._sharedData.worldInvScale - body.GetPosition().get_y(),
|
||||
towardX * this._sharedData.worldInvScale - body.GetPosition().get_x()
|
||||
);
|
||||
body.ApplyForce(
|
||||
this.b2Vec2(length * Math.cos(angle), length * Math.sin(angle)),
|
||||
this.b2Vec2Sec(
|
||||
positionX * this._sharedData.invScaleX,
|
||||
positionY * this._sharedData.invScaleY
|
||||
positionX * this._sharedData.worldInvScale,
|
||||
positionY * this._sharedData.worldInvScale
|
||||
),
|
||||
// TODO Should let Box2d awake the object itself.
|
||||
false
|
||||
@@ -1667,8 +1695,8 @@ namespace gdjs {
|
||||
body.ApplyLinearImpulse(
|
||||
this.b2Vec2(impulseX, impulseY),
|
||||
this.b2Vec2Sec(
|
||||
positionX * this._sharedData.invScaleX,
|
||||
positionY * this._sharedData.invScaleY
|
||||
positionX * this._sharedData.worldInvScale,
|
||||
positionY * this._sharedData.worldInvScale
|
||||
),
|
||||
// TODO Should let Box2d awake the object itself.
|
||||
false
|
||||
@@ -1695,8 +1723,8 @@ namespace gdjs {
|
||||
body.ApplyLinearImpulse(
|
||||
this.b2Vec2(length * Math.cos(angle), length * Math.sin(angle)),
|
||||
this.b2Vec2Sec(
|
||||
positionX * this._sharedData.invScaleX,
|
||||
positionY * this._sharedData.invScaleY
|
||||
positionX * this._sharedData.worldInvScale,
|
||||
positionY * this._sharedData.worldInvScale
|
||||
),
|
||||
// TODO Should let Box2d awake the object itself.
|
||||
false
|
||||
@@ -1719,16 +1747,17 @@ namespace gdjs {
|
||||
// Wake up the object
|
||||
body.SetAwake(true);
|
||||
|
||||
// TODO Optimize this using a unit vector instead of trigonometry.
|
||||
// Apply the impulse
|
||||
const angle = Math.atan2(
|
||||
towardY * this._sharedData.invScaleY - body.GetPosition().get_y(),
|
||||
towardX * this._sharedData.invScaleX - body.GetPosition().get_x()
|
||||
towardY * this._sharedData.worldInvScale - body.GetPosition().get_y(),
|
||||
towardX * this._sharedData.worldInvScale - body.GetPosition().get_x()
|
||||
);
|
||||
body.ApplyLinearImpulse(
|
||||
this.b2Vec2(length * Math.cos(angle), length * Math.sin(angle)),
|
||||
this.b2Vec2Sec(
|
||||
positionX * this._sharedData.invScaleX,
|
||||
positionY * this._sharedData.invScaleY
|
||||
positionX * this._sharedData.worldInvScale,
|
||||
positionY * this._sharedData.worldInvScale
|
||||
),
|
||||
// TODO Should let Box2d awake the object itself.
|
||||
false
|
||||
@@ -1805,7 +1834,7 @@ namespace gdjs {
|
||||
const body = this._body!;
|
||||
|
||||
// Get the mass center on X
|
||||
return body.GetWorldCenter().get_x() * this._sharedData.scaleX;
|
||||
return body.GetWorldCenter().get_x() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getMassCenterY(): float {
|
||||
@@ -1816,7 +1845,7 @@ namespace gdjs {
|
||||
const body = this._body!;
|
||||
|
||||
// Get the mass center on Y
|
||||
return body.GetWorldCenter().get_y() * this._sharedData.scaleY;
|
||||
return body.GetWorldCenter().get_y() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
// Joints
|
||||
@@ -1983,8 +2012,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x1 * this._sharedData.invScaleX,
|
||||
y1 * this._sharedData.invScaleY
|
||||
x1 * this._sharedData.worldInvScale,
|
||||
y1 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1992,17 +2021,17 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
otherBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x2 * this._sharedData.invScaleX,
|
||||
y2 * this._sharedData.invScaleY
|
||||
x2 * this._sharedData.worldInvScale,
|
||||
y2 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
jointDef.set_length(
|
||||
length > 0
|
||||
? length * this._sharedData.invScaleX
|
||||
? length * this._sharedData.worldInvScale
|
||||
: this.b2Vec2(
|
||||
(x2 - x1) * this._sharedData.invScaleX,
|
||||
(y2 - y1) * this._sharedData.invScaleY
|
||||
(x2 - x1) * this._sharedData.worldInvScale,
|
||||
(y2 - y1) * this._sharedData.worldInvScale
|
||||
).Length()
|
||||
);
|
||||
jointDef.set_frequencyHz(frequency >= 0 ? frequency : 0);
|
||||
@@ -2031,7 +2060,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint length
|
||||
return joint.GetLength() * this._sharedData.scaleX;
|
||||
return joint.GetLength() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
setDistanceJointLength(jointId: integer | string, length: float): void {
|
||||
@@ -2049,7 +2078,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Set the joint length
|
||||
joint.SetLength(length * this._sharedData.invScaleX);
|
||||
joint.SetLength(length * this._sharedData.worldInvScale);
|
||||
|
||||
// Awake the bodies
|
||||
joint.GetBodyA().SetAwake(true);
|
||||
@@ -2148,8 +2177,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
this._sharedData.staticBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x * this._sharedData.invScaleX,
|
||||
y * this._sharedData.invScaleY
|
||||
x * this._sharedData.worldInvScale,
|
||||
y * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -2157,8 +2186,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x * this._sharedData.invScaleX,
|
||||
y * this._sharedData.invScaleY
|
||||
x * this._sharedData.worldInvScale,
|
||||
y * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -2233,8 +2262,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x1 * this._sharedData.invScaleX,
|
||||
y1 * this._sharedData.invScaleY
|
||||
x1 * this._sharedData.worldInvScale,
|
||||
y1 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -2242,8 +2271,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
otherBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x2 * this._sharedData.invScaleX,
|
||||
y2 * this._sharedData.invScaleY
|
||||
x2 * this._sharedData.worldInvScale,
|
||||
y2 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -2534,8 +2563,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x1 * this._sharedData.invScaleX,
|
||||
y1 * this._sharedData.invScaleY
|
||||
x1 * this._sharedData.worldInvScale,
|
||||
y1 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -2543,8 +2572,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
otherBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x2 * this._sharedData.invScaleX,
|
||||
y2 * this._sharedData.invScaleY
|
||||
x2 * this._sharedData.worldInvScale,
|
||||
y2 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -2564,13 +2593,17 @@ namespace gdjs {
|
||||
|
||||
// The translation range must include zero
|
||||
jointDef.set_lowerTranslation(
|
||||
lowerTranslation < 0 ? lowerTranslation * this._sharedData.invScaleX : 0
|
||||
lowerTranslation < 0
|
||||
? lowerTranslation * this._sharedData.worldInvScale
|
||||
: 0
|
||||
);
|
||||
jointDef.set_upperTranslation(
|
||||
upperTranslation > 0 ? upperTranslation * this._sharedData.invScaleX : 0
|
||||
upperTranslation > 0
|
||||
? upperTranslation * this._sharedData.worldInvScale
|
||||
: 0
|
||||
);
|
||||
jointDef.set_enableMotor(enableMotor);
|
||||
jointDef.set_motorSpeed(motorSpeed * this._sharedData.invScaleX);
|
||||
jointDef.set_motorSpeed(motorSpeed * this._sharedData.worldInvScale);
|
||||
jointDef.set_maxMotorForce(maxMotorForce);
|
||||
jointDef.set_collideConnected(collideConnected);
|
||||
|
||||
@@ -2633,7 +2666,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint current translation
|
||||
return joint.GetJointTranslation() * this._sharedData.scaleX;
|
||||
return joint.GetJointTranslation() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getPrismaticJointSpeed(jointId: integer | string): float {
|
||||
@@ -2648,7 +2681,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint speed
|
||||
return joint.GetJointSpeed() * this._sharedData.scaleX;
|
||||
return joint.GetJointSpeed() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
isPrismaticJointLimitsEnabled(jointId: integer | string): boolean {
|
||||
@@ -2696,7 +2729,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint lower limit
|
||||
return joint.GetLowerLimit() * this._sharedData.scaleX;
|
||||
return joint.GetLowerLimit() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getPrismaticJointMaxTranslation(jointId: integer | string): float {
|
||||
@@ -2711,7 +2744,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint upper angle
|
||||
return joint.GetUpperLimit() * this._sharedData.scaleX;
|
||||
return joint.GetUpperLimit() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
setPrismaticJointLimits(
|
||||
@@ -2742,8 +2775,8 @@ namespace gdjs {
|
||||
|
||||
// Set the joint limits
|
||||
joint.SetLimits(
|
||||
lowerTranslation * this._sharedData.invScaleX,
|
||||
upperTranslation * this._sharedData.invScaleX
|
||||
lowerTranslation * this._sharedData.worldInvScale,
|
||||
upperTranslation * this._sharedData.worldInvScale
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2792,7 +2825,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint motor speed
|
||||
return joint.GetMotorSpeed() * this._sharedData.scaleX;
|
||||
return joint.GetMotorSpeed() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
setPrismaticJointMotorSpeed(jointId: integer | string, speed): void {
|
||||
@@ -2807,7 +2840,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Set the joint motor speed
|
||||
joint.SetMotorSpeed(speed * this._sharedData.invScaleX);
|
||||
joint.SetMotorSpeed(speed * this._sharedData.worldInvScale);
|
||||
}
|
||||
|
||||
getPrismaticJointMaxMotorForce(jointId: integer | string): float {
|
||||
@@ -2904,8 +2937,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x1 * this._sharedData.invScaleX,
|
||||
y1 * this._sharedData.invScaleY
|
||||
x1 * this._sharedData.worldInvScale,
|
||||
y1 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -2913,37 +2946,37 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
otherBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x2 * this._sharedData.invScaleX,
|
||||
y2 * this._sharedData.invScaleY
|
||||
x2 * this._sharedData.worldInvScale,
|
||||
y2 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
jointDef.set_groundAnchorA(
|
||||
this.b2Vec2(
|
||||
groundX1 * this._sharedData.invScaleX,
|
||||
groundY1 * this._sharedData.invScaleY
|
||||
groundX1 * this._sharedData.worldInvScale,
|
||||
groundY1 * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
jointDef.set_groundAnchorB(
|
||||
this.b2Vec2(
|
||||
groundX2 * this._sharedData.invScaleX,
|
||||
groundY2 * this._sharedData.invScaleY
|
||||
groundX2 * this._sharedData.worldInvScale,
|
||||
groundY2 * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
jointDef.set_lengthA(
|
||||
lengthA > 0
|
||||
? lengthA * this._sharedData.invScaleX
|
||||
? lengthA * this._sharedData.worldInvScale
|
||||
: this.b2Vec2(
|
||||
(groundX1 - x1) * this._sharedData.invScaleX,
|
||||
(groundY1 - y1) * this._sharedData.invScaleY
|
||||
(groundX1 - x1) * this._sharedData.worldInvScale,
|
||||
(groundY1 - y1) * this._sharedData.worldInvScale
|
||||
).Length()
|
||||
);
|
||||
jointDef.set_lengthB(
|
||||
lengthB > 0
|
||||
? lengthB * this._sharedData.invScaleX
|
||||
? lengthB * this._sharedData.worldInvScale
|
||||
: this.b2Vec2(
|
||||
(groundX2 - x2) * this._sharedData.invScaleX,
|
||||
(groundY2 - y2) * this._sharedData.invScaleY
|
||||
(groundX2 - x2) * this._sharedData.worldInvScale,
|
||||
(groundY2 - y2) * this._sharedData.worldInvScale
|
||||
).Length()
|
||||
);
|
||||
jointDef.set_ratio(ratio > 0 ? ratio : 1);
|
||||
@@ -2971,7 +3004,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint ground anchor
|
||||
return joint.GetGroundAnchorA().get_x() * this._sharedData.scaleX;
|
||||
return joint.GetGroundAnchorA().get_x() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getPulleyJointFirstGroundAnchorY(jointId: integer | string): float {
|
||||
@@ -2984,7 +3017,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint ground anchor
|
||||
return joint.GetGroundAnchorA().get_y() * this._sharedData.scaleY;
|
||||
return joint.GetGroundAnchorA().get_y() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getPulleyJointSecondGroundAnchorX(jointId: integer | string): float {
|
||||
@@ -2997,7 +3030,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint ground anchor
|
||||
return joint.GetGroundAnchorB().get_x() * this._sharedData.scaleX;
|
||||
return joint.GetGroundAnchorB().get_x() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getPulleyJointSecondGroundAnchorY(jointId: integer | string): float {
|
||||
@@ -3010,7 +3043,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint ground anchor
|
||||
return joint.GetGroundAnchorB().get_y() * this._sharedData.scaleY;
|
||||
return joint.GetGroundAnchorB().get_y() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getPulleyJointFirstLength(jointId: integer | string): float {
|
||||
@@ -3023,7 +3056,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint length
|
||||
return joint.GetCurrentLengthA() * this._sharedData.scaleX;
|
||||
return joint.GetCurrentLengthA() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getPulleyJointSecondLength(jointId: integer | string): float {
|
||||
@@ -3036,7 +3069,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint length
|
||||
return joint.GetCurrentLengthB() * this._sharedData.scaleX;
|
||||
return joint.GetCurrentLengthB() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getPulleyJointRatio(jointId: integer | string): float {
|
||||
@@ -3188,8 +3221,8 @@ namespace gdjs {
|
||||
jointDef.set_bodyB(body);
|
||||
jointDef.set_target(
|
||||
this.b2Vec2(
|
||||
targetX * this._sharedData.invScaleX,
|
||||
targetY * this._sharedData.invScaleY
|
||||
targetX * this._sharedData.worldInvScale,
|
||||
targetY * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
jointDef.set_maxForce(maxForce >= 0 ? maxForce : 0);
|
||||
@@ -3217,7 +3250,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint target X
|
||||
return joint.GetTarget().get_x() * this._sharedData.scaleX;
|
||||
return joint.GetTarget().get_x() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getMouseJointTargetY(jointId: integer | string): float {
|
||||
@@ -3229,7 +3262,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint target Y
|
||||
return joint.GetTarget().get_y() * this._sharedData.scaleY;
|
||||
return joint.GetTarget().get_y() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
setMouseJointTarget(
|
||||
@@ -3247,8 +3280,8 @@ namespace gdjs {
|
||||
// Set the joint target
|
||||
joint.SetTarget(
|
||||
this.b2Vec2(
|
||||
targetX * this._sharedData.invScaleX,
|
||||
targetY * this._sharedData.invScaleY
|
||||
targetX * this._sharedData.worldInvScale,
|
||||
targetY * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
|
||||
@@ -3386,8 +3419,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x1 * this._sharedData.invScaleX,
|
||||
y1 * this._sharedData.invScaleY
|
||||
x1 * this._sharedData.worldInvScale,
|
||||
y1 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -3395,8 +3428,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
otherBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x2 * this._sharedData.invScaleX,
|
||||
y2 * this._sharedData.invScaleY
|
||||
x2 * this._sharedData.worldInvScale,
|
||||
y2 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -3451,7 +3484,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint current translation
|
||||
return joint.GetJointTranslation() * this._sharedData.scaleX;
|
||||
return joint.GetJointTranslation() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getWheelJointSpeed(jointId: integer | string): float {
|
||||
@@ -3668,8 +3701,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x1 * this._sharedData.invScaleX,
|
||||
y1 * this._sharedData.invScaleY
|
||||
x1 * this._sharedData.worldInvScale,
|
||||
y1 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -3677,8 +3710,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
otherBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x2 * this._sharedData.invScaleX,
|
||||
y2 * this._sharedData.invScaleY
|
||||
x2 * this._sharedData.worldInvScale,
|
||||
y2 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -3817,8 +3850,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x1 * this._sharedData.invScaleX,
|
||||
y1 * this._sharedData.invScaleY
|
||||
x1 * this._sharedData.worldInvScale,
|
||||
y1 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -3826,17 +3859,17 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
otherBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x2 * this._sharedData.invScaleX,
|
||||
y2 * this._sharedData.invScaleY
|
||||
x2 * this._sharedData.worldInvScale,
|
||||
y2 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
jointDef.set_maxLength(
|
||||
maxLength > 0
|
||||
? maxLength * this._sharedData.invScaleX
|
||||
? maxLength * this._sharedData.worldInvScale
|
||||
: this.b2Vec2(
|
||||
(x2 - x1) * this._sharedData.invScaleX,
|
||||
(y2 - y1) * this._sharedData.invScaleY
|
||||
(x2 - x1) * this._sharedData.worldInvScale,
|
||||
(y2 - y1) * this._sharedData.worldInvScale
|
||||
).Length()
|
||||
);
|
||||
jointDef.set_collideConnected(collideConnected);
|
||||
@@ -3863,7 +3896,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint maximum length
|
||||
return joint.GetMaxLength() * this._sharedData.scaleX;
|
||||
return joint.GetMaxLength() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
setRopeJointMaxLength(jointId: integer | string, maxLength: float): void {
|
||||
@@ -3881,7 +3914,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Set the joint maximum length
|
||||
joint.SetMaxLength(maxLength * this._sharedData.invScaleX);
|
||||
joint.SetMaxLength(maxLength * this._sharedData.worldInvScale);
|
||||
|
||||
// Awake the bodies
|
||||
joint.GetBodyA().SetAwake(true);
|
||||
@@ -3927,8 +3960,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorA(
|
||||
body.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x1 * this._sharedData.invScaleX,
|
||||
y1 * this._sharedData.invScaleY
|
||||
x1 * this._sharedData.worldInvScale,
|
||||
y1 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -3936,8 +3969,8 @@ namespace gdjs {
|
||||
jointDef.set_localAnchorB(
|
||||
otherBody.GetLocalPoint(
|
||||
this.b2Vec2(
|
||||
x2 * this._sharedData.invScaleX,
|
||||
y2 * this._sharedData.invScaleY
|
||||
x2 * this._sharedData.worldInvScale,
|
||||
y2 * this._sharedData.worldInvScale
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -4061,8 +4094,8 @@ namespace gdjs {
|
||||
jointDef.set_bodyB(otherBody);
|
||||
jointDef.set_linearOffset(
|
||||
this.b2Vec2(
|
||||
offsetX * this._sharedData.invScaleX,
|
||||
offsetY * this._sharedData.invScaleY
|
||||
offsetX * this._sharedData.worldInvScale,
|
||||
offsetY * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
jointDef.set_angularOffset(gdjs.toRad(offsetAngle));
|
||||
@@ -4095,7 +4128,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint offset
|
||||
return joint.GetLinearOffset().get_x() * this._sharedData.scaleX;
|
||||
return joint.GetLinearOffset().get_x() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
getMotorJointOffsetY(jointId: integer | string): float {
|
||||
@@ -4108,7 +4141,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Get the joint offset
|
||||
return joint.GetLinearOffset().get_y() * this._sharedData.scaleY;
|
||||
return joint.GetLinearOffset().get_y() * this._sharedData.worldScale;
|
||||
}
|
||||
|
||||
setMotorJointOffset(
|
||||
@@ -4127,8 +4160,8 @@ namespace gdjs {
|
||||
// Set the joint offset
|
||||
joint.SetLinearOffset(
|
||||
this.b2Vec2(
|
||||
offsetX * this._sharedData.invScaleX,
|
||||
offsetY * this._sharedData.invScaleY
|
||||
offsetX * this._sharedData.worldInvScale,
|
||||
offsetY * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -73,6 +73,38 @@ module.exports = {
|
||||
.setFunctionName('setAnimationMixingDuration')
|
||||
.setGetter('getAnimationMixingDuration');
|
||||
|
||||
object
|
||||
.addExpressionAndCondition(
|
||||
'number',
|
||||
'PointAttachmentX',
|
||||
_('Point attachment X position'),
|
||||
_('x position of spine point attachment'),
|
||||
_('x position of spine _PARAM1_ point attachment for _PARAM2_ slot'),
|
||||
_('Animations and images'),
|
||||
'JsPlatform/Extensions/spine.svg'
|
||||
)
|
||||
.addParameter('object', _('Spine'), 'SpineObject')
|
||||
.addParameter('string', _('Attachment name'))
|
||||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('getPointAttachmentX');
|
||||
|
||||
object
|
||||
.addExpressionAndCondition(
|
||||
'number',
|
||||
'PointAttachmentY',
|
||||
_('Point attachment Y position'),
|
||||
_('y position of spine point attachment'),
|
||||
_('y position of spine _PARAM1_ point attachment for _PARAM2_ slot'),
|
||||
_('Animations and images'),
|
||||
'JsPlatform/Extensions/spine.svg'
|
||||
)
|
||||
.addParameter('object', _('Spine'), 'SpineObject')
|
||||
.addParameter('string', _('Attachment name'))
|
||||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('getPointAttachmentY');
|
||||
|
||||
return extension;
|
||||
},
|
||||
|
||||
|
@@ -2,6 +2,21 @@ namespace gdjs {
|
||||
const isSpine = (obj: any): obj is pixi_spine.Spine =>
|
||||
obj instanceof pixi_spine.Spine;
|
||||
|
||||
// See https://github.com/pixijs/spine/issues/562
|
||||
// IPointAttachment is not declared and exported but its implementation does exist and it is used in runtime
|
||||
interface IPointAttachment extends pixi_spine.IVertexAttachment {
|
||||
computeWorldPosition(
|
||||
bone: pixi_spine.IBone,
|
||||
point: pixi_spine.Vector2
|
||||
): pixi_spine.Vector2;
|
||||
computeWorldRotation(bone: pixi_spine.IBone): number;
|
||||
}
|
||||
|
||||
const isPointAttachment = (
|
||||
attachment: pixi_spine.IAttachment
|
||||
): attachment is IPointAttachment =>
|
||||
!!attachment && attachment.type === pixi_spine.AttachmentType.Point;
|
||||
|
||||
export class SpineRuntimeObjectPixiRenderer {
|
||||
private _object: gdjs.SpineRuntimeObject;
|
||||
private _rendererObject: pixi_spine.Spine | PIXI.Container;
|
||||
@@ -182,6 +197,44 @@ namespace gdjs {
|
||||
return this._isAnimationComplete;
|
||||
}
|
||||
|
||||
getPointAttachmentPosition(
|
||||
attachmentName: string,
|
||||
slotName?: string
|
||||
): pixi_spine.Vector2 {
|
||||
if (!slotName) {
|
||||
slotName = attachmentName;
|
||||
}
|
||||
if (!isSpine(this._rendererObject)) {
|
||||
return new pixi_spine.Vector2(
|
||||
this._rendererObject.x,
|
||||
this._rendererObject.y
|
||||
);
|
||||
}
|
||||
const slot = this._rendererObject.skeleton.findSlot(slotName);
|
||||
if (!slot) {
|
||||
throw new Error(
|
||||
`Unable to find ${slotName} slot name for ${attachmentName} point attachment.`
|
||||
);
|
||||
}
|
||||
const attachment = this._rendererObject.skeleton.getAttachmentByName(
|
||||
slotName,
|
||||
attachmentName
|
||||
);
|
||||
if (!isPointAttachment(attachment)) {
|
||||
throw new Error(
|
||||
`Unable to find ${attachmentName} point attachment with ${slotName} slot name.`
|
||||
);
|
||||
}
|
||||
|
||||
return new PIXI.Matrix()
|
||||
.rotate(this._rendererObject.rotation)
|
||||
.scale(this._rendererObject.scale.x, this._rendererObject.scale.y)
|
||||
.translate(this._rendererObject.x, this._rendererObject.y)
|
||||
.apply(
|
||||
attachment.computeWorldPosition(slot.bone, new pixi_spine.Vector2())
|
||||
);
|
||||
}
|
||||
|
||||
private constructRendererObject(): pixi_spine.Spine | PIXI.Container {
|
||||
const game = this.instanceContainer.getGame();
|
||||
const spineManager = game.getSpineManager();
|
||||
|
@@ -67,7 +67,7 @@ namespace gdjs {
|
||||
this._animations = objectData.content.animations;
|
||||
this._originalScale = objectData.content.scale;
|
||||
this.spineResourceName = objectData.content.spineResourceName;
|
||||
this._animationMixingDuration = 0.1;
|
||||
this._animationMixingDuration = 0;
|
||||
this._renderer = new gdjs.SpineRuntimeObjectRenderer(
|
||||
this,
|
||||
instanceContainer
|
||||
@@ -500,6 +500,16 @@ namespace gdjs {
|
||||
this._isPausedFrameDirty = true;
|
||||
}
|
||||
|
||||
getPointAttachmentX(attachmentName: string, slotName?: string): number {
|
||||
return this._renderer.getPointAttachmentPosition(attachmentName, slotName)
|
||||
.x;
|
||||
}
|
||||
|
||||
getPointAttachmentY(attachmentName: string, slotName?: string): number {
|
||||
return this._renderer.getPointAttachmentPosition(attachmentName, slotName)
|
||||
.y;
|
||||
}
|
||||
|
||||
getAnimationDuration(): number {
|
||||
if (this._animations.length === 0) {
|
||||
return 0;
|
||||
|
@@ -278,7 +278,7 @@ module.exports = {
|
||||
|
||||
// Properties expressions/conditions/actions:
|
||||
|
||||
// Deprecated
|
||||
// Deprecated, see TextContainerCapability
|
||||
object
|
||||
.addExpressionAndConditionAndAction(
|
||||
'string',
|
||||
@@ -298,6 +298,7 @@ module.exports = {
|
||||
.setFunctionName('setText')
|
||||
.setGetter('getText');
|
||||
|
||||
// Deprecated, see TextContainerCapability
|
||||
object
|
||||
.addStrExpression(
|
||||
'Text',
|
||||
@@ -306,6 +307,7 @@ module.exports = {
|
||||
'',
|
||||
'res/conditions/text24_black.png'
|
||||
)
|
||||
.setHidden()
|
||||
.addParameter('object', _('Text input'), 'TextInputObject', false)
|
||||
.setFunctionName('getText');
|
||||
|
||||
|
@@ -49,7 +49,7 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
|
||||
"res/actions/font.png")
|
||||
|
||||
.AddParameter("object", _("Object"), "Text")
|
||||
.AddParameter("police", _("Font"))
|
||||
.AddParameter("fontResource", _("Font resource name"))
|
||||
.SetFunctionName("ChangeFont");
|
||||
|
||||
obj.AddAction(
|
||||
|
@@ -38,6 +38,9 @@ class TextObjectJsExtension : public gd::PlatformExtension {
|
||||
GetAllExpressionsForObject("TextObject::Text")["FontSize"]
|
||||
.SetFunctionName("getCharacterSize");
|
||||
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::Font"]
|
||||
.SetFunctionName("setFontName");
|
||||
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::SetBold"]
|
||||
.SetFunctionName("setBold");
|
||||
GetAllConditionsForObject("TextObject::Text")["TextObject::IsBold"]
|
||||
@@ -188,8 +191,6 @@ class TextObjectJsExtension : public gd::PlatformExtension {
|
||||
.SetFunctionName("setShadow");
|
||||
|
||||
// Unimplemented actions and conditions:
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::Font"]
|
||||
.SetFunctionName("");
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::SetUnderlined"]
|
||||
.SetFunctionName("");
|
||||
GetAllConditionsForObject("TextObject::Text")["TextObject::IsUnderlined"]
|
||||
|
@@ -184,7 +184,6 @@ const defineTileMap = function (extension, _, gd) {
|
||||
objectTileMap
|
||||
)
|
||||
.setCategoryFullName(_('Advanced'))
|
||||
.addDefaultBehavior('EffectCapability::EffectBehavior')
|
||||
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
|
||||
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
|
||||
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
|
||||
@@ -721,7 +720,6 @@ const defineSimpleTileMap = function (extension, _, gd) {
|
||||
objectSimpleTileMap
|
||||
)
|
||||
.setCategoryFullName(_('General'))
|
||||
.addDefaultBehavior('EffectCapability::EffectBehavior')
|
||||
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
|
||||
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
|
||||
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
|
||||
@@ -1240,7 +1238,6 @@ const defineCollisionMask = function (extension, _, gd) {
|
||||
collisionMaskObject
|
||||
)
|
||||
.setCategoryFullName(_('Advanced'))
|
||||
.addDefaultBehavior('EffectCapability::EffectBehavior')
|
||||
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
|
||||
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
|
||||
.setIncludeFile('Extensions/TileMap/tilemapcollisionmaskruntimeobject.js')
|
||||
|
@@ -96,6 +96,16 @@ namespace gdjs {
|
||||
(this._transformationUpToDateCount + 1) % Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
invalidateTile(layerIndex: integer, x: integer, y: integer) {
|
||||
const layer = this.getLayer(layerIndex);
|
||||
if (layer) {
|
||||
const tile = layer.get(x, y);
|
||||
if (tile) {
|
||||
tile.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The tile map width in pixels.
|
||||
*/
|
||||
@@ -690,6 +700,28 @@ namespace gdjs {
|
||||
this.affineTransformationUpToDateCount = this.layer.tileMap._transformationUpToDateCount;
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
this.affineTransformationUpToDateCount = -1;
|
||||
// Also invalidate neighbors because their hit boxes may need to be
|
||||
// extended differently.
|
||||
let neighbor = this.layer.get(this.x - 1, this.y);
|
||||
if (neighbor) {
|
||||
neighbor.affineTransformationUpToDateCount = -1;
|
||||
}
|
||||
neighbor = this.layer.get(this.x + 1, this.y);
|
||||
if (neighbor) {
|
||||
neighbor.affineTransformationUpToDateCount = -1;
|
||||
}
|
||||
neighbor = this.layer.get(this.x, this.y - 1);
|
||||
if (neighbor) {
|
||||
neighbor.affineTransformationUpToDateCount = -1;
|
||||
}
|
||||
neighbor = this.layer.get(this.x, this.y + 1);
|
||||
if (neighbor) {
|
||||
neighbor.affineTransformationUpToDateCount = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The hitboxes of this tile in the scene basis.
|
||||
*/
|
||||
@@ -701,7 +733,6 @@ namespace gdjs {
|
||||
const definition = this.getDefinition();
|
||||
if (!definition) {
|
||||
this._setHitboxesUpToDate();
|
||||
// It should already be []
|
||||
this.hitBoxes.length = 0;
|
||||
return this.hitBoxes;
|
||||
}
|
||||
@@ -709,14 +740,100 @@ namespace gdjs {
|
||||
const definitionHitboxes = definition.getHitBoxes(tag);
|
||||
if (!definitionHitboxes) {
|
||||
this._setHitboxesUpToDate();
|
||||
// It should already be []
|
||||
this.hitBoxes.length = 0;
|
||||
return this.hitBoxes;
|
||||
}
|
||||
|
||||
const layerTransformation = this.layer.tileMap.getTransformation();
|
||||
const width = this.layer.tileMap.getTileWidth();
|
||||
const height = this.layer.tileMap.getTileHeight();
|
||||
const tileMap = this.layer.tileMap;
|
||||
const width = tileMap.getTileWidth();
|
||||
const height = tileMap.getTileHeight();
|
||||
|
||||
// Extend the hit boxes.
|
||||
// It avoids small objects to be pushed side way into a wall when they
|
||||
// should be pop out of the wall.
|
||||
|
||||
const hasFullHitBox =
|
||||
definitionHitboxes.length === 1 && definition.hasFullHitBox(tag);
|
||||
if (hasFullHitBox) {
|
||||
const isLeftFull = this._hasNeighborFullHitBox(-1, 0);
|
||||
const isRightFull = this._hasNeighborFullHitBox(1, 0);
|
||||
const isTopFull = this._hasNeighborFullHitBox(0, -1);
|
||||
const isBottomFull = this._hasNeighborFullHitBox(0, 1);
|
||||
|
||||
let hitBoxesCount = 0;
|
||||
if (isLeftFull || isRightFull) {
|
||||
let minX = isLeftFull ? -width : 0;
|
||||
let maxX = isRightFull ? 2 * width : width;
|
||||
if (hitBoxesCount >= this.hitBoxes.length) {
|
||||
this.hitBoxes[hitBoxesCount] = gdjs.Polygon.createRectangle(0, 0);
|
||||
}
|
||||
TransformedCollisionTile.setRectangle(
|
||||
this.hitBoxes[hitBoxesCount],
|
||||
minX,
|
||||
0,
|
||||
maxX,
|
||||
height
|
||||
);
|
||||
hitBoxesCount++;
|
||||
}
|
||||
if (isTopFull || isBottomFull) {
|
||||
let minY = isTopFull ? -height : 0;
|
||||
let maxY = isBottomFull ? 2 * height : height;
|
||||
if (hitBoxesCount >= this.hitBoxes.length) {
|
||||
this.hitBoxes[hitBoxesCount] = gdjs.Polygon.createRectangle(0, 0);
|
||||
}
|
||||
TransformedCollisionTile.setRectangle(
|
||||
this.hitBoxes[hitBoxesCount],
|
||||
0,
|
||||
minY,
|
||||
width,
|
||||
maxY
|
||||
);
|
||||
hitBoxesCount++;
|
||||
}
|
||||
if (hitBoxesCount === 0) {
|
||||
if (this.hitBoxes.length === 0) {
|
||||
this.hitBoxes[0] = gdjs.Polygon.createRectangle(0, 0);
|
||||
}
|
||||
TransformedCollisionTile.setRectangle(
|
||||
this.hitBoxes[0],
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height
|
||||
);
|
||||
hitBoxesCount++;
|
||||
}
|
||||
this.hitBoxes.length = hitBoxesCount;
|
||||
} else {
|
||||
for (
|
||||
let polygonIndex = 0;
|
||||
polygonIndex < definitionHitboxes.length;
|
||||
polygonIndex++
|
||||
) {
|
||||
const defPolygon = definitionHitboxes[polygonIndex];
|
||||
if (polygonIndex >= this.hitBoxes.length) {
|
||||
// This can't happen in practice as only the simple tile map can be
|
||||
// modify and it only contains full hit boxes.
|
||||
this.hitBoxes[polygonIndex] = gdjs.Polygon.createRectangle(0, 0);
|
||||
}
|
||||
const polygon = this.hitBoxes[polygonIndex];
|
||||
|
||||
for (
|
||||
let vertexIndex = 0;
|
||||
vertexIndex < polygon.vertices.length;
|
||||
vertexIndex++
|
||||
) {
|
||||
const defVertex = defPolygon[vertexIndex];
|
||||
const vertex = polygon.vertices[vertexIndex];
|
||||
|
||||
vertex[0] = defVertex[0];
|
||||
vertex[1] = defVertex[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the hit boxes.
|
||||
|
||||
const tileTransformation =
|
||||
TransformedCollisionTile.workingTransformation;
|
||||
@@ -731,16 +848,12 @@ namespace gdjs {
|
||||
tileTransformation.flipX(width / 2);
|
||||
tileTransformation.rotateAround(Math.PI / 2, width / 2, height / 2);
|
||||
}
|
||||
tileTransformation.preConcatenate(layerTransformation);
|
||||
|
||||
// The tile map can't change at runtime so the existing arrays can be
|
||||
// reused safely.
|
||||
tileTransformation.preConcatenate(tileMap.getTransformation());
|
||||
for (
|
||||
let polygonIndex = 0;
|
||||
polygonIndex < this.hitBoxes.length;
|
||||
polygonIndex++
|
||||
) {
|
||||
const defPolygon = definitionHitboxes[polygonIndex];
|
||||
const polygon = this.hitBoxes[polygonIndex];
|
||||
|
||||
for (
|
||||
@@ -748,15 +861,42 @@ namespace gdjs {
|
||||
vertexIndex < polygon.vertices.length;
|
||||
vertexIndex++
|
||||
) {
|
||||
const defVertex = defPolygon[vertexIndex];
|
||||
const vertex = polygon.vertices[vertexIndex];
|
||||
|
||||
tileTransformation.transform(defVertex, vertex);
|
||||
tileTransformation.transform(vertex, vertex);
|
||||
}
|
||||
}
|
||||
this._setHitboxesUpToDate();
|
||||
return this.hitBoxes;
|
||||
}
|
||||
|
||||
private _hasNeighborFullHitBox(deltaX: integer, deltaY: integer) {
|
||||
const sourceLayer = this.layer._source;
|
||||
const tileId = sourceLayer.getTileId(this.x + deltaX, this.y + deltaY);
|
||||
const tileDefinition =
|
||||
tileId && this.layer.tileMap.getTileDefinition(tileId);
|
||||
return (
|
||||
tileDefinition && tileDefinition.hasFullHitBox(this.layer.tileMap.tag)
|
||||
);
|
||||
}
|
||||
|
||||
private static setRectangle(
|
||||
polygon: gdjs.Polygon,
|
||||
minX: float,
|
||||
minY: float,
|
||||
maxX: float,
|
||||
maxY: float
|
||||
) {
|
||||
const vertices = polygon.vertices;
|
||||
vertices[0][0] = minX;
|
||||
vertices[0][1] = minY;
|
||||
vertices[1][0] = maxX;
|
||||
vertices[1][1] = minY;
|
||||
vertices[2][0] = maxX;
|
||||
vertices[2][1] = maxY;
|
||||
vertices[3][0] = minX;
|
||||
vertices[3][1] = maxY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"version":3,"file":"TiledTileMapLoader.d.ts","sourceRoot":"","sources":["../../../src/load/tiled/TiledTileMapLoader.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EAGhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAO7C;;GAEG;AACH,yBAAiB,kBAAkB,CAAC;IAClC;;;;;;OAMG;IACH,SAAgB,IAAI,CAClB,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,GAAG,GACR,eAAe,GAAG,IAAI,CA2KxB;CACF"}
|
||||
{"version":3,"file":"TiledTileMapLoader.d.ts","sourceRoot":"","sources":["../../../src/load/tiled/TiledTileMapLoader.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EAGhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAO7C;;GAEG;AACH,yBAAiB,kBAAkB,CAAC;IAClC;;;;;;OAMG;IACH,SAAgB,IAAI,CAClB,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,GAAG,GACR,eAAe,GAAG,IAAI,CAkLxB;CACF"}
|
@@ -431,8 +431,13 @@ export declare class TileDefinition {
|
||||
* Add a polygon for the collision layer
|
||||
* @param tag The tag to allow collision layer filtering.
|
||||
* @param polygon The polygon to use for collisions.
|
||||
* @param hasFullHitBox Set to `true` when the hitBox cover the whole tile.
|
||||
*/
|
||||
addHitBox(tag: string, polygon: PolygonVertices): void;
|
||||
addHitBox(
|
||||
tag: string,
|
||||
polygon: PolygonVertices,
|
||||
hasFullHitBox: boolean
|
||||
): void;
|
||||
/**
|
||||
* This property is used by {@link TransformedCollisionTileMap}
|
||||
* to make collision classes.
|
||||
@@ -446,6 +451,12 @@ export declare class TileDefinition {
|
||||
* @returns The hit boxes for this tile.
|
||||
*/
|
||||
getHitBoxes(tag: string): PolygonVertices[] | undefined;
|
||||
/**
|
||||
* Return `true` if the hit-box cover the whole tile.
|
||||
* @param tag The tag to allow collision layer filtering.
|
||||
* @returns `true` if the hit-box cover the whole tile.
|
||||
*/
|
||||
hasFullHitBox(tag: string): boolean;
|
||||
/**
|
||||
* Animated tiles have a limitation:
|
||||
* they are only able to use frames arranged horizontally one next
|
||||
|
File diff suppressed because one or more lines are too long
@@ -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;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"}
|
||||
{"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;IAYP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP;;;;;;;OAOG;IACH,kCAAkC,CAChC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwBP,WAAW,IAAI,IAAI;CAIpB"}
|
@@ -231,12 +231,16 @@ namespace gdjs {
|
||||
);
|
||||
return;
|
||||
}
|
||||
tileDefinition.addHitBox(this._hitBoxTag, [
|
||||
[0, 0],
|
||||
[0, tileMap.getTileHeight()],
|
||||
[tileMap.getTileWidth(), tileMap.getTileHeight()],
|
||||
[tileMap.getTileWidth(), 0],
|
||||
]);
|
||||
tileDefinition.addHitBox(
|
||||
this._hitBoxTag,
|
||||
[
|
||||
[0, 0],
|
||||
[0, tileMap.getTileHeight()],
|
||||
[tileMap.getTileWidth(), tileMap.getTileHeight()],
|
||||
[tileMap.getTileWidth(), 0],
|
||||
],
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
this._tileMapManager.getOrLoadSimpleTileMapTextureCache(
|
||||
@@ -624,7 +628,38 @@ namespace gdjs {
|
||||
columnIndex: integer,
|
||||
rowIndex: integer
|
||||
) {
|
||||
this._renderer.setTileId(columnIndex, rowIndex, 0, tileId);
|
||||
const tileMap = this._renderer._tileMap;
|
||||
if (!tileMap) {
|
||||
return;
|
||||
}
|
||||
const layer = tileMap.getTileLayer(this._layerIndex);
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
const oldTileId = layer.getTileId(columnIndex, rowIndex);
|
||||
if (tileId === oldTileId) {
|
||||
return;
|
||||
}
|
||||
layer.setTile(columnIndex, rowIndex, tileId);
|
||||
|
||||
if (this._collisionTileMap) {
|
||||
const oldTileDefinition =
|
||||
oldTileId !== undefined && tileMap.getTileDefinition(oldTileId);
|
||||
const newTileDefinition = tileMap.getTileDefinition(tileId);
|
||||
const hadFullHitBox =
|
||||
!!oldTileDefinition &&
|
||||
oldTileDefinition.hasFullHitBox(this._hitBoxTag);
|
||||
const haveFullHitBox =
|
||||
!!newTileDefinition &&
|
||||
newTileDefinition.hasFullHitBox(this._hitBoxTag);
|
||||
if (hadFullHitBox !== haveFullHitBox) {
|
||||
this._collisionTileMap.invalidateTile(
|
||||
this._layerIndex,
|
||||
columnIndex,
|
||||
rowIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
this._isTileMapDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
@@ -702,7 +737,26 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
|
||||
this._renderer.removeTile(columnIndex, rowIndex, 0);
|
||||
const tileMap = this._renderer._tileMap;
|
||||
if (!tileMap) {
|
||||
return;
|
||||
}
|
||||
const layer = tileMap.getTileLayer(this._layerIndex);
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
const oldTileId = layer.getTileId(columnIndex, rowIndex);
|
||||
if (oldTileId === undefined) {
|
||||
return;
|
||||
}
|
||||
layer.removeTile(columnIndex, rowIndex);
|
||||
if (this._collisionTileMap) {
|
||||
this._collisionTileMap.invalidateTile(
|
||||
this._layerIndex,
|
||||
columnIndex,
|
||||
rowIndex
|
||||
);
|
||||
}
|
||||
this._isTileMapDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
@@ -10,7 +10,8 @@ namespace gdjs {
|
||||
private _object:
|
||||
| gdjs.TileMapRuntimeObject
|
||||
| gdjs.SimpleTileMapRuntimeObject;
|
||||
private _tileMap: TileMapHelper.EditableTileMap | null = null;
|
||||
// TODO Move this attribute in the object as it's a model.
|
||||
_tileMap: TileMapHelper.EditableTileMap | null = null;
|
||||
|
||||
private _pixiObject: PIXI.tilemap.CompositeTilemap;
|
||||
|
||||
@@ -173,18 +174,6 @@ namespace gdjs {
|
||||
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.
|
||||
@@ -231,17 +220,6 @@ namespace gdjs {
|
||||
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.
|
||||
*/
|
||||
|
@@ -87,6 +87,7 @@ gd::String EventsCodeGenerator::GenerateEventsListCompleteFunctionCode(
|
||||
functionPreEventsCode + "\n" +
|
||||
globalObjectListsReset + "\n" +
|
||||
wholeEventsCode + "\n" +
|
||||
globalObjectListsReset + "\n" +
|
||||
functionPostEventsCode + "\n" +
|
||||
functionReturnCode + "\n" +
|
||||
"}\n";
|
||||
|
@@ -131,8 +131,11 @@ gd::ObjectMetadata &MetadataDeclarationHelper::DeclareObjectMetadata(
|
||||
// PlatformExtension but this line will be removed soon.
|
||||
.SetCategoryFullName(extension.GetCategory())
|
||||
.AddDefaultBehavior("ResizableCapability::ResizableBehavior")
|
||||
.AddDefaultBehavior("ScalableCapability::ScalableBehavior")
|
||||
.AddDefaultBehavior("FlippableCapability::FlippableBehavior");
|
||||
if (!eventsBasedObject.IsInnerAreaFollowingParentSize()) {
|
||||
objectMetadata
|
||||
.AddDefaultBehavior("ScalableCapability::ScalableBehavior");
|
||||
}
|
||||
if (eventsBasedObject.IsRenderedIn3D()) {
|
||||
objectMetadata
|
||||
.MarkAsRenderedIn3D()
|
||||
|
@@ -101,6 +101,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
|
||||
usedExtensionsResult.Has3DObjects(),
|
||||
/*includeWebsocketDebuggerClient=*/false,
|
||||
/*includeWindowMessageDebuggerClient=*/false,
|
||||
/*includeMinimalDebuggerClient=*/false,
|
||||
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
|
||||
includesFiles);
|
||||
|
||||
|
@@ -96,7 +96,7 @@ static gd::String CleanProjectName(gd::String projectName) {
|
||||
ExporterHelper::ExporterHelper(gd::AbstractFileSystem &fileSystem,
|
||||
gd::String gdjsRoot_,
|
||||
gd::String codeOutputDir_)
|
||||
: fs(fileSystem), gdjsRoot(gdjsRoot_), codeOutputDir(codeOutputDir_){};
|
||||
: fs(fileSystem), gdjsRoot(gdjsRoot_), codeOutputDir(codeOutputDir_) {};
|
||||
|
||||
bool ExporterHelper::ExportProjectForPixiPreview(
|
||||
const PreviewExportOptions &options) {
|
||||
@@ -154,6 +154,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
|
||||
!options.websocketDebuggerServerAddress.empty(),
|
||||
/*includeWindowMessageDebuggerClient=*/
|
||||
options.useWindowMessageDebuggerClient,
|
||||
/*includeMinimalDebuggerClient=*/
|
||||
options.useMinimalDebuggerClient,
|
||||
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
|
||||
includesFiles);
|
||||
|
||||
@@ -248,6 +250,24 @@ bool ExporterHelper::ExportProjectForPixiPreview(
|
||||
runtimeGameOptions.AddChild("playerToken")
|
||||
.SetStringValue(options.playerToken);
|
||||
}
|
||||
if (!options.crashReportUploadLevel.empty()) {
|
||||
runtimeGameOptions.AddChild("crashReportUploadLevel")
|
||||
.SetStringValue(options.crashReportUploadLevel);
|
||||
}
|
||||
if (!options.previewContext.empty()) {
|
||||
runtimeGameOptions.AddChild("previewContext")
|
||||
.SetStringValue(options.previewContext);
|
||||
}
|
||||
runtimeGameOptions.AddChild("gdevelopVersionWithHash")
|
||||
.SetStringValue(options.gdevelopVersionWithHash);
|
||||
if (!options.projectTemplateSlug.empty()) {
|
||||
runtimeGameOptions.AddChild("projectTemplateSlug")
|
||||
.SetStringValue(options.projectTemplateSlug);
|
||||
}
|
||||
if (!options.sourceGameId.empty()) {
|
||||
runtimeGameOptions.AddChild("sourceGameId")
|
||||
.SetStringValue(options.sourceGameId);
|
||||
}
|
||||
|
||||
// Pass in the options the list of scripts files - useful for hot-reloading.
|
||||
auto &scriptFilesElement = runtimeGameOptions.AddChild("scriptFiles");
|
||||
@@ -735,6 +755,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
||||
bool pixiInThreeRenderers,
|
||||
bool includeWebsocketDebuggerClient,
|
||||
bool includeWindowMessageDebuggerClient,
|
||||
bool includeMinimalDebuggerClient,
|
||||
gd::String gdevelopLogoStyle,
|
||||
std::vector<gd::String> &includesFiles) {
|
||||
// First, do not forget common includes (they must be included before events
|
||||
@@ -809,6 +830,9 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
||||
InsertUnique(includesFiles,
|
||||
"debugger-client/window-message-debugger-client.js");
|
||||
}
|
||||
if (includeMinimalDebuggerClient) {
|
||||
InsertUnique(includesFiles, "debugger-client/minimal-debugger-client.js");
|
||||
}
|
||||
|
||||
if (pixiInThreeRenderers) {
|
||||
InsertUnique(includesFiles, "pixi-renderers/three.js");
|
||||
|
@@ -37,6 +37,7 @@ struct PreviewExportOptions {
|
||||
: project(project_),
|
||||
exportPath(exportPath_),
|
||||
useWindowMessageDebuggerClient(false),
|
||||
useMinimalDebuggerClient(false),
|
||||
nativeMobileApp(false),
|
||||
projectDataOnlyExport(false),
|
||||
fullLoadingScreen(false),
|
||||
@@ -93,6 +94,14 @@ struct PreviewExportOptions {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set that the game should have a minimal debugger client.
|
||||
*/
|
||||
PreviewExportOptions &UseMinimalDebuggerClient() {
|
||||
useMinimalDebuggerClient = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set that the preview is launched from a GDevelop native mobile app
|
||||
* (iOS or Android).
|
||||
@@ -203,11 +212,56 @@ struct PreviewExportOptions {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the level of crash reports to be sent to GDevelop APIs.
|
||||
*/
|
||||
PreviewExportOptions &SetCrashReportUploadLevel(
|
||||
const gd::String& crashReportUploadLevel_) {
|
||||
crashReportUploadLevel = crashReportUploadLevel_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the context of the preview.
|
||||
*/
|
||||
PreviewExportOptions &SetPreviewContext(
|
||||
const gd::String& previewContext_) {
|
||||
previewContext = previewContext_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the GDevelop version so the game is aware of it.
|
||||
*/
|
||||
PreviewExportOptions &SetGDevelopVersionWithHash(
|
||||
const gd::String& gdevelopVersionWithHash_) {
|
||||
gdevelopVersionWithHash = gdevelopVersionWithHash_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the template slug that was used to create the project.
|
||||
*/
|
||||
PreviewExportOptions &SetProjectTemplateSlug(
|
||||
const gd::String& projectTemplateSlug_) {
|
||||
projectTemplateSlug = projectTemplateSlug_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the source game id that was used to create the project.
|
||||
*/
|
||||
PreviewExportOptions &SetSourceGameId(const gd::String& sourceGameId_) {
|
||||
sourceGameId = sourceGameId_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
gd::Project &project;
|
||||
gd::String exportPath;
|
||||
gd::String websocketDebuggerServerAddress;
|
||||
gd::String websocketDebuggerServerPort;
|
||||
bool useWindowMessageDebuggerClient;
|
||||
bool useMinimalDebuggerClient;
|
||||
gd::String layoutName;
|
||||
gd::String externalLayoutName;
|
||||
gd::String fallbackAuthorUsername;
|
||||
@@ -224,6 +278,11 @@ struct PreviewExportOptions {
|
||||
gd::String electronRemoteRequirePath;
|
||||
gd::String gdevelopResourceToken;
|
||||
bool allowAuthenticationUsingIframeForPreview;
|
||||
gd::String crashReportUploadLevel;
|
||||
gd::String previewContext;
|
||||
gd::String gdevelopVersionWithHash;
|
||||
gd::String projectTemplateSlug;
|
||||
gd::String sourceGameId;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -326,6 +385,7 @@ class ExporterHelper {
|
||||
bool pixiInThreeRenderers,
|
||||
bool includeWebsocketDebuggerClient,
|
||||
bool includeWindowMessageDebuggerClient,
|
||||
bool includeMinimalDebuggerClient,
|
||||
gd::String gdevelopLogoStyle,
|
||||
std::vector<gd::String> &includesFiles);
|
||||
|
||||
|
@@ -4,6 +4,8 @@
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('CustomRuntimeObject');
|
||||
|
||||
export type ObjectConfiguration = {
|
||||
content: any;
|
||||
};
|
||||
@@ -11,24 +13,6 @@ namespace gdjs {
|
||||
export type CustomObjectConfiguration = ObjectConfiguration & {
|
||||
animatable?: SpriteAnimationData[];
|
||||
childrenContent: { [objectName: string]: ObjectConfiguration & any };
|
||||
instances: InstanceData[];
|
||||
layers: LayerData[];
|
||||
// The flat representation of defaultSize.
|
||||
areaMinX: float;
|
||||
areaMinY: float;
|
||||
areaMinZ: float;
|
||||
areaMaxX: float;
|
||||
areaMaxY: float;
|
||||
areaMaxZ: float;
|
||||
/**
|
||||
* A value shared by every object instances.
|
||||
*
|
||||
* @see gdjs.CustomRuntimeObject._forcedDefaultSize
|
||||
**/
|
||||
defaultSize: {
|
||||
min: [float, float, float];
|
||||
max: [float, float, float];
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -56,14 +40,21 @@ namespace gdjs {
|
||||
/** The dimension of this object is calculated from its children AABBs. */
|
||||
private _unrotatedAABB: AABB = { min: [0, 0], max: [0, 0] };
|
||||
/**
|
||||
* The default size defined by users in the custom object initial instances editor.
|
||||
*
|
||||
* Don't modify it as it would affect every instance.
|
||||
* The bounds of the object content before any transformation.
|
||||
* @see gdjs.CustomRuntimeObjectInstanceContainer._initialInnerArea
|
||||
**/
|
||||
private _forcedDefaultSize: {
|
||||
protected _innerArea: {
|
||||
min: [float, float, float];
|
||||
max: [float, float, float];
|
||||
} | null = null;
|
||||
/**
|
||||
* When the parent dimensions change:
|
||||
* - if `false`, the object is stretch proportionally while children local
|
||||
* positions stay the same ({@link gdjs.CustomRuntimeObject._innerArea} don't change).
|
||||
* - if `true`, the children local positions need to be adapted by events
|
||||
* to follow their parent size.
|
||||
*/
|
||||
protected _isInnerAreaFollowingParentSize = false;
|
||||
private _scaleX: float = 1;
|
||||
private _scaleY: float = 1;
|
||||
private _flippedX: boolean = false;
|
||||
@@ -73,6 +64,7 @@ namespace gdjs {
|
||||
private _localTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
|
||||
private _localInverseTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
|
||||
private _isLocalTransformationDirty: boolean = true;
|
||||
_type: string;
|
||||
|
||||
/**
|
||||
* @param parent The container the object belongs to
|
||||
@@ -83,19 +75,50 @@ namespace gdjs {
|
||||
objectData: ObjectData & CustomObjectConfiguration
|
||||
) {
|
||||
super(parent, objectData);
|
||||
this._type = objectData.type;
|
||||
this._instanceContainer = new gdjs.CustomRuntimeObjectInstanceContainer(
|
||||
parent,
|
||||
this
|
||||
);
|
||||
this._renderer = this._createRender();
|
||||
|
||||
this._createDefaultSizeIfNeeded(objectData);
|
||||
this._instanceContainer.loadFrom(objectData);
|
||||
this._initializeFromObjectData(objectData);
|
||||
|
||||
// The generated code calls onCreated at the constructor end
|
||||
// and onCreated calls its super implementation at its end.
|
||||
}
|
||||
|
||||
private _initializeFromObjectData(
|
||||
objectData: ObjectData & CustomObjectConfiguration
|
||||
) {
|
||||
const eventsBasedObjectData = this._runtimeScene
|
||||
.getGame()
|
||||
.getEventsBasedObjectData(objectData.type);
|
||||
if (!eventsBasedObjectData) {
|
||||
logger.error(
|
||||
`A CustomRuntimeObject was initialized (or re-initialized) from object data referring to an non existing events based object data with type "${objectData.type}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._isInnerAreaFollowingParentSize =
|
||||
eventsBasedObjectData.isInnerAreaFollowingParentSize;
|
||||
if (eventsBasedObjectData.instances.length > 0) {
|
||||
if (!this._innerArea) {
|
||||
this._innerArea = {
|
||||
min: [0, 0, 0],
|
||||
max: [0, 0, 0],
|
||||
};
|
||||
}
|
||||
this._innerArea.min[0] = eventsBasedObjectData.areaMinX;
|
||||
this._innerArea.min[1] = eventsBasedObjectData.areaMinY;
|
||||
this._innerArea.min[2] = eventsBasedObjectData.areaMinZ;
|
||||
this._innerArea.max[0] = eventsBasedObjectData.areaMaxX;
|
||||
this._innerArea.max[1] = eventsBasedObjectData.areaMaxY;
|
||||
this._innerArea.max[2] = eventsBasedObjectData.areaMaxZ;
|
||||
}
|
||||
this._instanceContainer.loadFrom(objectData, eventsBasedObjectData);
|
||||
}
|
||||
|
||||
protected abstract _createRender():
|
||||
| gdjs.CustomRuntimeObject2DRenderer
|
||||
| gdjs.CustomRuntimeObject3DRenderer;
|
||||
@@ -104,38 +127,13 @@ namespace gdjs {
|
||||
reinitialize(objectData: ObjectData & CustomObjectConfiguration) {
|
||||
super.reinitialize(objectData);
|
||||
|
||||
this._createDefaultSizeIfNeeded(objectData);
|
||||
this._instanceContainer.loadFrom(objectData);
|
||||
this._initializeFromObjectData(objectData);
|
||||
this._reinitializeRenderer();
|
||||
|
||||
// The generated code calls the onCreated super implementation at the end.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize `defaultSize` if it doesn't exist.
|
||||
* `defaultSize` is shared by every instance to save memory.
|
||||
*/
|
||||
private _createDefaultSizeIfNeeded(objectData: CustomObjectConfiguration) {
|
||||
if (objectData.instances.length > 0) {
|
||||
if (!objectData.defaultSize) {
|
||||
objectData.defaultSize = {
|
||||
min: [
|
||||
objectData.areaMinX,
|
||||
objectData.areaMinY,
|
||||
objectData.areaMinZ,
|
||||
],
|
||||
max: [
|
||||
objectData.areaMaxX,
|
||||
objectData.areaMaxY,
|
||||
objectData.areaMaxZ,
|
||||
],
|
||||
};
|
||||
}
|
||||
this._forcedDefaultSize = objectData.defaultSize;
|
||||
}
|
||||
}
|
||||
|
||||
updateFromObjectData(
|
||||
oldObjectData: ObjectData & CustomObjectConfiguration,
|
||||
newObjectData: ObjectData & CustomObjectConfiguration
|
||||
@@ -147,7 +145,7 @@ namespace gdjs {
|
||||
newObjectData.animatable || []
|
||||
);
|
||||
}
|
||||
return this._instanceContainer.updateFrom(oldObjectData, newObjectData);
|
||||
return true;
|
||||
}
|
||||
|
||||
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
|
||||
@@ -228,6 +226,10 @@ namespace gdjs {
|
||||
return this._renderer;
|
||||
}
|
||||
|
||||
getChildrenContainer(): gdjs.RuntimeInstanceContainer {
|
||||
return this._instanceContainer;
|
||||
}
|
||||
|
||||
onChildrenLocationChanged() {
|
||||
this._isUntransformedHitBoxesDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
@@ -393,8 +395,8 @@ namespace gdjs {
|
||||
|
||||
getDrawableX(): float {
|
||||
let minX = 0;
|
||||
if (this._forcedDefaultSize) {
|
||||
minX = this._forcedDefaultSize.min[0];
|
||||
if (this._innerArea) {
|
||||
minX = this._innerArea.min[0];
|
||||
} else {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
@@ -415,8 +417,8 @@ namespace gdjs {
|
||||
|
||||
getDrawableY(): float {
|
||||
let minY = 0;
|
||||
if (this._forcedDefaultSize) {
|
||||
minY = this._forcedDefaultSize.min[1];
|
||||
if (this._innerArea) {
|
||||
minY = this._innerArea.min[1];
|
||||
} else {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
@@ -435,12 +437,64 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the internal left bound of the object according to its children.
|
||||
*/
|
||||
getInnerAreaMinX(): number {
|
||||
if (this._innerArea) {
|
||||
return this._innerArea.min[0];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return this._unrotatedAABB.min[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the internal top bound of the object according to its children.
|
||||
*/
|
||||
getInnerAreaMinY(): number {
|
||||
if (this._innerArea) {
|
||||
return this._innerArea.min[1];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return this._unrotatedAABB.min[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the internal right bound of the object according to its children.
|
||||
*/
|
||||
getInnerAreaMaxX(): number {
|
||||
if (this._innerArea) {
|
||||
return this._innerArea.max[0];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return this._unrotatedAABB.max[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the internal bottom bound of the object according to its children.
|
||||
*/
|
||||
getInnerAreaMaxY(): number {
|
||||
if (this._innerArea) {
|
||||
return this._innerArea.max[1];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return this._unrotatedAABB.max[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the internal width of the object according to its children.
|
||||
*/
|
||||
getUnscaledWidth(): float {
|
||||
if (this._forcedDefaultSize) {
|
||||
return this._forcedDefaultSize.max[0] - this._forcedDefaultSize.min[0];
|
||||
if (this._innerArea) {
|
||||
return this._innerArea.max[0] - this._innerArea.min[0];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
@@ -452,8 +506,8 @@ namespace gdjs {
|
||||
* @return the internal height of the object according to its children.
|
||||
*/
|
||||
getUnscaledHeight(): float {
|
||||
if (this._forcedDefaultSize) {
|
||||
return this._forcedDefaultSize.max[1] - this._forcedDefaultSize.min[1];
|
||||
if (this._innerArea) {
|
||||
return this._innerArea.max[1] - this._innerArea.min[1];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
@@ -468,10 +522,8 @@ namespace gdjs {
|
||||
if (this._customCenter) {
|
||||
return this._customCenter[0];
|
||||
}
|
||||
if (this._forcedDefaultSize) {
|
||||
return (
|
||||
(this._forcedDefaultSize.min[0] + this._forcedDefaultSize.max[0]) / 2
|
||||
);
|
||||
if (this._innerArea) {
|
||||
return (this._innerArea.min[0] + this._innerArea.max[0]) / 2;
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
@@ -486,10 +538,8 @@ namespace gdjs {
|
||||
if (this._customCenter) {
|
||||
return this._customCenter[1];
|
||||
}
|
||||
if (this._forcedDefaultSize) {
|
||||
return (
|
||||
(this._forcedDefaultSize.min[1] + this._forcedDefaultSize.max[1]) / 2
|
||||
);
|
||||
if (this._innerArea) {
|
||||
return (this._innerArea.min[1] + this._innerArea.max[1]) / 2;
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
@@ -547,15 +597,29 @@ namespace gdjs {
|
||||
|
||||
setWidth(newWidth: float): void {
|
||||
const unscaledWidth = this.getUnscaledWidth();
|
||||
if (unscaledWidth !== 0) {
|
||||
this.setScaleX(newWidth / unscaledWidth);
|
||||
if (unscaledWidth === 0) {
|
||||
return;
|
||||
}
|
||||
const scaleX = newWidth / unscaledWidth;
|
||||
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
|
||||
this._innerArea.min[0] *= scaleX;
|
||||
this._innerArea.max[0] *= scaleX;
|
||||
} else {
|
||||
this.setScaleX(scaleX);
|
||||
}
|
||||
}
|
||||
|
||||
setHeight(newHeight: float): void {
|
||||
const unscaledHeight = this.getUnscaledHeight();
|
||||
if (unscaledHeight !== 0) {
|
||||
this.setScaleY(newHeight / unscaledHeight);
|
||||
if (unscaledHeight === 0) {
|
||||
return;
|
||||
}
|
||||
const scaleY = newHeight / unscaledHeight;
|
||||
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
|
||||
this._innerArea.min[1] *= scaleY;
|
||||
this._innerArea.max[1] *= scaleY;
|
||||
} else {
|
||||
this.setScaleY(scaleY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,6 +670,10 @@ namespace gdjs {
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScale(newScale: float): void {
|
||||
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
|
||||
// The scale is always 1;
|
||||
return;
|
||||
}
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
@@ -628,6 +696,10 @@ namespace gdjs {
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleX(newScale: float): void {
|
||||
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
|
||||
// The scale is always 1;
|
||||
return;
|
||||
}
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
@@ -646,6 +718,10 @@ namespace gdjs {
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleY(newScale: float): void {
|
||||
if (this._innerArea && this._isInnerAreaFollowingParentSize) {
|
||||
// The scale is always 1;
|
||||
return;
|
||||
}
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
|
@@ -4,8 +4,6 @@
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('CustomRuntimeObject');
|
||||
|
||||
/**
|
||||
* The instance container of a custom object, containing instances of objects rendered on screen.
|
||||
*
|
||||
@@ -19,6 +17,17 @@ namespace gdjs {
|
||||
/** The object that is built with the instances of this container. */
|
||||
_customObject: gdjs.CustomRuntimeObject;
|
||||
_isLoaded: boolean = false;
|
||||
/**
|
||||
* The default size defined by users in the custom object initial instances editor.
|
||||
*
|
||||
* Don't modify it as it would affect every instance.
|
||||
*
|
||||
* @see gdjs.CustomRuntimeObject._innerArea
|
||||
**/
|
||||
private _initialInnerArea: {
|
||||
min: [float, float, float];
|
||||
max: [float, float, float];
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* @param parent the parent container that contains the object associated
|
||||
@@ -54,18 +63,15 @@ namespace gdjs {
|
||||
* @param customObjectData An object containing the container data.
|
||||
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
|
||||
*/
|
||||
loadFrom(customObjectData: ObjectData & CustomObjectConfiguration) {
|
||||
loadFrom(
|
||||
customObjectData: ObjectData & CustomObjectConfiguration,
|
||||
eventsBasedObjectData: EventsBasedObjectData
|
||||
) {
|
||||
if (this._isLoaded) {
|
||||
this.onDestroyFromScene(this._parent);
|
||||
}
|
||||
|
||||
const eventsBasedObjectData = this._runtimeScene
|
||||
.getGame()
|
||||
.getEventsBasedObjectData(customObjectData.type);
|
||||
if (!eventsBasedObjectData) {
|
||||
logger.error('loadFrom was called without an events-based object');
|
||||
return;
|
||||
}
|
||||
this._setOriginalInnerArea(eventsBasedObjectData);
|
||||
|
||||
// Registering objects
|
||||
for (
|
||||
@@ -74,16 +80,26 @@ namespace gdjs {
|
||||
++i
|
||||
) {
|
||||
const childObjectData = eventsBasedObjectData.objects[i];
|
||||
this.registerObject({
|
||||
...childObjectData,
|
||||
...customObjectData.childrenContent[childObjectData.name],
|
||||
});
|
||||
if (customObjectData.childrenContent) {
|
||||
this.registerObject({
|
||||
...childObjectData,
|
||||
// The custom object overrides its events-based object configuration.
|
||||
...customObjectData.childrenContent[childObjectData.name],
|
||||
});
|
||||
} else {
|
||||
// The custom object follows its events-based object configuration.
|
||||
this.registerObject(childObjectData);
|
||||
}
|
||||
}
|
||||
|
||||
if (customObjectData.layers.length > 0) {
|
||||
if (eventsBasedObjectData.layers.length > 0) {
|
||||
// Load layers
|
||||
for (let i = 0, len = customObjectData.layers.length; i < len; ++i) {
|
||||
this.addLayer(customObjectData.layers[i]);
|
||||
for (
|
||||
let i = 0, len = eventsBasedObjectData.layers.length;
|
||||
i < len;
|
||||
++i
|
||||
) {
|
||||
this.addLayer(eventsBasedObjectData.layers[i]);
|
||||
}
|
||||
} else {
|
||||
// Add a default layer
|
||||
@@ -112,7 +128,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
this.createObjectsFrom(
|
||||
customObjectData.instances,
|
||||
eventsBasedObjectData.instances,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -127,48 +143,29 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the container must be updated using the specified
|
||||
* objectData. This is the case during hot-reload, and is only called if
|
||||
* the object was modified.
|
||||
*
|
||||
* @param oldCustomObjectData The previous data for the object.
|
||||
* @param newCustomObjectData The new data for the object.
|
||||
* @returns true if the object was updated, false if it could not
|
||||
* (i.e: hot-reload is not supported).
|
||||
* Initialize `_initialInnerArea` if it doesn't exist.
|
||||
* `_initialInnerArea` is shared by every instance to save memory.
|
||||
*/
|
||||
updateFrom(
|
||||
oldCustomObjectData: ObjectData & CustomObjectConfiguration,
|
||||
newCustomObjectData: ObjectData & CustomObjectConfiguration
|
||||
): boolean {
|
||||
const eventsBasedObjectData = this._runtimeScene
|
||||
.getGame()
|
||||
.getEventsBasedObjectData(newCustomObjectData.type);
|
||||
if (!eventsBasedObjectData) {
|
||||
logger.error('updateFrom was called without an events-based object');
|
||||
return false;
|
||||
}
|
||||
|
||||
for (
|
||||
let i = 0, len = eventsBasedObjectData.objects.length;
|
||||
i < len;
|
||||
++i
|
||||
) {
|
||||
const childName = eventsBasedObjectData.objects[i].name;
|
||||
const oldChildData = {
|
||||
...eventsBasedObjectData.objects[i],
|
||||
...oldCustomObjectData.childrenContent[childName],
|
||||
};
|
||||
const newChildData = {
|
||||
...eventsBasedObjectData.objects[i],
|
||||
...newCustomObjectData.childrenContent[childName],
|
||||
};
|
||||
this.updateObject(newChildData);
|
||||
|
||||
for (const child of this.getInstancesOf(childName)) {
|
||||
child.updateFromObjectData(oldChildData, newChildData);
|
||||
private _setOriginalInnerArea(
|
||||
eventsBasedObjectData: EventsBasedObjectData
|
||||
) {
|
||||
if (eventsBasedObjectData.instances.length > 0) {
|
||||
if (!eventsBasedObjectData._initialInnerArea) {
|
||||
eventsBasedObjectData._initialInnerArea = {
|
||||
min: [
|
||||
eventsBasedObjectData.areaMinX,
|
||||
eventsBasedObjectData.areaMinY,
|
||||
eventsBasedObjectData.areaMinZ,
|
||||
],
|
||||
max: [
|
||||
eventsBasedObjectData.areaMaxX,
|
||||
eventsBasedObjectData.areaMaxY,
|
||||
eventsBasedObjectData.areaMaxZ,
|
||||
],
|
||||
};
|
||||
}
|
||||
this._initialInnerArea = eventsBasedObjectData._initialInnerArea;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,8 +184,10 @@ namespace gdjs {
|
||||
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
|
||||
const object = allInstancesList[i];
|
||||
object.onDeletedFromScene(this);
|
||||
// The object can free all its resource directly...
|
||||
object.onDestroyed();
|
||||
}
|
||||
|
||||
// ...as its container cache `_instancesRemoved` is also destroy.
|
||||
this._destroy();
|
||||
|
||||
this._isLoaded = false;
|
||||
@@ -291,6 +290,38 @@ namespace gdjs {
|
||||
return this._runtimeScene;
|
||||
}
|
||||
|
||||
getUnrotatedViewportMinX(): float {
|
||||
return this._customObject.getInnerAreaMinX();
|
||||
}
|
||||
|
||||
getUnrotatedViewportMinY(): float {
|
||||
return this._customObject.getInnerAreaMinY();
|
||||
}
|
||||
|
||||
getUnrotatedViewportMaxX(): float {
|
||||
return this._customObject.getInnerAreaMaxX();
|
||||
}
|
||||
|
||||
getUnrotatedViewportMaxY(): float {
|
||||
return this._customObject.getInnerAreaMaxY();
|
||||
}
|
||||
|
||||
getInitialUnrotatedViewportMinX(): float {
|
||||
return this._initialInnerArea ? this._initialInnerArea.min[0] : 0;
|
||||
}
|
||||
|
||||
getInitialUnrotatedViewportMinY(): float {
|
||||
return this._initialInnerArea ? this._initialInnerArea.min[1] : 0;
|
||||
}
|
||||
|
||||
getInitialUnrotatedViewportMaxX(): float {
|
||||
return this._initialInnerArea ? this._initialInnerArea.max[0] : 0;
|
||||
}
|
||||
|
||||
getInitialUnrotatedViewportMaxY(): float {
|
||||
return this._initialInnerArea ? this._initialInnerArea.max[1] : 0;
|
||||
}
|
||||
|
||||
getViewportWidth(): float {
|
||||
return this._customObject.getUnscaledWidth();
|
||||
}
|
||||
|
@@ -102,6 +102,70 @@ namespace gdjs {
|
||||
result: FloatPoint
|
||||
): FloatPoint;
|
||||
|
||||
/**
|
||||
* @return the left bound of:
|
||||
* - the game resolution for a {@link gdjs.RuntimeScene}
|
||||
* - the default dimensions (the AABB of all its children) for a
|
||||
* {@link gdjs.CustomRuntimeObject}.
|
||||
*/
|
||||
abstract getUnrotatedViewportMinX(): float;
|
||||
|
||||
/**
|
||||
* @return the top bound of:
|
||||
* - the game resolution for a {@link gdjs.RuntimeScene}
|
||||
* - the default dimensions (the AABB of all its children) for a
|
||||
* {@link gdjs.CustomRuntimeObject}.
|
||||
*/
|
||||
abstract getUnrotatedViewportMinY(): float;
|
||||
|
||||
/**
|
||||
* @return the right bound of:
|
||||
* - the game resolution for a {@link gdjs.RuntimeScene}
|
||||
* - the default dimensions (the AABB of all its children) for a
|
||||
* {@link gdjs.CustomRuntimeObject}.
|
||||
*/
|
||||
abstract getUnrotatedViewportMaxX(): float;
|
||||
|
||||
/**
|
||||
* @return the bottom bound of:
|
||||
* - the game resolution for a {@link gdjs.RuntimeScene}
|
||||
* - the default dimensions (the AABB of all its children) for a
|
||||
* {@link gdjs.CustomRuntimeObject}.
|
||||
*/
|
||||
abstract getUnrotatedViewportMaxY(): float;
|
||||
|
||||
/**
|
||||
* @return the left bound of:
|
||||
* - the initial game resolution for a {@link gdjs.RuntimeScene}
|
||||
* - the initial default dimensions (inner area) set in the editor for a
|
||||
* {@link gdjs.CustomRuntimeObject}.
|
||||
*/
|
||||
abstract getInitialUnrotatedViewportMinX(): float;
|
||||
|
||||
/**
|
||||
* @return the top bound of:
|
||||
* - the initial game resolution for a {@link gdjs.RuntimeScene}
|
||||
* - the initial default dimensions (inner area) set in the editor for a
|
||||
* {@link gdjs.CustomRuntimeObject}.
|
||||
*/
|
||||
abstract getInitialUnrotatedViewportMinY(): float;
|
||||
|
||||
/**
|
||||
* @return the right bound of:
|
||||
* - the initial game resolution for a {@link gdjs.RuntimeScene}
|
||||
* - the initial default dimensions (inner area) set in the editor for a
|
||||
* {@link gdjs.CustomRuntimeObject}.
|
||||
*/
|
||||
abstract getInitialUnrotatedViewportMaxX(): float;
|
||||
|
||||
/**
|
||||
* @return the bottom bound of:
|
||||
* - the initial game resolution for a {@link gdjs.RuntimeScene}
|
||||
* - the initial default dimensions (inner area) set in the editor for a
|
||||
* {@link gdjs.CustomRuntimeObject}.
|
||||
*/
|
||||
abstract getInitialUnrotatedViewportMaxY(): float;
|
||||
|
||||
/**
|
||||
* @return the width of:
|
||||
* - the game resolution for a {@link gdjs.RuntimeScene}
|
||||
|
@@ -123,6 +123,9 @@ namespace gdjs {
|
||||
frameData: SpriteFrameData,
|
||||
textureManager: gdjs.AnimationFrameTextureManager<T>
|
||||
) {
|
||||
this.image = frameData.image;
|
||||
this.texture = textureManager.getAnimationFrameTexture(this.image);
|
||||
|
||||
this.points.clear();
|
||||
for (let i = 0, len = frameData.points.length; i < len; ++i) {
|
||||
const ptData = frameData.points[i];
|
||||
|
@@ -79,14 +79,6 @@ namespace gdjs {
|
||||
},
|
||||
};
|
||||
|
||||
const isErrorComingFromJavaScriptCode = (
|
||||
exception: Error | null
|
||||
): boolean => {
|
||||
if (!exception || !exception.stack) return false;
|
||||
|
||||
return exception.stack.includes('GDJSInlineCode');
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays uncaught exceptions on top of the game.
|
||||
* Could be reworked in the future to support a minimal debugger inside the game.
|
||||
@@ -120,7 +112,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
if (this._uncaughtException) {
|
||||
const errorIsInJs = isErrorComingFromJavaScriptCode(
|
||||
const errorIsInJs = gdjs.AbstractDebuggerClient.isErrorComingFromJavaScriptCode(
|
||||
this._uncaughtException
|
||||
);
|
||||
this._uncaughtExceptionElement = (
|
||||
|
@@ -88,6 +88,69 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
/** Replacer function for JSON.stringify to convert Error objects into plain objects that can be logged. */
|
||||
const errorReplacer = (_, value: any) => {
|
||||
if (value instanceof Error) {
|
||||
// See https://stackoverflow.com/questions/18391212/is-it-not-possible-to-stringify-an-error-using-json-stringify
|
||||
const errorObject = {};
|
||||
Object.getOwnPropertyNames(value).forEach((prop) => {
|
||||
errorObject[prop] = value[prop];
|
||||
});
|
||||
|
||||
return errorObject;
|
||||
}
|
||||
// Return the value unchanged if it's not an Error object.
|
||||
return value;
|
||||
};
|
||||
|
||||
const buildGameCrashReport = (
|
||||
exception: Error,
|
||||
runtimeGame: gdjs.RuntimeGame
|
||||
) => {
|
||||
const sceneNames = runtimeGame.getSceneStack().getAllSceneNames();
|
||||
const currentScene = runtimeGame.getSceneStack().getCurrentScene();
|
||||
return {
|
||||
type: 'javascript-uncaught-exception',
|
||||
exception,
|
||||
platformInfo: runtimeGame.getPlatformInfo(),
|
||||
playerId: runtimeGame.getPlayerId(),
|
||||
sessionId: runtimeGame.getSessionId(),
|
||||
isPreview: runtimeGame.isPreview(),
|
||||
gdevelop: {
|
||||
previewContext: runtimeGame.getAdditionalOptions().previewContext,
|
||||
isNativeMobileApp: runtimeGame.getAdditionalOptions().nativeMobileApp,
|
||||
versionWithHash: runtimeGame.getAdditionalOptions()
|
||||
.gdevelopVersionWithHash,
|
||||
environment: runtimeGame.getAdditionalOptions().environment,
|
||||
},
|
||||
game: {
|
||||
gameId: gdjs.projectData.properties.projectUuid,
|
||||
name: runtimeGame.getGameData().properties.name || '',
|
||||
packageName: runtimeGame.getGameData().properties.packageName || '',
|
||||
version: runtimeGame.getGameData().properties.version || '',
|
||||
location: window.location.href,
|
||||
projectTemplateSlug: runtimeGame.getAdditionalOptions()
|
||||
.projectTemplateSlug,
|
||||
sourceGameId: runtimeGame.getAdditionalOptions().sourceGameId,
|
||||
},
|
||||
gameState: {
|
||||
sceneNames,
|
||||
isWebGLSupported: runtimeGame.getRenderer().isWebGLSupported(),
|
||||
hasPixiRenderer: !!runtimeGame.getRenderer().getPIXIRenderer(),
|
||||
hasThreeRenderer: !!runtimeGame.getRenderer().getThreeRenderer(),
|
||||
resourcesTotalCount: runtimeGame.getGameData().resources.resources
|
||||
.length,
|
||||
antialiasingMode: runtimeGame.getAntialiasingMode(),
|
||||
isAntialisingEnabledOnMobile: runtimeGame.isAntialisingEnabledOnMobile(),
|
||||
scriptFiles: runtimeGame.getAdditionalOptions().scriptFiles,
|
||||
currentSceneTimeFromStart: currentScene
|
||||
? currentScene.getTimeManager().getTimeFromStart()
|
||||
: null,
|
||||
gdjsKeys: Object.keys(gdjs).slice(0, 1000),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The base class describing a debugger client, that can be used to inspect
|
||||
* a runtime game (dump its state) or alter it.
|
||||
@@ -98,6 +161,8 @@ namespace gdjs {
|
||||
_originalConsole = originalConsole;
|
||||
_inGameDebugger: gdjs.InGameDebugger;
|
||||
|
||||
_hasLoggedUncaughtException = false;
|
||||
|
||||
constructor(runtimeGame: RuntimeGame) {
|
||||
this._runtimegame = runtimeGame;
|
||||
this._hotReloader = new gdjs.HotReloader(runtimeGame);
|
||||
@@ -213,10 +278,68 @@ namespace gdjs {
|
||||
*/
|
||||
protected abstract _sendMessage(message: string): void;
|
||||
|
||||
static isErrorComingFromJavaScriptCode(exception: Error | null): boolean {
|
||||
if (!exception || !exception.stack) return false;
|
||||
|
||||
return exception.stack.includes('GDJSInlineCode');
|
||||
}
|
||||
|
||||
async _reportCrash(exception: Error) {
|
||||
const gameCrashReport = buildGameCrashReport(
|
||||
exception,
|
||||
this._runtimegame
|
||||
);
|
||||
|
||||
// Let a debugger server know about the crash.
|
||||
this._sendMessage(
|
||||
circularSafeStringify(
|
||||
{
|
||||
command: 'game.crashed',
|
||||
payload: gameCrashReport,
|
||||
},
|
||||
errorReplacer
|
||||
)
|
||||
);
|
||||
|
||||
// Send the report to the APIs, if allowed.
|
||||
if (
|
||||
!this._runtimegame.getAdditionalOptions().crashReportUploadLevel ||
|
||||
this._runtimegame.getAdditionalOptions().crashReportUploadLevel ===
|
||||
'none' ||
|
||||
(this._runtimegame.getAdditionalOptions().crashReportUploadLevel ===
|
||||
'exclude-javascript-code-events' &&
|
||||
AbstractDebuggerClient.isErrorComingFromJavaScriptCode(exception))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootApi = this._runtimegame.isUsingGDevelopDevelopmentEnvironment()
|
||||
? 'https://api-dev.gdevelop.io'
|
||||
: 'https://api.gdevelop.io';
|
||||
const baseUrl = `${rootApi}/analytics`;
|
||||
|
||||
try {
|
||||
await fetch(`${baseUrl}/game-crash-report`, {
|
||||
body: circularSafeStringify(gameCrashReport, errorReplacer),
|
||||
method: 'POST',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error while sending the crash report:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onUncaughtException(exception: Error): void {
|
||||
logger.error('Uncaught exception: ' + exception);
|
||||
|
||||
this._inGameDebugger.setUncaughtException(exception);
|
||||
|
||||
if (!this._hasLoggedUncaughtException) {
|
||||
// Only log an uncaught exception once, to avoid spamming the debugger server
|
||||
// in case of an exception at each frame.
|
||||
this._hasLoggedUncaughtException = true;
|
||||
|
||||
this._reportCrash(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -41,6 +41,17 @@ namespace gdjs {
|
||||
}, {});
|
||||
}
|
||||
|
||||
static indexByName<E extends { name: string | null }>(
|
||||
objectsWithName: E[]
|
||||
): Map<string, E> {
|
||||
return objectsWithName.reduce(function (objectsMap, object) {
|
||||
if (object.name) {
|
||||
objectsMap.set(object.name, object);
|
||||
}
|
||||
return objectsMap;
|
||||
}, new Map<string, E>());
|
||||
}
|
||||
|
||||
_canReloadScriptFile(srcFilename: string): boolean {
|
||||
function endsWith(str: string, suffix: string): boolean {
|
||||
const suffixPosition = str.indexOf(suffix);
|
||||
@@ -193,19 +204,22 @@ namespace gdjs {
|
||||
if (errorTarget instanceof HTMLScriptElement) {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message: 'Unable to reload script:' + errorTarget.src,
|
||||
message: 'Unable to reload script: ' + errorTarget.src,
|
||||
});
|
||||
} else {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message:
|
||||
'Unexpected error happened while hot-reloading:' +
|
||||
'Unexpected error happened while hot-reloading: ' +
|
||||
error.message,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info('Hot reload finished with logs:', this._logs);
|
||||
logger.info(
|
||||
'Hot reload finished with logs:',
|
||||
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
|
||||
);
|
||||
this._runtimeGame.pause(false);
|
||||
return this._logs;
|
||||
});
|
||||
@@ -338,28 +352,90 @@ namespace gdjs {
|
||||
runtimeGame.getVariables()
|
||||
);
|
||||
|
||||
// Update extension's global variables.
|
||||
for (const newExtensionData of newProjectData.eventsFunctionsExtensions) {
|
||||
const oldExtensionData = oldProjectData.eventsFunctionsExtensions.find(
|
||||
(oldExtensionData) => oldExtensionData.name === newExtensionData.name
|
||||
);
|
||||
|
||||
const oldGlobalVariables = oldExtensionData
|
||||
? oldExtensionData.globalVariables
|
||||
: [];
|
||||
const newGlobalVariables = newExtensionData.globalVariables;
|
||||
|
||||
if (oldGlobalVariables.length > 0 || newGlobalVariables.length > 0) {
|
||||
const currentVariables = runtimeGame.getVariablesForExtension(
|
||||
newExtensionData.name
|
||||
);
|
||||
if (currentVariables) {
|
||||
this._hotReloadVariablesContainer(
|
||||
oldGlobalVariables,
|
||||
newGlobalVariables,
|
||||
currentVariables
|
||||
);
|
||||
} else {
|
||||
runtimeGame._variablesByExtensionName.set(
|
||||
newExtensionData.name,
|
||||
new gdjs.VariablesContainer(newGlobalVariables)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldlayoutDataMap = HotReloader.indexByName(oldProjectData.layouts);
|
||||
const newlayoutDataMap = HotReloader.indexByName(newProjectData.layouts);
|
||||
|
||||
// Reload runtime scenes
|
||||
sceneStack._stack.forEach((runtimeScene) => {
|
||||
const oldLayoutData = oldProjectData.layouts.filter(
|
||||
(layoutData) => layoutData.name === runtimeScene.getName()
|
||||
)[0];
|
||||
const newLayoutData = newProjectData.layouts.filter(
|
||||
(layoutData) => layoutData.name === runtimeScene.getName()
|
||||
)[0];
|
||||
const oldLayoutData = oldlayoutDataMap.get(runtimeScene.getName());
|
||||
const newLayoutData = newlayoutDataMap.get(runtimeScene.getName());
|
||||
if (oldLayoutData && newLayoutData) {
|
||||
this._hotReloadRuntimeScene(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
oldLayoutData,
|
||||
newLayoutData,
|
||||
changedRuntimeBehaviors,
|
||||
runtimeScene
|
||||
);
|
||||
|
||||
// Update extension's scene variables.
|
||||
for (const newExtensionData of newProjectData.eventsFunctionsExtensions) {
|
||||
const oldExtensionData = oldProjectData.eventsFunctionsExtensions.find(
|
||||
(oldExtensionData) =>
|
||||
oldExtensionData.name === newExtensionData.name
|
||||
);
|
||||
|
||||
const oldSceneVariables = oldExtensionData
|
||||
? oldExtensionData.sceneVariables
|
||||
: [];
|
||||
const newSceneVariables = newExtensionData.sceneVariables;
|
||||
|
||||
if (oldSceneVariables.length > 0 || newSceneVariables.length > 0) {
|
||||
const currentVariables = runtimeScene.getVariablesForExtension(
|
||||
newExtensionData.name
|
||||
);
|
||||
if (currentVariables) {
|
||||
this._hotReloadVariablesContainer(
|
||||
oldSceneVariables,
|
||||
newSceneVariables,
|
||||
currentVariables
|
||||
);
|
||||
} else {
|
||||
runtimeScene._variablesByExtensionName.set(
|
||||
newExtensionData.name,
|
||||
new gdjs.VariablesContainer(newSceneVariables)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// A scene was removed. Not hot-reloading this.
|
||||
this._logs.push({
|
||||
kind: 'error',
|
||||
message:
|
||||
'Scene ' +
|
||||
oldLayoutData.name +
|
||||
(oldLayoutData && oldLayoutData.name) +
|
||||
' was removed. A fresh preview should be launched.',
|
||||
});
|
||||
}
|
||||
@@ -377,8 +453,20 @@ namespace gdjs {
|
||||
// hot-reload all the scenes.
|
||||
!HotReloader.deepEqual(oldExternalLayoutData, newExternalLayoutData)
|
||||
) {
|
||||
const oldLayoutData = oldlayoutDataMap.get(
|
||||
oldExternalLayoutData.associatedLayout
|
||||
);
|
||||
const newLayoutData = newlayoutDataMap.get(
|
||||
newExternalLayoutData.associatedLayout
|
||||
);
|
||||
|
||||
sceneStack._stack.forEach((runtimeScene) => {
|
||||
this._hotReloadRuntimeSceneInstances(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
oldLayoutData ? oldLayoutData.objects : [],
|
||||
newLayoutData ? newLayoutData.objects : [],
|
||||
oldExternalLayoutData.instances,
|
||||
newExternalLayoutData.instances,
|
||||
runtimeScene
|
||||
@@ -455,6 +543,7 @@ namespace gdjs {
|
||||
variablesContainer.remove(oldVariableData.name);
|
||||
}
|
||||
});
|
||||
variablesContainer.rebuildIndexFrom(newVariablesData);
|
||||
}
|
||||
|
||||
_hotReloadStructureVariable(
|
||||
@@ -535,6 +624,8 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
_hotReloadRuntimeScene(
|
||||
oldProjectData: ProjectData,
|
||||
newProjectData: ProjectData,
|
||||
oldLayoutData: LayoutData,
|
||||
newLayoutData: LayoutData,
|
||||
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
|
||||
@@ -562,25 +653,12 @@ namespace gdjs {
|
||||
runtimeScene
|
||||
);
|
||||
|
||||
// Re-instantiate any gdjs.RuntimeBehavior that was changed.
|
||||
this._reinstantiateRuntimeSceneRuntimeBehaviors(
|
||||
this._hotReloadRuntimeInstanceContainer(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
oldLayoutData,
|
||||
newLayoutData,
|
||||
changedRuntimeBehaviors,
|
||||
newLayoutData.objects,
|
||||
runtimeScene
|
||||
);
|
||||
this._hotReloadRuntimeSceneObjects(
|
||||
oldLayoutData.objects,
|
||||
newLayoutData.objects,
|
||||
runtimeScene
|
||||
);
|
||||
this._hotReloadRuntimeSceneInstances(
|
||||
oldLayoutData.instances,
|
||||
newLayoutData.instances,
|
||||
runtimeScene
|
||||
);
|
||||
this._hotReloadRuntimeSceneLayers(
|
||||
oldLayoutData.layers,
|
||||
newLayoutData.layers,
|
||||
runtimeScene
|
||||
);
|
||||
|
||||
@@ -591,6 +669,101 @@ namespace gdjs {
|
||||
runtimeScene.setEventsGeneratedCodeFunction(newLayoutData);
|
||||
}
|
||||
|
||||
static resolveCustomObjectConfigurations(
|
||||
projectData: ProjectData,
|
||||
objectDatas: ObjectData[]
|
||||
): ObjectData[] {
|
||||
return objectDatas.map((objectData) => {
|
||||
const [extensionName, eventsBasedObjectName] = objectData.type.split(
|
||||
'::'
|
||||
);
|
||||
|
||||
const extensionData = projectData.eventsFunctionsExtensions.find(
|
||||
(extension) => extension.name === extensionName
|
||||
);
|
||||
if (!extensionData) {
|
||||
return objectData;
|
||||
}
|
||||
|
||||
const eventsBasedObjectData =
|
||||
extensionData &&
|
||||
extensionData.eventsBasedObjects.find(
|
||||
(object) => object.name === eventsBasedObjectName
|
||||
);
|
||||
if (!eventsBasedObjectData) {
|
||||
return objectData;
|
||||
}
|
||||
|
||||
const customObjectConfiguration = objectData as ObjectData &
|
||||
CustomObjectConfiguration;
|
||||
|
||||
const mergedChildObjectDataList = customObjectConfiguration.childrenContent
|
||||
? eventsBasedObjectData.objects.map((objectData) => ({
|
||||
...objectData,
|
||||
...customObjectConfiguration.childrenContent[objectData.name],
|
||||
}))
|
||||
: eventsBasedObjectData.objects;
|
||||
|
||||
const mergedObjectConfiguration = {
|
||||
...eventsBasedObjectData,
|
||||
...objectData,
|
||||
// ObjectData doesn't have an `objects` attribute.
|
||||
// This is a small optimization to avoid to create an
|
||||
// InstanceContainerData for each instance to hot-reload their inner
|
||||
// scene (see `_hotReloadRuntimeInstanceContainer` call from
|
||||
// `_hotReloadRuntimeSceneInstances`).
|
||||
objects: mergedChildObjectDataList,
|
||||
childrenContent: mergedChildObjectDataList,
|
||||
};
|
||||
return mergedObjectConfiguration;
|
||||
});
|
||||
}
|
||||
|
||||
_hotReloadRuntimeInstanceContainer(
|
||||
oldProjectData: ProjectData,
|
||||
newProjectData: ProjectData,
|
||||
oldLayoutData: InstanceContainerData,
|
||||
newLayoutData: InstanceContainerData,
|
||||
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
const oldObjectDataList = HotReloader.resolveCustomObjectConfigurations(
|
||||
oldProjectData,
|
||||
oldLayoutData.objects
|
||||
);
|
||||
const newObjectDataList = HotReloader.resolveCustomObjectConfigurations(
|
||||
newProjectData,
|
||||
newLayoutData.objects
|
||||
);
|
||||
|
||||
// Re-instantiate any gdjs.RuntimeBehavior that was changed.
|
||||
this._reinstantiateRuntimeSceneRuntimeBehaviors(
|
||||
changedRuntimeBehaviors,
|
||||
newObjectDataList,
|
||||
runtimeInstanceContainer
|
||||
);
|
||||
this._hotReloadRuntimeSceneObjects(
|
||||
oldObjectDataList,
|
||||
newObjectDataList,
|
||||
runtimeInstanceContainer
|
||||
);
|
||||
this._hotReloadRuntimeSceneInstances(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
oldObjectDataList,
|
||||
newObjectDataList,
|
||||
oldLayoutData.instances,
|
||||
newLayoutData.instances,
|
||||
runtimeInstanceContainer
|
||||
);
|
||||
this._hotReloadRuntimeSceneLayers(
|
||||
oldLayoutData.layers,
|
||||
newLayoutData.layers,
|
||||
runtimeInstanceContainer
|
||||
);
|
||||
}
|
||||
|
||||
_hotReloadRuntimeSceneBehaviorsSharedData(
|
||||
oldBehaviorsSharedData: BehaviorSharedData[],
|
||||
newBehaviorsSharedData: BehaviorSharedData[],
|
||||
@@ -637,12 +810,12 @@ namespace gdjs {
|
||||
_reinstantiateRuntimeSceneRuntimeBehaviors(
|
||||
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
|
||||
newObjects: ObjectData[],
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
newObjects.forEach((newObjectData) => {
|
||||
const objectName = newObjectData.name;
|
||||
const newBehaviors = newObjectData.behaviors;
|
||||
const runtimeObjects = runtimeScene.getObjects(objectName)!;
|
||||
const runtimeObjects = runtimeInstanceContainer.getObjects(objectName)!;
|
||||
changedRuntimeBehaviors.forEach((changedRuntimeBehavior) => {
|
||||
const behaviorTypeName = changedRuntimeBehavior.behaviorTypeName;
|
||||
|
||||
@@ -727,7 +900,7 @@ namespace gdjs {
|
||||
_hotReloadRuntimeSceneObjects(
|
||||
oldObjects: ObjectData[],
|
||||
newObjects: ObjectData[],
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
oldObjects.forEach((oldObjectData) => {
|
||||
const name = oldObjectData.name;
|
||||
@@ -742,13 +915,13 @@ namespace gdjs {
|
||||
// its name (it's not expected to change).
|
||||
if (!newObjectData || oldObjectData.type !== newObjectData.type) {
|
||||
// Object was removed or object type was changed (considered as a removal of the old object)
|
||||
runtimeScene.unregisterObject(name);
|
||||
runtimeInstanceContainer.unregisterObject(name);
|
||||
} else {
|
||||
if (runtimeScene.isObjectRegistered(name)) {
|
||||
if (runtimeInstanceContainer.isObjectRegistered(name)) {
|
||||
this._hotReloadRuntimeSceneObject(
|
||||
oldObjectData,
|
||||
newObjectData,
|
||||
runtimeScene
|
||||
runtimeInstanceContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -760,10 +933,10 @@ namespace gdjs {
|
||||
)[0];
|
||||
if (
|
||||
(!oldObjectData || oldObjectData.type !== newObjectData.type) &&
|
||||
!runtimeScene.isObjectRegistered(name)
|
||||
!runtimeInstanceContainer.isObjectRegistered(name)
|
||||
) {
|
||||
// Object was added or object type was changed (considered as adding the new object)
|
||||
runtimeScene.registerObject(newObjectData);
|
||||
runtimeInstanceContainer.registerObject(newObjectData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -771,7 +944,7 @@ namespace gdjs {
|
||||
_hotReloadRuntimeSceneObject(
|
||||
oldObjectData: ObjectData,
|
||||
newObjectData: ObjectData,
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
let hotReloadSucceeded = true;
|
||||
if (!HotReloader.deepEqual(oldObjectData, newObjectData)) {
|
||||
@@ -784,10 +957,12 @@ namespace gdjs {
|
||||
});
|
||||
|
||||
// Register the updated object data, used for new instances.
|
||||
runtimeScene.updateObject(newObjectData);
|
||||
runtimeInstanceContainer.updateObject(newObjectData);
|
||||
|
||||
// Update existing instances
|
||||
const runtimeObjects = runtimeScene.getObjects(newObjectData.name)!;
|
||||
const runtimeObjects = runtimeInstanceContainer.getObjects(
|
||||
newObjectData.name
|
||||
)!;
|
||||
|
||||
// Update instances state
|
||||
runtimeObjects.forEach((runtimeObject) => {
|
||||
@@ -1005,7 +1180,7 @@ namespace gdjs {
|
||||
_hotReloadRuntimeSceneLayers(
|
||||
oldLayers: LayerData[],
|
||||
newLayers: LayerData[],
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
oldLayers.forEach((oldLayerData) => {
|
||||
const name = oldLayerData.name;
|
||||
@@ -1014,10 +1189,10 @@ namespace gdjs {
|
||||
)[0];
|
||||
if (!newLayerData) {
|
||||
// Layer was removed
|
||||
runtimeScene.removeLayer(name);
|
||||
runtimeInstanceContainer.removeLayer(name);
|
||||
} else {
|
||||
if (runtimeScene.hasLayer(name)) {
|
||||
const layer = runtimeScene.getLayer(name);
|
||||
if (runtimeInstanceContainer.hasLayer(name)) {
|
||||
const layer = runtimeInstanceContainer.getLayer(name);
|
||||
this._hotReloadRuntimeLayer(oldLayerData, newLayerData, layer);
|
||||
}
|
||||
}
|
||||
@@ -1027,13 +1202,13 @@ namespace gdjs {
|
||||
const oldLayerData = oldLayers.filter(
|
||||
(layerData) => layerData.name === name
|
||||
)[0];
|
||||
if (!oldLayerData && !runtimeScene.hasLayer(name)) {
|
||||
if (!oldLayerData && !runtimeInstanceContainer.hasLayer(name)) {
|
||||
// Layer was added
|
||||
runtimeScene.addLayer(newLayerData);
|
||||
runtimeInstanceContainer.addLayer(newLayerData);
|
||||
}
|
||||
});
|
||||
newLayers.forEach((newLayerData, index) => {
|
||||
runtimeScene.setLayerIndex(newLayerData.name, index);
|
||||
runtimeInstanceContainer.setLayerIndex(newLayerData.name, index);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1168,11 +1343,16 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
_hotReloadRuntimeSceneInstances(
|
||||
oldProjectData: ProjectData,
|
||||
newProjectData: ProjectData,
|
||||
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
|
||||
oldObjects: ObjectData[],
|
||||
newObjects: ObjectData[],
|
||||
oldInstances: InstanceData[],
|
||||
newInstances: InstanceData[],
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
const runtimeObjects = runtimeScene.getAdhocListOfAllInstances();
|
||||
const runtimeObjects = runtimeInstanceContainer.getAdhocListOfAllInstances();
|
||||
const groupedOldInstances: {
|
||||
[key: number]: InstanceData;
|
||||
} = HotReloader.groupByPersistentUuid(oldInstances);
|
||||
@@ -1182,30 +1362,73 @@ namespace gdjs {
|
||||
const groupedRuntimeObjects: {
|
||||
[key: number]: gdjs.RuntimeObject;
|
||||
} = HotReloader.groupByPersistentUuid(runtimeObjects);
|
||||
for (let persistentUuid in groupedOldInstances) {
|
||||
|
||||
const oldObjectsMap = HotReloader.indexByName(oldObjects);
|
||||
const newObjectsMap = HotReloader.indexByName(newObjects);
|
||||
|
||||
for (const persistentUuid in groupedOldInstances) {
|
||||
const oldInstance = groupedOldInstances[persistentUuid];
|
||||
const newInstance = groupedNewInstances[persistentUuid];
|
||||
const runtimeObject = groupedRuntimeObjects[persistentUuid];
|
||||
|
||||
if (
|
||||
oldInstance &&
|
||||
(!newInstance || oldInstance.name !== newInstance.name)
|
||||
) {
|
||||
// Instance was deleted (or object name changed, in which case it will be re-created later)
|
||||
if (runtimeObject) {
|
||||
runtimeObject.deleteFromScene(runtimeScene);
|
||||
runtimeObject.deleteFromScene(runtimeInstanceContainer);
|
||||
}
|
||||
} else {
|
||||
if (oldInstance && newInstance && runtimeObject) {
|
||||
// Instance was not deleted nor created, maybe modified (or not):
|
||||
this._hotReloadRuntimeInstance(
|
||||
oldInstance,
|
||||
newInstance,
|
||||
runtimeObject
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let persistentUuid in groupedNewInstances) {
|
||||
|
||||
for (const persistentUuid in groupedRuntimeObjects) {
|
||||
const runtimeObject = groupedRuntimeObjects[persistentUuid];
|
||||
const oldObjectData = oldObjectsMap.get(runtimeObject.getName());
|
||||
const newObjectData = newObjectsMap.get(runtimeObject.getName());
|
||||
if (!runtimeObject || !oldObjectData || !newObjectData) {
|
||||
// New objects or deleted objects can't have instances to hot-reload.
|
||||
continue;
|
||||
}
|
||||
const oldInstance = groupedOldInstances[persistentUuid];
|
||||
const newInstance = groupedNewInstances[persistentUuid];
|
||||
if (oldInstance && newInstance) {
|
||||
// Instance was not deleted nor created, maybe modified (or not):
|
||||
this._hotReloadRuntimeInstance(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
oldObjectData,
|
||||
newObjectData,
|
||||
oldInstance,
|
||||
newInstance,
|
||||
runtimeObject
|
||||
);
|
||||
} else if (runtimeObject instanceof gdjs.CustomRuntimeObject) {
|
||||
const childrenInstanceContainer = runtimeObject.getChildrenContainer();
|
||||
|
||||
// The `objects` attribute is already resolved by `resolveCustomObjectConfigurations()`.
|
||||
const oldCustomObjectData = oldObjectData as ObjectData &
|
||||
CustomObjectConfiguration &
|
||||
InstanceContainerData;
|
||||
const newCustomObjectData = newObjectData as ObjectData &
|
||||
CustomObjectConfiguration &
|
||||
InstanceContainerData;
|
||||
|
||||
// Reload the content of custom objects that were created at runtime.
|
||||
this._hotReloadRuntimeInstanceContainer(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
oldCustomObjectData,
|
||||
newCustomObjectData,
|
||||
changedRuntimeBehaviors,
|
||||
childrenInstanceContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const persistentUuid in groupedNewInstances) {
|
||||
const oldInstance = groupedOldInstances[persistentUuid];
|
||||
const newInstance = groupedNewInstances[persistentUuid];
|
||||
const runtimeObject = groupedRuntimeObjects[persistentUuid];
|
||||
@@ -1216,7 +1439,7 @@ namespace gdjs {
|
||||
) {
|
||||
// Instance was created (or object name changed, in which case it was destroyed previously)
|
||||
// and we verified that runtimeObject does not exist.
|
||||
runtimeScene.createObjectsFrom(
|
||||
runtimeInstanceContainer.createObjectsFrom(
|
||||
[newInstance],
|
||||
0,
|
||||
0,
|
||||
@@ -1229,6 +1452,11 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
_hotReloadRuntimeInstance(
|
||||
oldProjectData: ProjectData,
|
||||
newProjectData: ProjectData,
|
||||
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
|
||||
oldObjectData: ObjectData,
|
||||
newObjectData: ObjectData,
|
||||
oldInstance: InstanceData,
|
||||
newInstance: InstanceData,
|
||||
runtimeObject: gdjs.RuntimeObject
|
||||
@@ -1326,6 +1554,26 @@ namespace gdjs {
|
||||
sizeChanged = true;
|
||||
}
|
||||
}
|
||||
if (runtimeObject instanceof gdjs.CustomRuntimeObject) {
|
||||
const childrenInstanceContainer = runtimeObject.getChildrenContainer();
|
||||
|
||||
// The `objects` attribute is already resolved by `resolveCustomObjectConfigurations()`.
|
||||
const oldCustomObjectData = oldObjectData as ObjectData &
|
||||
CustomObjectConfiguration &
|
||||
InstanceContainerData;
|
||||
const newCustomObjectData = newObjectData as ObjectData &
|
||||
CustomObjectConfiguration &
|
||||
InstanceContainerData;
|
||||
|
||||
this._hotReloadRuntimeInstanceContainer(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
oldCustomObjectData,
|
||||
newCustomObjectData,
|
||||
changedRuntimeBehaviors,
|
||||
childrenInstanceContainer
|
||||
);
|
||||
}
|
||||
|
||||
// Update variables
|
||||
this._hotReloadVariablesContainer(
|
||||
|
16
GDJS/Runtime/debugger-client/minimal-debugger-client.ts
Normal file
16
GDJS/Runtime/debugger-client/minimal-debugger-client.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace gdjs {
|
||||
/**
|
||||
* Does nothing apart from allowing to reporting errors.
|
||||
*/
|
||||
export class MinimalDebuggerClient extends gdjs.AbstractDebuggerClient {
|
||||
constructor(runtimeGame: RuntimeGame) {
|
||||
super(runtimeGame);
|
||||
}
|
||||
|
||||
protected _sendMessage(message: string) {}
|
||||
}
|
||||
|
||||
//Register the class to let the engine use it.
|
||||
// @ts-ignore
|
||||
export const DebuggerClient = WindowMessageDebuggerClient;
|
||||
}
|
@@ -8,9 +8,6 @@ namespace gdjs {
|
||||
export class WebsocketDebuggerClient extends gdjs.AbstractDebuggerClient {
|
||||
_ws: WebSocket | null;
|
||||
|
||||
/**
|
||||
* @param path - The path of the property to modify, starting from the RuntimeGame.
|
||||
*/
|
||||
constructor(runtimeGame: RuntimeGame) {
|
||||
super(runtimeGame);
|
||||
this._ws = null;
|
||||
|
@@ -8,9 +8,6 @@ namespace gdjs {
|
||||
export class WindowMessageDebuggerClient extends gdjs.AbstractDebuggerClient {
|
||||
_opener: Window | null = null;
|
||||
|
||||
/**
|
||||
* @param path - The path of the property to modify, starting from the RuntimeGame.
|
||||
*/
|
||||
constructor(runtimeGame: RuntimeGame) {
|
||||
super(runtimeGame);
|
||||
|
||||
|
@@ -72,7 +72,7 @@ namespace gdjs {
|
||||
electronRemoteRequirePath?: string;
|
||||
|
||||
/**
|
||||
* the token to use by the game engine when requiring any resource stored on
|
||||
* The token to use by the game engine when requiring any resource stored on
|
||||
* GDevelop Cloud buckets. Note that this is only useful during previews.
|
||||
*/
|
||||
gdevelopResourceToken?: string;
|
||||
@@ -85,6 +85,21 @@ namespace gdjs {
|
||||
*/
|
||||
allowAuthenticationUsingIframeForPreview?: boolean;
|
||||
|
||||
/** If set, the game will send crash reports to GDevelop APIs. */
|
||||
crashReportUploadLevel?: 'all' | 'exclude-javascript-code-events' | 'none';
|
||||
|
||||
/** Arbitrary string explaining in which context the game is being played. */
|
||||
previewContext?: string;
|
||||
|
||||
/** The GDevelop version used to build the game. */
|
||||
gdevelopVersionWithHash?: string;
|
||||
|
||||
/** The template slug that was used to create the project. */
|
||||
projectTemplateSlug?: string;
|
||||
|
||||
/** The source game id that was used to create the project. */
|
||||
sourceGameId?: string;
|
||||
|
||||
/**
|
||||
* If set, this data is used to authenticate automatically when launching the game.
|
||||
* This is only useful during previews.
|
||||
@@ -189,6 +204,7 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
}
|
||||
this._eventsBasedObjectDatas = new Map<String, EventsBasedObjectData>();
|
||||
this._data = data;
|
||||
this._updateSceneAndExtensionsData();
|
||||
|
||||
@@ -250,18 +266,6 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
this._eventsBasedObjectDatas = new Map<String, EventsBasedObjectData>();
|
||||
if (this._data.eventsFunctionsExtensions) {
|
||||
for (const extension of this._data.eventsFunctionsExtensions) {
|
||||
for (const eventsBasedObject of extension.eventsBasedObjects) {
|
||||
this._eventsBasedObjectDatas.set(
|
||||
extension.name + '::' + eventsBasedObject.name,
|
||||
eventsBasedObject
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isUsingGDevelopDevelopmentEnvironment()) {
|
||||
logger.info(
|
||||
'This game will run on the development version of GDevelop APIs.'
|
||||
@@ -292,6 +296,18 @@ namespace gdjs {
|
||||
sceneData,
|
||||
usedExtensionsWithVariablesData,
|
||||
}));
|
||||
|
||||
this._eventsBasedObjectDatas.clear();
|
||||
if (this._data.eventsFunctionsExtensions) {
|
||||
for (const extension of this._data.eventsFunctionsExtensions) {
|
||||
for (const eventsBasedObject of extension.eventsBasedObjects) {
|
||||
this._eventsBasedObjectDatas.set(
|
||||
extension.name + '::' + eventsBasedObject.name,
|
||||
eventsBasedObject
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -605,6 +605,38 @@ namespace gdjs {
|
||||
return this;
|
||||
}
|
||||
|
||||
getUnrotatedViewportMinX(): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getUnrotatedViewportMinY(): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getUnrotatedViewportMaxX(): float {
|
||||
return this._cachedGameResolutionWidth;
|
||||
}
|
||||
|
||||
getUnrotatedViewportMaxY(): float {
|
||||
return this._cachedGameResolutionHeight;
|
||||
}
|
||||
|
||||
getInitialUnrotatedViewportMinX(): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getInitialUnrotatedViewportMinY(): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getInitialUnrotatedViewportMaxX(): float {
|
||||
return this.getGame().getOriginalWidth();
|
||||
}
|
||||
|
||||
getInitialUnrotatedViewportMaxY(): float {
|
||||
return this.getGame().getOriginalHeight();
|
||||
}
|
||||
|
||||
getViewportWidth(): float {
|
||||
return this._cachedGameResolutionWidth;
|
||||
}
|
||||
|
@@ -208,11 +208,16 @@ namespace gdjs {
|
||||
return this._wasFirstSceneLoaded;
|
||||
}
|
||||
|
||||
getAllSceneNames(): Array<string> {
|
||||
return this._stack.map((scene) => scene.getName());
|
||||
}
|
||||
|
||||
getNetworkSyncData(
|
||||
syncOptions: GetNetworkSyncDataOptions
|
||||
): SceneStackNetworkSyncData | null {
|
||||
const syncedPlayerNumber = syncOptions.playerNumber;
|
||||
if (syncedPlayerNumber !== undefined && syncedPlayerNumber !== 1) {
|
||||
const isHost = syncOptions.isHost;
|
||||
if (syncedPlayerNumber !== undefined && !isHost) {
|
||||
// If we are getting sync data of a specific player,
|
||||
// and they are not the host, we don't sync the scene stack.
|
||||
return null;
|
||||
|
@@ -109,6 +109,7 @@ namespace gdjs {
|
||||
newObjectData.animations
|
||||
);
|
||||
this._updateIfNotVisible = !!newObjectData.updateIfNotVisible;
|
||||
this._updateAnimationFrame();
|
||||
this.invalidateHitboxes();
|
||||
return true;
|
||||
}
|
||||
|
39
GDJS/Runtime/types/project-data.d.ts
vendored
39
GDJS/Runtime/types/project-data.d.ts
vendored
@@ -39,7 +39,10 @@ declare type ObjectData = {
|
||||
effects: Array<EffectData>;
|
||||
};
|
||||
|
||||
declare type GetNetworkSyncDataOptions = { playerNumber?: number };
|
||||
declare type GetNetworkSyncDataOptions = {
|
||||
playerNumber?: number;
|
||||
isHost?: boolean;
|
||||
};
|
||||
|
||||
/** Object containing basic properties for all objects synchronizing over the network. */
|
||||
declare type BasicObjectNetworkSyncData = {
|
||||
@@ -149,7 +152,14 @@ declare interface GdVersionData {
|
||||
revision: number;
|
||||
}
|
||||
|
||||
declare interface LayoutData {
|
||||
declare interface InstanceContainerData {
|
||||
variables: RootVariableData[];
|
||||
instances: InstanceData[];
|
||||
objects: ObjectData[];
|
||||
layers: LayerData[];
|
||||
}
|
||||
|
||||
declare interface LayoutData extends InstanceContainerData {
|
||||
r: number;
|
||||
v: number;
|
||||
b: number;
|
||||
@@ -157,10 +167,6 @@ declare interface LayoutData {
|
||||
name: string;
|
||||
stopSoundsOnStartup: boolean;
|
||||
title: string;
|
||||
variables: RootVariableData[];
|
||||
instances: InstanceData[];
|
||||
objects: ObjectData[];
|
||||
layers: LayerData[];
|
||||
behaviorsSharedData: BehaviorSharedData[];
|
||||
usedResources: ResourceReference[];
|
||||
}
|
||||
@@ -200,9 +206,25 @@ declare interface SceneAndExtensionsData {
|
||||
usedExtensionsWithVariablesData: EventsFunctionsExtensionData[];
|
||||
}
|
||||
|
||||
declare interface EventsBasedObjectData {
|
||||
declare interface EventsBasedObjectData extends InstanceContainerData {
|
||||
name: string;
|
||||
objects: Array<ObjectData & any>;
|
||||
isInnerAreaFollowingParentSize: boolean;
|
||||
// The flat representation of defaultSize.
|
||||
areaMinX: float;
|
||||
areaMinY: float;
|
||||
areaMinZ: float;
|
||||
areaMaxX: float;
|
||||
areaMaxY: float;
|
||||
areaMaxZ: float;
|
||||
/**
|
||||
* A value shared by every object instances.
|
||||
*
|
||||
* @see gdjs.CustomRuntimeObjectInstanceContainer._originalInnerArea
|
||||
**/
|
||||
_initialInnerArea: {
|
||||
min: [float, float, float];
|
||||
max: [float, float, float];
|
||||
} | null;
|
||||
}
|
||||
|
||||
declare interface BehaviorSharedData {
|
||||
@@ -212,6 +234,7 @@ declare interface BehaviorSharedData {
|
||||
|
||||
declare interface ExternalLayoutData {
|
||||
name: string;
|
||||
associatedLayout: string;
|
||||
instances: InstanceData[];
|
||||
}
|
||||
|
||||
|
@@ -86,6 +86,16 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
rebuildIndexFrom(data: VariableData[]) {
|
||||
this._variablesArray.length = 0;
|
||||
for (const variableData of data) {
|
||||
if (variableData.name) {
|
||||
const variable = this._variables.get(variableData.name);
|
||||
this._variablesArray.push(variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a new variable.
|
||||
* This should only be used by generated code.
|
||||
@@ -224,6 +234,7 @@ namespace gdjs {
|
||||
syncOptions: GetNetworkSyncDataOptions
|
||||
): VariableNetworkSyncData[] {
|
||||
const syncedPlayerNumber = syncOptions.playerNumber;
|
||||
const isHost = syncOptions.isHost;
|
||||
const networkSyncData: VariableNetworkSyncData[] = [];
|
||||
const variableNames = [];
|
||||
this._variables.keys(variableNames);
|
||||
@@ -237,10 +248,10 @@ namespace gdjs {
|
||||
variableOwner === null ||
|
||||
// Getting sync data for a specific player:
|
||||
(syncedPlayerNumber !== undefined &&
|
||||
// Owned by host but we are not player 1.
|
||||
// Variable is owned by host but this player number is not the host.
|
||||
variableOwner === 0 &&
|
||||
syncedPlayerNumber !== 1) ||
|
||||
// Owned by a player but we are not this player.
|
||||
!isHost) ||
|
||||
// Variable is owned by a player but not getting sync data for this player number.
|
||||
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)
|
||||
) {
|
||||
// In those cases, the variable should not be synchronized.
|
||||
@@ -442,6 +453,9 @@ namespace gdjs {
|
||||
getVariableNameInContainerByLoopingThroughAllVariables: function () {
|
||||
return '';
|
||||
},
|
||||
rebuildIndexFrom: function () {
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -161,6 +161,7 @@ module.exports = function (config) {
|
||||
'./GDJS/tests/tests-utils/init.js',
|
||||
'./GDJS/tests/tests-utils/init.pixiruntimegamewithassets.js',
|
||||
'./GDJS/tests/tests-utils/init.pixiruntimegame.js',
|
||||
'./GDJS/tests/tests-utils/MockedCustomObject.js',
|
||||
|
||||
// Test helpers
|
||||
'./Extensions/PlatformBehavior/tests/PlatformerTestHelper.js',
|
||||
|
52
GDJS/tests/tests-utils/MockedCustomObject.js
Normal file
52
GDJS/tests/tests-utils/MockedCustomObject.js
Normal file
@@ -0,0 +1,52 @@
|
||||
gdjs.evtsExt__MyExtension__MyCustomObject = gdjs.evtsExt__MyExtension__MyCustomObject || {};
|
||||
|
||||
/**
|
||||
* Mocked generated custom object for MyExtension::MyCustomObject.
|
||||
*/
|
||||
gdjs.evtsExt__MyExtension__MyCustomObject.MyCustomObject = class MyCustomObject extends gdjs.CustomRuntimeObject2D {
|
||||
constructor(parentInstanceContainer, objectData) {
|
||||
super(parentInstanceContainer, objectData);
|
||||
this._parentInstanceContainer = parentInstanceContainer;
|
||||
|
||||
this._onceTriggers = new gdjs.OnceTriggers();
|
||||
this._objectData = {};
|
||||
|
||||
this._objectData.MyProperty = objectData.content.MyProperty !== undefined ? objectData.content.MyProperty : Number("123") || 0;
|
||||
|
||||
|
||||
// It calls the onCreated super implementation at the end.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
// Hot-reload:
|
||||
updateFromObjectData(oldObjectData, newObjectData) {
|
||||
super.updateFromObjectData(oldObjectData, newObjectData);
|
||||
if (oldObjectData.content.MyProperty !== newObjectData.content.MyProperty)
|
||||
this._objectData.MyProperty = newObjectData.content.MyProperty;
|
||||
|
||||
this.onHotReloading(this._parentInstanceContainer);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Properties:
|
||||
|
||||
_getMyProperty() {
|
||||
return this._objectData.MyProperty !== undefined ? this._objectData.MyProperty : Number("123") || 0;
|
||||
}
|
||||
_setMyProperty(newValue) {
|
||||
this._objectData.MyProperty = newValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Methods:
|
||||
|
||||
gdjs.evtsExt__MyExtension__MyCustomObject.MyCustomObject.prototype.doStepPreEvents = function() {
|
||||
this._onceTriggers.startNewFrame();
|
||||
};
|
||||
|
||||
|
||||
gdjs.registerObject("MyExtension::MyCustomObject", gdjs.evtsExt__MyExtension__MyCustomObject.MyCustomObject);
|
@@ -4,10 +4,10 @@
|
||||
* Create and return a minimum working game.
|
||||
* @internal
|
||||
* @param {{layouts?: LayoutData[], resources?: ResourcesData, propertiesOverrides?: Partial<ProjectPropertiesData>}=} settings
|
||||
* @returns {gdjs.RuntimeGame}
|
||||
* @returns {ProjectData}
|
||||
*/
|
||||
gdjs.getPixiRuntimeGame = (settings) => {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
gdjs.createProjectData = (settings) => {
|
||||
return {
|
||||
variables: [],
|
||||
properties: {
|
||||
adaptGameResolutionAtRuntime: true,
|
||||
@@ -65,7 +65,14 @@ gdjs.getPixiRuntimeGame = (settings) => {
|
||||
resources: (settings && settings.resources) || { resources: [] },
|
||||
eventsFunctionsExtensions: [],
|
||||
usedResources: [],
|
||||
});
|
||||
|
||||
return runtimeGame;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create and return a minimum working game.
|
||||
* @internal
|
||||
* @param {{layouts?: LayoutData[], resources?: ResourcesData, propertiesOverrides?: Partial<ProjectPropertiesData>}=} settings
|
||||
* @returns {gdjs.RuntimeGame}
|
||||
*/
|
||||
gdjs.getPixiRuntimeGame = (settings) =>
|
||||
new gdjs.RuntimeGame(gdjs.createProjectData(settings));
|
||||
|
@@ -104,13 +104,24 @@ gdjs.getPixiRuntimeGameWithAssets = () => {
|
||||
{
|
||||
name: 'MySprite',
|
||||
type: 'Sprite',
|
||||
updateIfNotVisible: false,
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
animations: [],
|
||||
effects: [],
|
||||
// @ts-ignore This is the object configuration.
|
||||
updateIfNotVisible: false,
|
||||
// @ts-ignore This is the object configuration.
|
||||
animations: [],
|
||||
},
|
||||
],
|
||||
instances: [],
|
||||
layers: [],
|
||||
areaMinX: 0,
|
||||
areaMinY: 0,
|
||||
areaMinZ: 0,
|
||||
areaMaxX: 0,
|
||||
areaMaxY: 0,
|
||||
areaMaxZ: 0,
|
||||
_initialInnerArea: null,
|
||||
},
|
||||
],
|
||||
sceneVariables: [],
|
||||
|
@@ -18,16 +18,7 @@ describe('gdjs.CustomRuntimeObject', function () {
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
instances: [],
|
||||
layers: [],
|
||||
content: {},
|
||||
areaMinX: 0,
|
||||
areaMinY: 0,
|
||||
areaMinZ: 0,
|
||||
areaMaxX: 0,
|
||||
areaMaxY: 0,
|
||||
areaMaxZ: 0,
|
||||
defaultSize: null,
|
||||
childrenContent: {
|
||||
MySprite: {
|
||||
updateIfNotVisible: false,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -662,8 +662,10 @@ interface ObjectsContainersList {
|
||||
[Value] VectorString GetAnimationNamesOfObject([Const] DOMString name);
|
||||
[Const, Value] DOMString GetTypeOfBehaviorInObjectOrGroup([Const] DOMString objectOrGroupName, [Const] DOMString behaviorName, boolean searchInGroups);
|
||||
boolean HasObjectOrGroupNamed([Const] DOMString name);
|
||||
|
||||
ObjectsContainersList_VariableExistence HasObjectOrGroupWithVariableNamed([Const] DOMString objectName, [Const] DOMString variableName);
|
||||
|
||||
[Const, Ref] ObjectsContainer GetObjectsContainer(unsigned long index);
|
||||
unsigned long GetObjectsContainersCount();
|
||||
};
|
||||
|
||||
interface ProjectScopedContainers {
|
||||
@@ -875,6 +877,12 @@ interface ObjectJsImplementation {
|
||||
interface CustomObjectConfiguration {
|
||||
[Value] UniquePtrObjectConfiguration Clone();
|
||||
|
||||
boolean IsForcedToOverrideEventsBasedObjectChildrenConfiguration();
|
||||
boolean IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration();
|
||||
void SetMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
|
||||
boolean isOverridingEventsBasedObjectChildrenConfiguration);
|
||||
|
||||
void ClearChildrenConfiguration();
|
||||
[Ref] ObjectConfiguration GetChildObjectConfiguration([Const] DOMString objectName);
|
||||
|
||||
[Value] MapStringPropertyDescriptor GetProperties();
|
||||
@@ -2948,6 +2956,8 @@ interface EventsBasedObject {
|
||||
boolean IsAnimatable();
|
||||
[Ref] EventsBasedObject MarkAsTextContainer(boolean isTextContainer);
|
||||
boolean IsTextContainer();
|
||||
[Ref] EventsBasedObject MarkAsInnerAreaExpandingWithParent(boolean value);
|
||||
boolean IsInnerAreaFollowingParentSize();
|
||||
|
||||
[Ref] InitialInstancesContainer GetInitialInstances();
|
||||
[Ref] LayersContainer GetLayers();
|
||||
@@ -3712,6 +3722,7 @@ interface PreviewExportOptions {
|
||||
void PreviewExportOptions([Ref] Project project, [Const] DOMString outputPath);
|
||||
[Ref] PreviewExportOptions UseWebsocketDebuggerClientWithServerAddress([Const] DOMString address, [Const] DOMString port);
|
||||
[Ref] PreviewExportOptions UseWindowMessageDebuggerClient();
|
||||
[Ref] PreviewExportOptions UseMinimalDebuggerClient();
|
||||
[Ref] PreviewExportOptions SetLayoutName([Const] DOMString layoutName);
|
||||
[Ref] PreviewExportOptions SetFallbackAuthor([Const] DOMString id, [Const] DOMString username);
|
||||
[Ref] PreviewExportOptions SetAuthenticatedPlayer([Const] DOMString playerId, [Const] DOMString playerUsername, [Const] DOMString playerToken);
|
||||
@@ -3725,6 +3736,11 @@ interface PreviewExportOptions {
|
||||
[Ref] PreviewExportOptions SetElectronRemoteRequirePath([Const] DOMString electronRemoteRequirePath);
|
||||
[Ref] PreviewExportOptions SetGDevelopResourceToken([Const] DOMString gdevelopResourceToken);
|
||||
[Ref] PreviewExportOptions SetAllowAuthenticationUsingIframeForPreview(boolean enable);
|
||||
[Ref] PreviewExportOptions SetCrashReportUploadLevel([Const] DOMString crashReportUploadLevel);
|
||||
[Ref] PreviewExportOptions SetPreviewContext([Const] DOMString previewContext);
|
||||
[Ref] PreviewExportOptions SetGDevelopVersionWithHash([Const] DOMString gdevelopVersionWithHash);
|
||||
[Ref] PreviewExportOptions SetProjectTemplateSlug([Const] DOMString projectTemplateSlug);
|
||||
[Ref] PreviewExportOptions SetSourceGameId([Const] DOMString sourceGameId);
|
||||
};
|
||||
|
||||
[Prefix="gdjs::"]
|
||||
|
14
GDevelop.js/types.d.ts
vendored
14
GDevelop.js/types.d.ts
vendored
@@ -599,6 +599,8 @@ export class ObjectsContainersList extends EmscriptenObject {
|
||||
getTypeOfBehaviorInObjectOrGroup(objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
|
||||
hasObjectOrGroupNamed(name: string): boolean;
|
||||
hasObjectOrGroupWithVariableNamed(objectName: string, variableName: string): ObjectsContainersList_VariableExistence;
|
||||
getObjectsContainer(index: number): ObjectsContainer;
|
||||
getObjectsContainersCount(): number;
|
||||
}
|
||||
|
||||
export class ProjectScopedContainers extends EmscriptenObject {
|
||||
@@ -729,6 +731,10 @@ export class ObjectJsImplementation extends ObjectConfiguration {
|
||||
|
||||
export class CustomObjectConfiguration extends ObjectConfiguration {
|
||||
clone(): UniquePtrObjectConfiguration;
|
||||
isForcedToOverrideEventsBasedObjectChildrenConfiguration(): boolean;
|
||||
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(): boolean;
|
||||
setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(isOverridingEventsBasedObjectChildrenConfiguration: boolean): void;
|
||||
clearChildrenConfiguration(): void;
|
||||
getChildObjectConfiguration(objectName: string): ObjectConfiguration;
|
||||
getProperties(): MapStringPropertyDescriptor;
|
||||
updateProperty(name: string, value: string): boolean;
|
||||
@@ -2146,6 +2152,8 @@ export class EventsBasedObject extends AbstractEventsBasedEntity {
|
||||
isAnimatable(): boolean;
|
||||
markAsTextContainer(isTextContainer: boolean): EventsBasedObject;
|
||||
isTextContainer(): boolean;
|
||||
markAsInnerAreaExpandingWithParent(value: boolean): EventsBasedObject;
|
||||
isInnerAreaFollowingParentSize(): boolean;
|
||||
getInitialInstances(): InitialInstancesContainer;
|
||||
getLayers(): LayersContainer;
|
||||
getObjects(): ObjectsContainer;
|
||||
@@ -2759,6 +2767,7 @@ export class PreviewExportOptions extends EmscriptenObject {
|
||||
constructor(project: Project, outputPath: string);
|
||||
useWebsocketDebuggerClientWithServerAddress(address: string, port: string): PreviewExportOptions;
|
||||
useWindowMessageDebuggerClient(): PreviewExportOptions;
|
||||
useMinimalDebuggerClient(): PreviewExportOptions;
|
||||
setLayoutName(layoutName: string): PreviewExportOptions;
|
||||
setFallbackAuthor(id: string, username: string): PreviewExportOptions;
|
||||
setAuthenticatedPlayer(playerId: string, playerUsername: string, playerToken: string): PreviewExportOptions;
|
||||
@@ -2772,6 +2781,11 @@ export class PreviewExportOptions extends EmscriptenObject {
|
||||
setElectronRemoteRequirePath(electronRemoteRequirePath: string): PreviewExportOptions;
|
||||
setGDevelopResourceToken(gdevelopResourceToken: string): PreviewExportOptions;
|
||||
setAllowAuthenticationUsingIframeForPreview(enable: boolean): PreviewExportOptions;
|
||||
setCrashReportUploadLevel(crashReportUploadLevel: string): PreviewExportOptions;
|
||||
setPreviewContext(previewContext: string): PreviewExportOptions;
|
||||
setGDevelopVersionWithHash(gdevelopVersionWithHash: string): PreviewExportOptions;
|
||||
setProjectTemplateSlug(projectTemplateSlug: string): PreviewExportOptions;
|
||||
setSourceGameId(sourceGameId: string): PreviewExportOptions;
|
||||
}
|
||||
|
||||
export class ExportOptions extends EmscriptenObject {
|
||||
|
@@ -1,6 +1,10 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdCustomObjectConfiguration extends gdObjectConfiguration {
|
||||
clone(): gdUniquePtrObjectConfiguration;
|
||||
isForcedToOverrideEventsBasedObjectChildrenConfiguration(): boolean;
|
||||
isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(): boolean;
|
||||
setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(isOverridingEventsBasedObjectChildrenConfiguration: boolean): void;
|
||||
clearChildrenConfiguration(): void;
|
||||
getChildObjectConfiguration(objectName: string): gdObjectConfiguration;
|
||||
getProperties(): gdMapStringPropertyDescriptor;
|
||||
updateProperty(name: string, value: string): boolean;
|
||||
|
@@ -15,6 +15,8 @@ declare class gdEventsBasedObject extends gdAbstractEventsBasedEntity {
|
||||
isAnimatable(): boolean;
|
||||
markAsTextContainer(isTextContainer: boolean): gdEventsBasedObject;
|
||||
isTextContainer(): boolean;
|
||||
markAsInnerAreaExpandingWithParent(value: boolean): gdEventsBasedObject;
|
||||
isInnerAreaFollowingParentSize(): boolean;
|
||||
getInitialInstances(): gdInitialInstancesContainer;
|
||||
getLayers(): gdLayersContainer;
|
||||
getObjects(): gdObjectsContainer;
|
||||
|
@@ -14,6 +14,8 @@ declare class gdObjectsContainersList {
|
||||
getTypeOfBehaviorInObjectOrGroup(objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
|
||||
hasObjectOrGroupNamed(name: string): boolean;
|
||||
hasObjectOrGroupWithVariableNamed(objectName: string, variableName: string): ObjectsContainersList_VariableExistence;
|
||||
getObjectsContainer(index: number): gdObjectsContainer;
|
||||
getObjectsContainersCount(): number;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -3,6 +3,7 @@ declare class gdPreviewExportOptions {
|
||||
constructor(project: gdProject, outputPath: string): void;
|
||||
useWebsocketDebuggerClientWithServerAddress(address: string, port: string): gdPreviewExportOptions;
|
||||
useWindowMessageDebuggerClient(): gdPreviewExportOptions;
|
||||
useMinimalDebuggerClient(): gdPreviewExportOptions;
|
||||
setLayoutName(layoutName: string): gdPreviewExportOptions;
|
||||
setFallbackAuthor(id: string, username: string): gdPreviewExportOptions;
|
||||
setAuthenticatedPlayer(playerId: string, playerUsername: string, playerToken: string): gdPreviewExportOptions;
|
||||
@@ -16,6 +17,11 @@ declare class gdPreviewExportOptions {
|
||||
setElectronRemoteRequirePath(electronRemoteRequirePath: string): gdPreviewExportOptions;
|
||||
setGDevelopResourceToken(gdevelopResourceToken: string): gdPreviewExportOptions;
|
||||
setAllowAuthenticationUsingIframeForPreview(enable: boolean): gdPreviewExportOptions;
|
||||
setCrashReportUploadLevel(crashReportUploadLevel: string): gdPreviewExportOptions;
|
||||
setPreviewContext(previewContext: string): gdPreviewExportOptions;
|
||||
setGDevelopVersionWithHash(gdevelopVersionWithHash: string): gdPreviewExportOptions;
|
||||
setProjectTemplateSlug(projectTemplateSlug: string): gdPreviewExportOptions;
|
||||
setSourceGameId(sourceGameId: string): gdPreviewExportOptions;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -27,23 +27,23 @@
|
||||
"pixi.js": "7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@rollup/plugin-node-resolve": "13.3.0",
|
||||
"@rollup/plugin-typescript": "8.3.3",
|
||||
"@types/expect.js": "^0.3.29",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"expect.js": "^0.3.1",
|
||||
"karma": "^6.1.0",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-firefox-launcher": "^2.0.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-typescript": "latest",
|
||||
"mocha": "^6.2.0",
|
||||
"@types/expect.js": "0.3.32",
|
||||
"@types/mocha": "5.2.7",
|
||||
"expect.js": "0.3.1",
|
||||
"karma": "6.4.3",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-firefox-launcher": "2.1.3",
|
||||
"karma-mocha": "1.3.0",
|
||||
"karma-typescript": "5.5.4",
|
||||
"mocha": "6.2.3",
|
||||
"prettier": "2.1.2",
|
||||
"rollup": "^2.66.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"ts-loader": "^9.2.3",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "latest"
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"ts-loader": "9.5.1",
|
||||
"tslib": "2.6.2",
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -75,7 +75,7 @@ export namespace LDtkTileMapLoader {
|
||||
gridSize,
|
||||
dimX,
|
||||
dimY,
|
||||
tileSet,
|
||||
tileSet
|
||||
);
|
||||
const composedTileMap = new Map<string, TileDefinition>();
|
||||
let nextComposedTileId = 0xfffffff;
|
||||
|
@@ -51,6 +51,7 @@ export namespace TiledTileMapLoader {
|
||||
continue;
|
||||
}
|
||||
let polygon: PolygonVertices | null = null;
|
||||
let hasFullHitBox = false;
|
||||
if (object.polygon) {
|
||||
const angle = (object.rotation * Math.PI) / 180;
|
||||
let cos = Math.cos(angle);
|
||||
@@ -67,6 +68,7 @@ export namespace TiledTileMapLoader {
|
||||
object.y + point.x * sin + point.y * cos,
|
||||
]);
|
||||
//TODO check that polygons are convex or split them?
|
||||
// TODO Set hasFullHitBox to true if the polygon covers the whole tile.
|
||||
}
|
||||
// TODO handle ellipses by creating a polygon?
|
||||
// Make an object property for the number of vertices or always create 8 ones?
|
||||
@@ -83,9 +85,14 @@ export namespace TiledTileMapLoader {
|
||||
[object.x + object.width, object.y + object.height],
|
||||
[object.x + object.width, object.y],
|
||||
];
|
||||
hasFullHitBox =
|
||||
object.x === 0 &&
|
||||
object.y === 0 &&
|
||||
object.width === tiledTileMap.tilewidth &&
|
||||
object.height === tiledTileMap.tileheight;
|
||||
}
|
||||
if (polygon) {
|
||||
tileDefinition.addHitBox(tag, polygon);
|
||||
tileDefinition.addHitBox(tag, polygon, hasFullHitBox);
|
||||
}
|
||||
}
|
||||
} else if (tileClass) {
|
||||
@@ -96,7 +103,7 @@ export namespace TiledTileMapLoader {
|
||||
[tiledTileMap.tilewidth, tiledTileMap.tileheight],
|
||||
[tiledTileMap.tilewidth, 0],
|
||||
];
|
||||
tileDefinition.addHitBox(tileClass, polygon);
|
||||
tileDefinition.addHitBox(tileClass, polygon, true);
|
||||
}
|
||||
definitions.set(
|
||||
getTileIdFromTiledGUI(firstGid + tile.id),
|
||||
@@ -117,7 +124,7 @@ export namespace TiledTileMapLoader {
|
||||
tiledTileMap.tileheight,
|
||||
tiledTileMap.width,
|
||||
tiledTileMap.height,
|
||||
definitions,
|
||||
definitions
|
||||
);
|
||||
|
||||
for (const tiledLayer of tiledTileMap.layers) {
|
||||
|
@@ -1056,6 +1056,7 @@ export class TileDefinition {
|
||||
private readonly taggedHitBoxes: {
|
||||
tag: string;
|
||||
polygons: PolygonVertices[];
|
||||
hasFullHitBox: boolean;
|
||||
}[];
|
||||
private readonly animationLength: integer;
|
||||
|
||||
@@ -1078,13 +1079,19 @@ export class TileDefinition {
|
||||
* Add a polygon for the collision layer
|
||||
* @param tag The tag to allow collision layer filtering.
|
||||
* @param polygon The polygon to use for collisions.
|
||||
* @param hasFullHitBox Set to `true` when the hitBox cover the whole tile.
|
||||
*/
|
||||
addHitBox(tag: string, polygon: PolygonVertices): void {
|
||||
addHitBox(
|
||||
tag: string,
|
||||
polygon: PolygonVertices,
|
||||
hasFullHitBox: boolean
|
||||
): void {
|
||||
let taggedHitBox = this.taggedHitBoxes.find((hitbox) => hitbox.tag === tag);
|
||||
if (!taggedHitBox) {
|
||||
taggedHitBox = { tag, polygons: [] };
|
||||
taggedHitBox = { tag, polygons: [], hasFullHitBox: false };
|
||||
this.taggedHitBoxes.push(taggedHitBox);
|
||||
}
|
||||
taggedHitBox.hasFullHitBox ||= hasFullHitBox;
|
||||
taggedHitBox.polygons.push(polygon);
|
||||
}
|
||||
|
||||
@@ -1110,6 +1117,18 @@ export class TileDefinition {
|
||||
return taggedHitBox && taggedHitBox.polygons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `true` if the hit-box cover the whole tile.
|
||||
* @param tag The tag to allow collision layer filtering.
|
||||
* @returns `true` if the hit-box cover the whole tile.
|
||||
*/
|
||||
hasFullHitBox(tag: string): boolean {
|
||||
const taggedHitBox = this.taggedHitBoxes.find(
|
||||
(hitbox) => hitbox.tag === tag
|
||||
);
|
||||
return taggedHitBox && taggedHitBox.hasFullHitBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated tiles have a limitation:
|
||||
* they are only able to use frames arranged horizontally one next
|
||||
|
@@ -131,14 +131,11 @@ export class TileMapManager {
|
||||
// TODO: Is it useful to cache the tilemap since it belongs to an instance?
|
||||
// const key = `${objectName}|${tileSize}|${tileSetColumnCount}|${tileSetRowCount}`;
|
||||
|
||||
const editableTileMap = EditableTileMap.from(
|
||||
tileMapAsJsObject,
|
||||
{
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
},
|
||||
);
|
||||
const editableTileMap = EditableTileMap.from(tileMapAsJsObject, {
|
||||
tileSize,
|
||||
tileSetColumnCount,
|
||||
tileSetRowCount,
|
||||
});
|
||||
callback(editableTileMap);
|
||||
}
|
||||
|
||||
@@ -224,9 +221,12 @@ export class TileMapManager {
|
||||
this._textureCacheCaches.getOrLoad(
|
||||
key,
|
||||
(textureCacheLoadingCallback) => {
|
||||
const atlasTexture = atlasImageResourceName
|
||||
? getTexture(atlasImageResourceName)
|
||||
: null;
|
||||
if (!atlasImageResourceName) {
|
||||
textureCacheLoadingCallback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const atlasTexture = getTexture(atlasImageResourceName);
|
||||
const textureCache = PixiTileMapHelper.parseSimpleTileMapAtlas(
|
||||
atlasTexture,
|
||||
columnCount,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// @ts-check
|
||||
const { mapVector } = require('./MapFor');
|
||||
const { mapFor } = require('./MapFor');
|
||||
const { generateReadMoreLink } = require('./WikiHelpLink');
|
||||
|
||||
// Types definitions used in this script:
|
||||
@@ -261,10 +261,11 @@ const generateExpressionReferenceRowsText = ({
|
||||
}) => {
|
||||
let parameterRows = [];
|
||||
let parameterStrings = [];
|
||||
mapVector(expressionMetadata.getParameters(), (parameterMetadata, index) => {
|
||||
mapFor(0, expressionMetadata.getParameters(), index => {
|
||||
if ((!!objectMetadata && index < 1) || (!!behaviorMetadata && index < 2)) {
|
||||
return; // Skip the first (or first twos) parameters by convention.
|
||||
}
|
||||
const parameterMetadata = expressionMetadata.getParameter(index);
|
||||
if (parameterMetadata.isCodeOnly()) return;
|
||||
|
||||
const sanitizedDescription = sanitizeExpressionDescription(
|
||||
|
@@ -539,6 +539,10 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
>
|
||||
<SelectField
|
||||
margin="none"
|
||||
disabled={
|
||||
property.getType() === 'Behavior' &&
|
||||
!property.isHidden()
|
||||
}
|
||||
value={
|
||||
property.isHidden()
|
||||
? 'Hidden'
|
||||
|
@@ -19,9 +19,12 @@ import Checkbox from '../UI/Checkbox';
|
||||
import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExtensionEditor';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import Window from '../Utils/Window';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const isDev = Window.isDev();
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
@@ -140,32 +143,34 @@ export default function EventsBasedBehaviorEditor({
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
<SelectField
|
||||
floatingLabelText={
|
||||
<Trans>Visibility in quick customization dialog</Trans>
|
||||
}
|
||||
value={eventsBasedBehavior.getQuickCustomizationVisibility()}
|
||||
onChange={(e, i, valueString: string) => {
|
||||
// $FlowFixMe
|
||||
const value: QuickCustomization_Visibility = valueString;
|
||||
eventsBasedBehavior.setQuickCustomizationVisibility(value);
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Default}
|
||||
label={t`Default (visible)`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Visible}
|
||||
label={t`Always visible`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Hidden}
|
||||
label={t`Hidden`}
|
||||
/>
|
||||
</SelectField>
|
||||
{isDev && (
|
||||
<SelectField
|
||||
floatingLabelText={
|
||||
<Trans>Visibility in quick customization dialog</Trans>
|
||||
}
|
||||
value={eventsBasedBehavior.getQuickCustomizationVisibility()}
|
||||
onChange={(e, i, valueString: string) => {
|
||||
// $FlowFixMe
|
||||
const value: QuickCustomization_Visibility = valueString;
|
||||
eventsBasedBehavior.setQuickCustomizationVisibility(value);
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Default}
|
||||
label={t`Default (visible)`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Visible}
|
||||
label={t`Always visible`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Hidden}
|
||||
label={t`Hidden`}
|
||||
/>
|
||||
</SelectField>
|
||||
)}
|
||||
<Checkbox
|
||||
label={<Trans>Private</Trans>}
|
||||
checked={eventsBasedBehavior.isPrivate()}
|
||||
|
@@ -1,347 +0,0 @@
|
||||
// @flow
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Line } from '../UI/Grid';
|
||||
import ObjectsList, { type ObjectsListInterface } from '../ObjectsList';
|
||||
import ObjectsRenderingService from '../ObjectsRendering/ObjectsRenderingService';
|
||||
import type { ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import ObjectEditorDialog from '../ObjectEditor/ObjectEditorDialog';
|
||||
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
|
||||
import { emptyStorageProvider } from '../ProjectsStorage/ProjectStorageProviders';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import {
|
||||
getObjectFolderOrObjectUnifiedName,
|
||||
type ObjectFolderOrObjectWithContext,
|
||||
} from '../ObjectsList/EnumerateObjectFolderOrObject';
|
||||
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
|
||||
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
eventsBasedObject: gdEventsBasedObject,
|
||||
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
editedObjectWithContext: ?ObjectWithContext,
|
||||
editedObjectInitialTab: ?ObjectEditorTab,
|
||||
selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[],
|
||||
|};
|
||||
|
||||
export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
_objectsList: ?ObjectsListInterface;
|
||||
|
||||
// TODO Reset selectedObjectFolderOrObjectsWithContext when a different eventsBasedObject is passed.
|
||||
// It will avoid to add objects in the tree of the wrong ObjectsContainer.
|
||||
state = {
|
||||
editedObjectWithContext: null,
|
||||
editedObjectInitialTab: 'properties',
|
||||
selectedObjectFolderOrObjectsWithContext: [],
|
||||
};
|
||||
|
||||
_onDeleteObjects = (i18n: I18nType) => (
|
||||
objectsWithContext: ObjectWithContext[],
|
||||
done: boolean => void
|
||||
) => {
|
||||
const { project, eventsBasedObject } = this.props;
|
||||
|
||||
objectsWithContext.forEach(objectWithContext => {
|
||||
const { object } = objectWithContext;
|
||||
gd.WholeProjectRefactorer.objectRemovedInEventsBasedObject(
|
||||
project,
|
||||
eventsBasedObject,
|
||||
object.getName()
|
||||
);
|
||||
});
|
||||
done(true);
|
||||
};
|
||||
|
||||
_getValidatedObjectOrGroupName = (newName: string) => {
|
||||
const { eventsBasedObject } = this.props;
|
||||
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
eventsBasedObject.getObjects().hasObjectNamed(tentativeNewName) ||
|
||||
eventsBasedObject
|
||||
.getObjects()
|
||||
.getObjectGroups()
|
||||
.has(tentativeNewName) ||
|
||||
// TODO EBO Use a constant instead a hard coded value "Object".
|
||||
tentativeNewName === 'Object'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
_onRenameEditedObject = (newName: string) => {
|
||||
const { editedObjectWithContext } = this.state;
|
||||
|
||||
if (editedObjectWithContext) {
|
||||
this._onRenameObject(editedObjectWithContext, newName);
|
||||
}
|
||||
};
|
||||
|
||||
_onRenameObject = (objectWithContext: ObjectWithContext, newName: string) => {
|
||||
const { object } = objectWithContext;
|
||||
const {
|
||||
project,
|
||||
eventsBasedObject,
|
||||
projectScopedContainersAccessor,
|
||||
} = this.props;
|
||||
|
||||
// newName is supposed to have been already validated
|
||||
|
||||
// Avoid triggering renaming refactoring if name has not really changed
|
||||
if (object.getName() !== newName) {
|
||||
gd.WholeProjectRefactorer.objectOrGroupRenamedInEventsBasedObject(
|
||||
project,
|
||||
projectScopedContainersAccessor.get(),
|
||||
eventsBasedObject,
|
||||
object.getName(),
|
||||
newName,
|
||||
/* isObjectGroup=*/ false
|
||||
);
|
||||
}
|
||||
|
||||
object.setName(newName);
|
||||
};
|
||||
|
||||
_onRenameObjectFolderOrObjectFinish = (
|
||||
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
|
||||
newName: string,
|
||||
done: boolean => void
|
||||
) => {
|
||||
const { objectFolderOrObject, global } = objectFolderOrObjectWithContext;
|
||||
|
||||
const unifiedName = getObjectFolderOrObjectUnifiedName(
|
||||
objectFolderOrObject
|
||||
);
|
||||
// Avoid triggering renaming refactoring if name has not really changed
|
||||
if (unifiedName === newName) {
|
||||
this._onObjectFolderOrObjectWithContextSelected(
|
||||
objectFolderOrObjectWithContext
|
||||
);
|
||||
done(false);
|
||||
return;
|
||||
}
|
||||
// newName is supposed to have been already validated.
|
||||
|
||||
if (objectFolderOrObject.isFolder()) {
|
||||
objectFolderOrObject.setFolderName(newName);
|
||||
done(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const object = objectFolderOrObject.getObject();
|
||||
|
||||
this._onRenameObject({ object, global }, newName);
|
||||
this._onObjectFolderOrObjectWithContextSelected(
|
||||
objectFolderOrObjectWithContext
|
||||
);
|
||||
done(true);
|
||||
};
|
||||
|
||||
editObject = (editedObject: ?gdObject, initialTab: ?ObjectEditorTab) => {
|
||||
if (editedObject) {
|
||||
this.setState({
|
||||
editedObjectWithContext: {
|
||||
object: editedObject,
|
||||
global: false,
|
||||
},
|
||||
editedObjectInitialTab: initialTab || 'properties',
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
editedObjectWithContext: null,
|
||||
editedObjectInitialTab: 'properties',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onObjectFolderOrObjectWithContextSelected = (
|
||||
objectFolderOrObjectWithContext: ?ObjectFolderOrObjectWithContext = null
|
||||
) => {
|
||||
const selectedObjectFolderOrObjectsWithContext = [];
|
||||
if (objectFolderOrObjectWithContext) {
|
||||
selectedObjectFolderOrObjectsWithContext.push(
|
||||
objectFolderOrObjectWithContext
|
||||
);
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
selectedObjectFolderOrObjectsWithContext,
|
||||
},
|
||||
() => {
|
||||
this.forceUpdateObjectsList();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
updateBehaviorsSharedData = () => {
|
||||
// TODO EBO Decide how BehaviorsSharedData of child-objects should work.
|
||||
// - Use a shared data per object instance
|
||||
// BehaviorsSharedData is configured on the CustomObject instead of the
|
||||
// scene and each CustomObject instance will have it own data.
|
||||
// - Use the layout shared data
|
||||
// Find all layouts that are using this object and update them
|
||||
// (something a bit like UsedExtensionsFinder, but the other way around).
|
||||
// const { project, eventsBasedObject } = this.props;
|
||||
// const layout = eventsBasedObject.getLayout();
|
||||
// layout.updateBehaviorsSharedData(project);
|
||||
};
|
||||
|
||||
forceUpdateObjectsList = () => {
|
||||
if (this._objectsList) this._objectsList.forceUpdateList();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { eventsBasedObject, project, eventsFunctionsExtension } = this.props;
|
||||
const { selectedObjectFolderOrObjectsWithContext } = this.state;
|
||||
|
||||
// TODO EBO Add a button icon to mark some objects solid or not.
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<React.Fragment>
|
||||
<Line expand useFullHeight>
|
||||
<ObjectsList
|
||||
getThumbnail={ObjectsRenderingService.getThumbnail.bind(
|
||||
ObjectsRenderingService
|
||||
)}
|
||||
project={project}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
unsavedChanges={this.props.unsavedChanges}
|
||||
objectsContainer={eventsBasedObject.getObjects()}
|
||||
globalObjectsContainer={null}
|
||||
layout={null}
|
||||
// TODO EBO Allow to use project resources as place holders
|
||||
resourceManagementProps={{
|
||||
resourceSources: [],
|
||||
resourceExternalEditors: [],
|
||||
onChooseResource: async () => [],
|
||||
getStorageProvider: () => emptyStorageProvider,
|
||||
onFetchNewlyAddedResources: async () => {},
|
||||
getStorageProviderResourceOperations: () => null,
|
||||
canInstallPrivateAsset: () => false,
|
||||
}}
|
||||
selectedObjectFolderOrObjectsWithContext={
|
||||
selectedObjectFolderOrObjectsWithContext
|
||||
}
|
||||
onEditObject={this.editObject}
|
||||
// Don't allow export as there is no assets.
|
||||
onExportAssets={() => {}}
|
||||
onDeleteObjects={this._onDeleteObjects(i18n)}
|
||||
getValidatedObjectOrGroupName={
|
||||
this._getValidatedObjectOrGroupName
|
||||
}
|
||||
// Nothing special to do.
|
||||
onObjectCreated={() => {}}
|
||||
// Nothing special to do.
|
||||
onObjectEdited={() => {}}
|
||||
onObjectFolderOrObjectWithContextSelected={
|
||||
this._onObjectFolderOrObjectWithContextSelected
|
||||
}
|
||||
onRenameObjectFolderOrObjectWithContextFinish={
|
||||
this._onRenameObjectFolderOrObjectFinish
|
||||
}
|
||||
// Instances can't be created from this context.
|
||||
onAddObjectInstance={() => {}}
|
||||
onObjectPasted={() => this.updateBehaviorsSharedData()}
|
||||
ref={objectsList => (this._objectsList = objectsList)}
|
||||
// TODO EBO Hide the preview button or implement it.
|
||||
// Note that it will be hard to do hot reload as extensions need
|
||||
// to be generated.
|
||||
hotReloadPreviewButtonProps={{
|
||||
hasPreviewsRunning: false,
|
||||
launchProjectDataOnlyPreview: async () => {},
|
||||
launchProjectWithLoadingScreenPreview: async () => {},
|
||||
}}
|
||||
canSetAsGlobalObject={false}
|
||||
/>
|
||||
</Line>
|
||||
{this.state.editedObjectWithContext && (
|
||||
<ObjectEditorDialog
|
||||
open
|
||||
object={this.state.editedObjectWithContext.object}
|
||||
initialTab={this.state.editedObjectInitialTab}
|
||||
project={project}
|
||||
layout={null}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
projectScopedContainersAccessor={
|
||||
this.props.projectScopedContainersAccessor
|
||||
}
|
||||
resourceManagementProps={{
|
||||
resourceSources: [],
|
||||
resourceExternalEditors: [],
|
||||
onChooseResource: async () => [],
|
||||
getStorageProvider: () => emptyStorageProvider,
|
||||
onFetchNewlyAddedResources: async () => {},
|
||||
getStorageProviderResourceOperations: () => null,
|
||||
canInstallPrivateAsset: () => false,
|
||||
}}
|
||||
onComputeAllVariableNames={() => {
|
||||
return [];
|
||||
// TODO EBO Find undeclared variables in the parent events.
|
||||
|
||||
// const { editedObjectWithContext } = this.state;
|
||||
// if (!editedObjectWithContext) return [];
|
||||
// return EventsRootVariablesFinder.findAllObjectVariables(
|
||||
// project.getCurrentPlatform(),
|
||||
// project,
|
||||
// eventsBasedObject,
|
||||
// editedObjectWithContext.object.getName()
|
||||
// );
|
||||
}}
|
||||
onCancel={() => {
|
||||
this.editObject(null);
|
||||
}}
|
||||
getValidatedObjectOrGroupName={
|
||||
this._getValidatedObjectOrGroupName
|
||||
}
|
||||
onRename={this._onRenameEditedObject}
|
||||
onApply={() => {
|
||||
this.editObject(null);
|
||||
this.updateBehaviorsSharedData();
|
||||
this.forceUpdateObjectsList();
|
||||
}}
|
||||
// TODO EBO Hide the preview button or implement it.
|
||||
// Note that it will be hard to do hot reload as extensions need
|
||||
// to be generated.
|
||||
hotReloadPreviewButtonProps={{
|
||||
hasPreviewsRunning: false,
|
||||
launchProjectDataOnlyPreview: async () => {},
|
||||
launchProjectWithLoadingScreenPreview: async () => {},
|
||||
}}
|
||||
onUpdateBehaviorsSharedData={() =>
|
||||
this.updateBehaviorsSharedData()
|
||||
}
|
||||
// TODO EBO Go to the behavior extension tab.
|
||||
openBehaviorEvents={() => {}}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
}
|
@@ -4,7 +4,6 @@ import * as React from 'react';
|
||||
import EventsBasedObjectEditor from './index';
|
||||
import { Tabs } from '../UI/Tabs';
|
||||
import EventsBasedObjectPropertiesEditor from './EventsBasedObjectPropertiesEditor';
|
||||
import EventBasedObjectChildrenEditor from './EventBasedObjectChildrenEditor';
|
||||
import Background from '../UI/Background';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
|
||||
@@ -61,10 +60,6 @@ export default function EventsBasedObjectEditorPanel({
|
||||
value: 'properties',
|
||||
label: <Trans>Properties</Trans>,
|
||||
},
|
||||
{
|
||||
value: 'children',
|
||||
label: <Trans>Children</Trans>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Column>
|
||||
@@ -86,14 +81,6 @@ export default function EventsBasedObjectEditorPanel({
|
||||
onEventsFunctionsAdded={onEventsFunctionsAdded}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'children' && (
|
||||
<EventBasedObjectChildrenEditor
|
||||
project={project}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
projectScopedContainersAccessor={projectScopedContainersAccessor}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
</Background>
|
||||
);
|
||||
|
@@ -13,12 +13,9 @@ import HelpButton from '../UI/HelpButton';
|
||||
import { Line } from '../UI/Grid';
|
||||
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Window from '../Utils/Window';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const isDev = Window.isDev();
|
||||
|
||||
type Props = {|
|
||||
eventsBasedObject: gdEventsBasedObject,
|
||||
onOpenCustomObjectEditor: () => void,
|
||||
@@ -119,15 +116,21 @@ export default function EventsBasedObjectEditor({
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
{isDev && (
|
||||
<Line noMargin justifyContent="center">
|
||||
<RaisedButton
|
||||
label={<Trans>Open visual editor for the object</Trans>}
|
||||
primary
|
||||
onClick={onOpenCustomObjectEditor}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
<Checkbox
|
||||
label={<Trans>Expand inner area with parent</Trans>}
|
||||
checked={eventsBasedObject.isInnerAreaFollowingParentSize()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsInnerAreaExpandingWithParent(checked);
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
<Line noMargin justifyContent="center">
|
||||
<RaisedButton
|
||||
label={<Trans>Open visual editor for the object</Trans>}
|
||||
primary
|
||||
onClick={onOpenCustomObjectEditor}
|
||||
/>
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<HelpButton
|
||||
key="help"
|
||||
|
@@ -182,6 +182,7 @@ export const EventsFunctionParametersEditor = ({
|
||||
parameters.insertNewParameter(newName, index).setType('objectList');
|
||||
forceUpdate();
|
||||
onParametersUpdated();
|
||||
setJustAddedParameterName(newName);
|
||||
},
|
||||
[eventsFunction, forceUpdate, onParametersUpdated]
|
||||
);
|
||||
@@ -550,8 +551,8 @@ export const EventsFunctionParametersEditor = ({
|
||||
draggedParameter.current = parameter;
|
||||
return {};
|
||||
}}
|
||||
canDrag={() => true}
|
||||
canDrop={() => true}
|
||||
canDrag={() => !isParameterDisabled(i)}
|
||||
canDrop={() => !isParameterDisabled(i)}
|
||||
drop={() => {
|
||||
moveParameterBefore(parameter);
|
||||
}}
|
||||
@@ -581,7 +582,9 @@ export const EventsFunctionParametersEditor = ({
|
||||
{connectDragSource(
|
||||
<span>
|
||||
<Column>
|
||||
<DragHandleIcon />
|
||||
<DragHandleIcon
|
||||
disabled={isParameterDisabled(i)}
|
||||
/>
|
||||
</Column>
|
||||
</span>
|
||||
)}
|
||||
@@ -629,7 +632,7 @@ export const EventsFunctionParametersEditor = ({
|
||||
label: i18n._(
|
||||
t`Add a parameter below`
|
||||
),
|
||||
enabled: !isParameterDisabled(i),
|
||||
enabled: !isParameterDisabled(i + 1),
|
||||
click: () => addParameterAt(i + 1),
|
||||
},
|
||||
{
|
||||
|
@@ -42,6 +42,7 @@ import Mark from '../UI/CustomSvgIcons/Mark';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
|
||||
import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog';
|
||||
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -75,6 +76,7 @@ type Props = {|
|
||||
initiallyFocusedObjectName: ?string,
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
onOpenCustomObjectEditor: gdEventsBasedObject => void,
|
||||
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
@@ -1313,6 +1315,9 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
settingsIcon={extensionEditIconReactNode}
|
||||
unsavedChanges={this.props.unsavedChanges}
|
||||
isActive={true}
|
||||
hotReloadPreviewButtonProps={
|
||||
this.props.hotReloadPreviewButtonProps
|
||||
}
|
||||
/>
|
||||
</Background>
|
||||
) : selectedEventsBasedBehavior ? (
|
||||
@@ -1527,6 +1532,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
open
|
||||
onCancel={() => this._editVariables(null)}
|
||||
onApply={() => this._editVariables(null)}
|
||||
hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps}
|
||||
/>
|
||||
)}
|
||||
{objectMethodSelectorDialogOpen && selectedEventsBasedObject && (
|
||||
|
@@ -16,9 +16,6 @@ import {
|
||||
extensionObjectsRootFolderId,
|
||||
} from '.';
|
||||
import Add from '../UI/CustomSvgIcons/Add';
|
||||
import Window from '../Utils/Window';
|
||||
|
||||
const isDev = Window.isDev();
|
||||
|
||||
const EVENTS_BASED_OBJECT_CLIPBOARD_KIND = 'Events Based Object';
|
||||
|
||||
@@ -146,13 +143,11 @@ export class EventsBasedObjectTreeViewItemContent
|
||||
|
||||
buildMenuTemplate(i18n: I18nType, index: number) {
|
||||
return [
|
||||
isDev
|
||||
? {
|
||||
label: i18n._(t`Open visual editor`),
|
||||
click: () =>
|
||||
this.props.onOpenCustomObjectEditor(this.eventsBasedObject),
|
||||
}
|
||||
: null,
|
||||
{
|
||||
label: i18n._(t`Open visual editor`),
|
||||
click: () =>
|
||||
this.props.onOpenCustomObjectEditor(this.eventsBasedObject),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Add a function`),
|
||||
click: () => this.addFunctionAtSelection(),
|
||||
|
@@ -123,6 +123,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
isGlobalTabInitiallyOpen={isGlobal}
|
||||
initiallySelectedVariableName={editorOpen.variableName}
|
||||
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
|
||||
hotReloadPreviewButtonProps={null}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
@@ -82,6 +82,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
preventRefactoringToDeleteInstructions
|
||||
initiallySelectedVariableName={editorOpen.variableName}
|
||||
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
|
||||
hotReloadPreviewButtonProps={null}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
@@ -201,6 +201,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
initiallySelectedVariableName={editorOpen.variableName}
|
||||
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
|
||||
onComputeAllVariableNames={onComputeAllVariableNames}
|
||||
hotReloadPreviewButtonProps={null}
|
||||
/>
|
||||
)}
|
||||
{editorOpen && project && objectGroup && (
|
||||
|
@@ -91,6 +91,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
shouldCreateInitiallySelectedVariable={
|
||||
editorOpen.shouldCreate || false
|
||||
}
|
||||
hotReloadPreviewButtonProps={null}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
@@ -103,7 +103,6 @@ const components = {
|
||||
atlasResource: AtlasResourceField,
|
||||
spineResource: SpineResourceField,
|
||||
color: ColorExpressionField,
|
||||
police: DefaultField, //TODO
|
||||
forceMultiplier: ForceMultiplierField,
|
||||
sceneName: SceneNameField,
|
||||
layerEffectName: LayerEffectNameField,
|
||||
|
@@ -115,6 +115,7 @@ import {
|
||||
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
|
||||
import LocalVariablesDialog from '../VariablesList/LocalVariablesDialog';
|
||||
import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog';
|
||||
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -149,6 +150,7 @@ type Props = {|
|
||||
onBeginCreateEventsFunction: () => void,
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
isActive: boolean,
|
||||
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|
||||
|};
|
||||
|
||||
type ComponentProps = {|
|
||||
@@ -1832,6 +1834,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
resourceManagementProps,
|
||||
onCreateEventsFunction,
|
||||
tutorials,
|
||||
hotReloadPreviewButtonProps,
|
||||
} = this.props;
|
||||
if (!project) return null;
|
||||
|
||||
@@ -2156,6 +2159,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
open
|
||||
onCancel={() => this.editLayoutVariables(false)}
|
||||
onApply={() => this.editLayoutVariables(false)}
|
||||
hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
|
||||
/>
|
||||
)}
|
||||
{this.state.textEditedEvent && (
|
||||
|
@@ -4,7 +4,10 @@ import BrowserPreviewErrorDialog from './BrowserPreviewErrorDialog';
|
||||
import BrowserS3FileSystem from '../BrowserS3FileSystem';
|
||||
import { findGDJS } from '../../../GameEngineFinder/BrowserS3GDJSFinder';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { type PreviewOptions } from '../../PreviewLauncher.flow';
|
||||
import {
|
||||
type PreviewOptions,
|
||||
type PreviewLauncherProps,
|
||||
} from '../../PreviewLauncher.flow';
|
||||
import { getBaseUrl } from '../../../Utils/GDevelopServices/Preview';
|
||||
import { makeTimestampedId } from '../../../Utils/TimestampedId';
|
||||
import {
|
||||
@@ -16,17 +19,13 @@ import Window from '../../../Utils/Window';
|
||||
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
|
||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type State = {|
|
||||
error: ?Error,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
getIncludeFileHashs: () => { [string]: number },
|
||||
onExport?: () => void,
|
||||
|};
|
||||
|
||||
let nextPreviewWindowId = 0;
|
||||
|
||||
/**
|
||||
@@ -60,7 +59,7 @@ export const immediatelyOpenNewPreviewWindow = (
|
||||
};
|
||||
|
||||
export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
Props,
|
||||
PreviewLauncherProps,
|
||||
State
|
||||
> {
|
||||
canDoNetworkPreview = () => false;
|
||||
@@ -163,7 +162,11 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
previewExportOptions.setExternalLayoutName(externalLayout.getName());
|
||||
}
|
||||
|
||||
previewExportOptions.useWindowMessageDebuggerClient();
|
||||
if (isNativeMobileApp()) {
|
||||
previewExportOptions.useMinimalDebuggerClient();
|
||||
} else {
|
||||
previewExportOptions.useWindowMessageDebuggerClient();
|
||||
}
|
||||
|
||||
// Scripts generated from extensions keep the same URL even after being modified.
|
||||
// Use a cache bursting parameter to force the browser to reload them.
|
||||
@@ -174,6 +177,13 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
);
|
||||
|
||||
previewExportOptions.setNativeMobileApp(isNativeMobileApp());
|
||||
previewExportOptions.setGDevelopVersionWithHash(getIDEVersionWithHash());
|
||||
previewExportOptions.setCrashReportUploadLevel(
|
||||
this.props.crashReportUploadLevel
|
||||
);
|
||||
previewExportOptions.setPreviewContext(this.props.previewContext);
|
||||
previewExportOptions.setProjectTemplateSlug(project.getTemplateSlug());
|
||||
previewExportOptions.setSourceGameId(this.props.sourceGameId);
|
||||
|
||||
if (previewOptions.fallbackAuthor) {
|
||||
previewExportOptions.setFallbackAuthor(
|
||||
|
@@ -8,7 +8,10 @@ import { timeFunction } from '../../../Utils/TimeFunction';
|
||||
import { findGDJS } from '../../../GameEngineFinder/LocalGDJSFinder';
|
||||
import LocalNetworkPreviewDialog from './LocalNetworkPreviewDialog';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { type PreviewOptions } from '../../PreviewLauncher.flow';
|
||||
import {
|
||||
type PreviewOptions,
|
||||
type PreviewLauncherProps,
|
||||
} from '../../PreviewLauncher.flow';
|
||||
import SubscriptionChecker, {
|
||||
type SubscriptionCheckerInterface,
|
||||
} from '../../../Profile/Subscription/SubscriptionChecker';
|
||||
@@ -17,16 +20,12 @@ import {
|
||||
localPreviewDebuggerServer,
|
||||
} from './LocalPreviewDebuggerServer';
|
||||
import Window from '../../../Utils/Window';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
const electron = optionalRequire('electron');
|
||||
const path = optionalRequire('path');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
getIncludeFileHashs: () => { [string]: number },
|
||||
onExport?: () => void,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
networkPreviewDialogOpen: boolean,
|
||||
networkPreviewHost: ?string,
|
||||
@@ -47,7 +46,7 @@ type State = {|
|
||||
|};
|
||||
|
||||
export default class LocalPreviewLauncher extends React.Component<
|
||||
Props,
|
||||
PreviewLauncherProps,
|
||||
State
|
||||
> {
|
||||
canDoNetworkPreview = () => true;
|
||||
@@ -249,6 +248,17 @@ export default class LocalPreviewLauncher extends React.Component<
|
||||
previewExportOptions.setFullLoadingScreen(
|
||||
previewOptions.fullLoadingScreen
|
||||
);
|
||||
previewExportOptions.setGDevelopVersionWithHash(
|
||||
getIDEVersionWithHash()
|
||||
);
|
||||
previewExportOptions.setCrashReportUploadLevel(
|
||||
this.props.crashReportUploadLevel
|
||||
);
|
||||
previewExportOptions.setPreviewContext(this.props.previewContext);
|
||||
previewExportOptions.setProjectTemplateSlug(
|
||||
project.getTemplateSlug()
|
||||
);
|
||||
previewExportOptions.setSourceGameId(this.props.sourceGameId);
|
||||
|
||||
if (previewOptions.fallbackAuthor) {
|
||||
previewExportOptions.setFallbackAuthor(
|
||||
|
@@ -22,6 +22,9 @@ export type PreviewOptions = {|
|
||||
|
||||
/** The props that PreviewLauncher must support */
|
||||
export type PreviewLauncherProps = {|
|
||||
crashReportUploadLevel: string,
|
||||
previewContext: string,
|
||||
sourceGameId: string,
|
||||
getIncludeFileHashs: () => { [string]: number },
|
||||
onExport: () => void,
|
||||
|};
|
||||
|
@@ -1,8 +1,5 @@
|
||||
// @flow
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import { getGravatarUrl } from '../../UI/GravatarUrl';
|
||||
import * as React from 'react';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
@@ -27,70 +24,14 @@ import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import LeftLoader from '../../UI/LeftLoader';
|
||||
import TextField from '../../UI/TextField';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import Trash from '../../UI/CustomSvgIcons/Trash';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
import SelectField from '../../UI/SelectField';
|
||||
import SelectOption from '../../UI/SelectOption';
|
||||
import Form from '../../UI/Form';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
|
||||
|
||||
export const emailRegex = /^(.+)@(.+)$/;
|
||||
|
||||
const getTranslatableLevel = (level: Level) => {
|
||||
switch (level) {
|
||||
case 'owner':
|
||||
return t`Owner`;
|
||||
case 'writer':
|
||||
return t`Read & Write`;
|
||||
case 'reader':
|
||||
return t`Read only`;
|
||||
default:
|
||||
return level;
|
||||
}
|
||||
};
|
||||
|
||||
const UserLine = ({
|
||||
username,
|
||||
email,
|
||||
level,
|
||||
onDelete,
|
||||
disabled,
|
||||
}: {|
|
||||
username: ?string,
|
||||
email: string,
|
||||
level: ?Level,
|
||||
onDelete?: () => void,
|
||||
disabled?: boolean,
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Line justifyContent="space-between">
|
||||
<Line noMargin expand>
|
||||
<Avatar src={getGravatarUrl(email, { size: 40 })} />
|
||||
<Column expand justifyContent="flex-end">
|
||||
{username && <Text noMargin>{username}</Text>}
|
||||
<Text noMargin color="secondary">
|
||||
{email}
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
<Column>
|
||||
{!!level && (
|
||||
<Text color="secondary">{i18n._(getTranslatableLevel(level))}</Text>
|
||||
)}
|
||||
</Column>
|
||||
{onDelete && (
|
||||
<Column noMargin>
|
||||
<IconButton size="small" onClick={onDelete} disabled={disabled}>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</Column>
|
||||
)}
|
||||
</Line>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
import UserLine from '../../UI/User/UserLine';
|
||||
import { getTranslatableLevel } from '../../Utils/AclUtils';
|
||||
import { emailRegex } from '../../Utils/EmailUtils';
|
||||
|
||||
const getEmailErrorText = (addError: ?string) => {
|
||||
switch (addError) {
|
||||
@@ -449,7 +390,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
|
||||
</ColumnStackLayout>
|
||||
{showCollaboratorAddDialog && (
|
||||
<Dialog
|
||||
title="Add a collaborator"
|
||||
title={<Trans>Add a collaborator</Trans>}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Back</Trans>}
|
||||
|
@@ -20,11 +20,12 @@ import {
|
||||
import IconButton from '../UI/IconButton';
|
||||
import ChevronArrowLeft from '../UI/CustomSvgIcons/ChevronArrowLeft';
|
||||
import ChevronArrowRight from '../UI/CustomSvgIcons/ChevronArrowRight';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import { Column, Line, Spacer } from '../UI/Grid';
|
||||
import Text from '../UI/Text';
|
||||
import Paper from '../UI/Paper';
|
||||
import BackgroundText from '../UI/BackgroundText';
|
||||
import { getDefaultRegisterGamePropertiesFromProject } from '../Utils/UseGameAndBuildsManager';
|
||||
import UserEarnings from './Monetization/UserEarnings';
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
@@ -220,13 +221,13 @@ const GamesList = ({ project, games, onRefreshGames, onOpenGameId }: Props) => {
|
||||
|
||||
return (
|
||||
<ColumnStackLayout noMargin>
|
||||
{!isGameRegistering && (
|
||||
<GameRegistration
|
||||
project={project}
|
||||
hideLoader
|
||||
onGameRegistered={onRefreshGames}
|
||||
/>
|
||||
)}
|
||||
<UserEarnings />
|
||||
<Spacer />
|
||||
<Line noMargin>
|
||||
<Text size="section-title" noMargin>
|
||||
<Trans>Published games</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line noMargin alignItems="center">
|
||||
<Column noMargin expand>
|
||||
<SearchBar
|
||||
@@ -257,7 +258,13 @@ const GamesList = ({ project, games, onRefreshGames, onOpenGameId }: Props) => {
|
||||
<ChevronArrowRight />
|
||||
</IconButton>
|
||||
</Line>
|
||||
|
||||
{!isGameRegistering && (
|
||||
<GameRegistration
|
||||
project={project}
|
||||
hideLoader
|
||||
onGameRegistered={onRefreshGames}
|
||||
/>
|
||||
)}
|
||||
{displayedGames.length > 0 ? (
|
||||
displayedGames.map(game => (
|
||||
<GameCard
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user