mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0a77e0264d | ||
![]() |
4cc8d15f4b | ||
![]() |
a2f0b3384e | ||
![]() |
432ffebf80 | ||
![]() |
165470d9c1 | ||
![]() |
53eab0ed62 | ||
![]() |
649a36ac30 | ||
![]() |
a3afd983ba | ||
![]() |
f8c0417210 | ||
![]() |
8f5fe159d7 | ||
![]() |
3daebaeb05 | ||
![]() |
1ba1e8a2b7 | ||
![]() |
36679eb76b | ||
![]() |
c8132ba864 | ||
![]() |
141a3d2b61 | ||
![]() |
dbe0343c67 | ||
![]() |
0983ac59a8 | ||
![]() |
9710360180 | ||
![]() |
43af1f34df | ||
![]() |
fd193e89fc | ||
![]() |
19dcaacafb | ||
![]() |
90721c4059 | ||
![]() |
6c118eaa53 | ||
![]() |
2b00c90356 | ||
![]() |
efe37cc3bf | ||
![]() |
bcedbdc0c3 | ||
![]() |
f63a16161f | ||
![]() |
c215a8c6bf | ||
![]() |
225884dfe4 | ||
![]() |
5d42956669 | ||
![]() |
977bbdde89 | ||
![]() |
73357c830d | ||
![]() |
c619f94277 | ||
![]() |
c5dabf9b6e | ||
![]() |
cc4303e21e | ||
![]() |
566aadafd9 | ||
![]() |
9cd96461d7 | ||
![]() |
0395b73f4a | ||
![]() |
bcc40d4728 |
@@ -21,6 +21,12 @@ vector<gd::String> CommentEvent::GetAllSearchableStrings() const {
|
||||
return allSearchableStrings;
|
||||
}
|
||||
|
||||
bool CommentEvent::ReplaceAllSearchableStrings(
|
||||
std::vector<gd::String> newSearchableString) {
|
||||
SetComment(newSearchableString[0]);
|
||||
return newSearchableString[0] == com1;
|
||||
}
|
||||
|
||||
void CommentEvent::SerializeTo(SerializerElement &element) const {
|
||||
element.AddChild("color")
|
||||
.SetAttribute("r", r)
|
||||
|
@@ -47,6 +47,8 @@ class GD_CORE_API CommentEvent : public gd::BaseEvent {
|
||||
void SetComment(const gd::String& comment) { com1 = comment; }
|
||||
|
||||
virtual std::vector<gd::String> GetAllSearchableStrings() const;
|
||||
virtual bool ReplaceAllSearchableStrings(
|
||||
std::vector<gd::String> newSearchableString);
|
||||
|
||||
virtual void SerializeTo(SerializerElement& element) const;
|
||||
virtual void UnserializeFrom(gd::Project& project,
|
||||
|
@@ -27,6 +27,12 @@ vector<gd::String> GroupEvent::GetAllSearchableStrings() const {
|
||||
return allSearchableStrings;
|
||||
}
|
||||
|
||||
bool GroupEvent::ReplaceAllSearchableStrings(
|
||||
std::vector<gd::String> newSearchableString) {
|
||||
SetName(newSearchableString[0]);
|
||||
return newSearchableString[0] == name;
|
||||
}
|
||||
|
||||
void GroupEvent::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("name", name);
|
||||
element.SetAttribute("source", source);
|
||||
@@ -69,5 +75,4 @@ void GroupEvent::SetBackgroundColor(unsigned int colorR_,
|
||||
colorB = colorB_;
|
||||
}
|
||||
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -107,6 +107,8 @@ class GD_CORE_API GroupEvent : public gd::BaseEvent {
|
||||
virtual gd::EventsList& GetSubEvents() { return events; };
|
||||
|
||||
virtual std::vector<gd::String> GetAllSearchableStrings() const;
|
||||
virtual bool ReplaceAllSearchableStrings(
|
||||
std::vector<gd::String> newSearchableString);
|
||||
|
||||
virtual void SerializeTo(SerializerElement& element) const;
|
||||
virtual void UnserializeFrom(gd::Project& project,
|
||||
|
@@ -126,6 +126,11 @@ class GD_CORE_API BaseEvent {
|
||||
return noSearchableStrings;
|
||||
};
|
||||
|
||||
virtual bool ReplaceAllSearchableStrings(
|
||||
std::vector<gd::String> newSearchableString) {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Return a list of all expressions of the event, each with their associated metadata.
|
||||
* \note Used to preprocess or search in the expressions of the event.
|
||||
|
@@ -236,6 +236,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
|
||||
.AddParameter("expression", _("Camera number (default : 0)"), "", true)
|
||||
.SetDefaultValue("0");
|
||||
|
||||
// TODO Deprecated: hide this action in a future release.
|
||||
extension
|
||||
.AddAction(
|
||||
"FixCamera",
|
||||
@@ -268,12 +269,37 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
|
||||
.SetDefaultValue("0")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddAction(
|
||||
"ClampCamera",
|
||||
_("Enforce camera boundaries"),
|
||||
_("Enforce camera boundaries by moving the camera back inside specified boundaries."),
|
||||
_("Enforce camera boundaries (left: _PARAM1_, top: _PARAM2_ "
|
||||
"right: _PARAM3_, bottom: _PARAM4_, layer: _PARAM5_)"),
|
||||
"",
|
||||
"res/actions/camera24.png",
|
||||
"res/actions/camera.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("expression",
|
||||
_("Left bound X Position"))
|
||||
.AddParameter("expression",
|
||||
_("Top bound Y Position"))
|
||||
.AddParameter("expression",
|
||||
_("Right bound X Position"))
|
||||
.AddParameter("expression",
|
||||
_("Bottom bound Y Position"))
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
|
||||
.SetDefaultValue("\"\"")
|
||||
.AddParameter("expression", _("Camera number (default : 0)"), "", true)
|
||||
.SetDefaultValue("0")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddAction(
|
||||
"CentreCamera",
|
||||
_("Center the camera on an object"),
|
||||
_("Center the camera on the specified object."),
|
||||
_("Center camera on _PARAM1_ (layer: _PARAM3_, camera: _PARAM4_)"),
|
||||
_("Center camera on _PARAM1_ (layer: _PARAM3_)"),
|
||||
"",
|
||||
"res/actions/camera24.png",
|
||||
"res/actions/camera.png")
|
||||
|
@@ -199,6 +199,18 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddCondition("Width",
|
||||
_("Width"),
|
||||
_("Compare the width of a Sprite object."),
|
||||
_("the width"),
|
||||
_("Size"),
|
||||
"res/conditions/scaleWidth24.png",
|
||||
"res/conditions/scaleWidth.png")
|
||||
|
||||
.AddParameter("object", _("Object"), "Sprite")
|
||||
.UseStandardRelationalOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("ChangeHeight",
|
||||
_("Height"),
|
||||
_("Change the height of a Sprite object."),
|
||||
@@ -211,6 +223,31 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddCondition("Height",
|
||||
_("Height"),
|
||||
_("Compare the height of a Sprite object."),
|
||||
_("the height"),
|
||||
_("Size"),
|
||||
"res/conditions/scaleHeight24.png",
|
||||
"res/conditions/scaleHeight.png")
|
||||
|
||||
.AddParameter("object", _("Object"), "Sprite")
|
||||
.UseStandardRelationalOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("SetSize",
|
||||
_("Size"),
|
||||
_("Change the size of an object."),
|
||||
_("Change the size of _PARAM0_: set to _PARAM1_x_PARAM2_"),
|
||||
_("Size"),
|
||||
"res/actions/scale24.png",
|
||||
"res/actions/scale.png")
|
||||
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("expression", _("Width"))
|
||||
.AddParameter("expression", _("Height"))
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddCondition(
|
||||
"Animation",
|
||||
_("Current animation"),
|
||||
|
@@ -542,7 +542,8 @@ void EventsRefactorer::ReplaceStringInEvents(gd::ObjectsContainer& project,
|
||||
gd::String newString,
|
||||
bool matchCase,
|
||||
bool inConditions,
|
||||
bool inActions) {
|
||||
bool inActions,
|
||||
bool inEventStrings) {
|
||||
for (std::size_t i = 0; i < events.size(); ++i) {
|
||||
if (inConditions) {
|
||||
vector<gd::InstructionsList*> conditionsVectors =
|
||||
@@ -571,6 +572,11 @@ void EventsRefactorer::ReplaceStringInEvents(gd::ObjectsContainer& project,
|
||||
}
|
||||
}
|
||||
|
||||
if (inEventStrings) {
|
||||
bool eventStringModified = ReplaceStringInEventSearchableStrings(
|
||||
project, layout, events[i], toReplace, newString, matchCase);
|
||||
}
|
||||
|
||||
if (events[i].CanHaveSubEvents())
|
||||
ReplaceStringInEvents(project,
|
||||
layout,
|
||||
@@ -579,7 +585,8 @@ void EventsRefactorer::ReplaceStringInEvents(gd::ObjectsContainer& project,
|
||||
newString,
|
||||
matchCase,
|
||||
inConditions,
|
||||
inActions);
|
||||
inActions,
|
||||
inEventStrings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,6 +684,29 @@ bool EventsRefactorer::ReplaceStringInConditions(
|
||||
return somethingModified;
|
||||
}
|
||||
|
||||
bool EventsRefactorer::ReplaceStringInEventSearchableStrings(
|
||||
gd::ObjectsContainer& project,
|
||||
gd::ObjectsContainer& layout,
|
||||
gd::BaseEvent& event,
|
||||
gd::String toReplace,
|
||||
gd::String newString,
|
||||
bool matchCase) {
|
||||
vector<gd::String> newEventStrings;
|
||||
vector<gd::String> stringEvent = event.GetAllSearchableStrings();
|
||||
|
||||
for (std::size_t sNb = 0; sNb < stringEvent.size(); ++sNb) {
|
||||
gd::String newStringEvent =
|
||||
matchCase ? stringEvent[sNb].FindAndReplace(toReplace, newString, true)
|
||||
: ReplaceAllOccurencesCaseUnsensitive(
|
||||
stringEvent[sNb], toReplace, newString);
|
||||
newEventStrings.push_back(newStringEvent);
|
||||
}
|
||||
|
||||
bool somethingModified = event.ReplaceAllSearchableStrings(newEventStrings);
|
||||
|
||||
return somethingModified;
|
||||
}
|
||||
|
||||
vector<EventsSearchResult> EventsRefactorer::SearchInEvents(
|
||||
const gd::Platform& platform,
|
||||
gd::EventsList& events,
|
||||
|
@@ -117,7 +117,8 @@ class GD_CORE_API EventsRefactorer {
|
||||
gd::String newString,
|
||||
bool matchCase,
|
||||
bool inConditions,
|
||||
bool inActions);
|
||||
bool inActions,
|
||||
bool inEventString);
|
||||
|
||||
virtual ~EventsRefactorer(){};
|
||||
|
||||
@@ -208,6 +209,20 @@ class GD_CORE_API EventsRefactorer {
|
||||
gd::String newString,
|
||||
bool matchCase);
|
||||
|
||||
/**
|
||||
* Replace all occurrences of a gd::String in strings of events (for example:
|
||||
* comments and name of groups).
|
||||
*
|
||||
* \return true if something was modified.
|
||||
*/
|
||||
static bool ReplaceStringInEventSearchableStrings(
|
||||
gd::ObjectsContainer& project,
|
||||
gd::ObjectsContainer& layout,
|
||||
gd::BaseEvent& event,
|
||||
gd::String toReplace,
|
||||
gd::String newString,
|
||||
bool matchCase);
|
||||
|
||||
static bool SearchStringInFormattedText(const gd::Platform& platform,
|
||||
gd::Instruction& instruction,
|
||||
gd::String search,
|
||||
|
@@ -536,17 +536,23 @@ void Project::UnserializeFrom(const SerializerElement& element) {
|
||||
authorIds.push_back(authorIdsElement.GetChild(i).GetStringValue());
|
||||
}
|
||||
|
||||
categories.clear();
|
||||
auto& categoriesElement = propElement.GetChild("categories");
|
||||
categoriesElement.ConsiderAsArray();
|
||||
for (std::size_t i = 0; i < categoriesElement.GetChildrenCount(); ++i) {
|
||||
categories.push_back(categoriesElement.GetChild(i).GetStringValue());
|
||||
}
|
||||
|
||||
auto& playableDevicesElement = propElement.GetChild("playableDevices");
|
||||
playableDevicesElement.ConsiderAsArray();
|
||||
for (std::size_t i = 0; i < playableDevicesElement.GetChildrenCount(); ++i) {
|
||||
const auto& playableDevice = playableDevicesElement.GetChild(i).GetStringValue();
|
||||
const auto& playableDevice =
|
||||
playableDevicesElement.GetChild(i).GetStringValue();
|
||||
if (playableDevice == "keyboard") {
|
||||
isPlayableWithKeyboard = true;
|
||||
}
|
||||
else if (playableDevice == "gamepad") {
|
||||
} else if (playableDevice == "gamepad") {
|
||||
isPlayableWithGamepad = true;
|
||||
}
|
||||
else if (playableDevice == "mobile") {
|
||||
} else if (playableDevice == "mobile") {
|
||||
isPlayableWithMobile = true;
|
||||
}
|
||||
}
|
||||
@@ -751,6 +757,12 @@ void Project::SerializeTo(SerializerElement& element) const {
|
||||
authorIdsElement.AddChild("").SetStringValue(authorId);
|
||||
}
|
||||
|
||||
auto& categoriesElement = propElement.AddChild("categories");
|
||||
categoriesElement.ConsiderAsArray();
|
||||
for (const auto& category : categories) {
|
||||
categoriesElement.AddChild("").SetStringValue(category);
|
||||
}
|
||||
|
||||
auto& playableDevicesElement = propElement.AddChild("playableDevices");
|
||||
playableDevicesElement.ConsiderAsArray();
|
||||
if (isPlayableWithKeyboard) {
|
||||
@@ -938,6 +950,8 @@ Project& Project::operator=(const Project& other) {
|
||||
|
||||
void Project::Init(const gd::Project& game) {
|
||||
name = game.name;
|
||||
categories = game.categories;
|
||||
description = game.description;
|
||||
firstLayout = game.firstLayout;
|
||||
version = game.version;
|
||||
windowWidth = game.windowWidth;
|
||||
|
@@ -65,6 +65,16 @@ class GD_CORE_API Project : public ObjectsContainer {
|
||||
*/
|
||||
const gd::String& GetName() const { return name; }
|
||||
|
||||
/**
|
||||
* \brief Get the categories/genres of the project.
|
||||
*/
|
||||
const std::vector<gd::String>& GetCategories() const { return categories; };
|
||||
|
||||
/**
|
||||
* \brief Get the categories of the project, to modify them (non-const).
|
||||
*/
|
||||
std::vector<gd::String>& GetCategories() { return categories; };
|
||||
|
||||
/**
|
||||
* \brief Change the project description
|
||||
*/
|
||||
@@ -967,6 +977,8 @@ class GD_CORE_API Project : public ObjectsContainer {
|
||||
gd::String author; ///< Game author name, for publishing purpose.
|
||||
std::vector<gd::String>
|
||||
authorIds; ///< Game author ids, from GDevelop users DB.
|
||||
std::vector<gd::String>
|
||||
categories; ///< Game categories
|
||||
bool isPlayableWithKeyboard; ///< The project is playable with a keyboard.
|
||||
bool isPlayableWithGamepad; ///< The project is playable with a gamepad.
|
||||
bool isPlayableWithMobile; ///< The project is playable on a mobile.
|
||||
|
@@ -15,7 +15,7 @@ class SerializerElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Behavior that allows objects to be dragged with the mouse (or touch).
|
||||
* \brief Behavior that destroys object outside the screen.
|
||||
*/
|
||||
class GD_EXTENSION_API DestroyOutsideBehavior : public gd::Behavior {
|
||||
public:
|
||||
|
@@ -6,6 +6,42 @@ This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#include "DraggableBehavior.h"
|
||||
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/Project/PropertyDescriptor.h"
|
||||
#include "GDCore/Serialization/SerializerElement.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
|
||||
DraggableBehavior::DraggableBehavior() {}
|
||||
|
||||
void DraggableBehavior::InitializeContent(gd::SerializerElement& content) {
|
||||
content.SetAttribute("checkCollisionMask", true);
|
||||
}
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor> DraggableBehavior::GetProperties(
|
||||
const gd::SerializerElement& behaviorContent) const {
|
||||
std::map<gd::String, gd::PropertyDescriptor> properties;
|
||||
properties["checkCollisionMask"]
|
||||
.SetValue(behaviorContent.GetBoolAttribute("checkCollisionMask")
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean")
|
||||
.SetLabel(_("Do a precision check against the object's collision mask"))
|
||||
.SetDescription(
|
||||
_("Use the object (custom) collision mask instead of the bounding "
|
||||
"box, making the behavior more precise at the cost of "
|
||||
"reduced performance"));
|
||||
;
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
bool DraggableBehavior::UpdateProperty(gd::SerializerElement& behaviorContent,
|
||||
const gd::String& name,
|
||||
const gd::String& value) {
|
||||
if (name == "checkCollisionMask") {
|
||||
behaviorContent.SetAttribute("checkCollisionMask", (value != "0"));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
@@ -24,9 +24,20 @@ class GD_EXTENSION_API DraggableBehavior : public gd::Behavior {
|
||||
public:
|
||||
DraggableBehavior();
|
||||
virtual ~DraggableBehavior(){};
|
||||
virtual Behavior* Clone() const { return new DraggableBehavior(*this); }
|
||||
virtual Behavior* Clone() const override {
|
||||
return new DraggableBehavior(*this);
|
||||
}
|
||||
|
||||
private:
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual std::map<gd::String, gd::PropertyDescriptor> GetProperties(
|
||||
const gd::SerializerElement& behaviorContent) const override;
|
||||
virtual bool UpdateProperty(gd::SerializerElement& behaviorContent,
|
||||
const gd::String& name,
|
||||
const gd::String& value) override;
|
||||
#endif
|
||||
|
||||
virtual void InitializeContent(
|
||||
gd::SerializerElement& behaviorContent) override;
|
||||
};
|
||||
|
||||
#endif // DRAGGABLEBEHAVIOR_H
|
||||
|
@@ -14,9 +14,11 @@ namespace gdjs {
|
||||
* When the owner is being dragged, no other manager can start dragging it.
|
||||
*/
|
||||
_draggedByDraggableManager: DraggableManager | null = null;
|
||||
_checkCollisionMask: boolean;
|
||||
|
||||
constructor(runtimeScene, behaviorData, owner) {
|
||||
super(runtimeScene, behaviorData, owner);
|
||||
this._checkCollisionMask = behaviorData.checkCollisionMask ? true : false;
|
||||
}
|
||||
|
||||
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
|
||||
@@ -196,6 +198,14 @@ namespace gdjs {
|
||||
!draggableRuntimeBehavior.owner.insideObject(position[0], position[1])
|
||||
) {
|
||||
return false;
|
||||
} else if (
|
||||
draggableRuntimeBehavior._checkCollisionMask &&
|
||||
!draggableRuntimeBehavior.owner.isCollidingWithPoint(
|
||||
position[0],
|
||||
position[1]
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this._draggableBehavior) {
|
||||
// The previous best object to drag will not be dragged.
|
||||
|
@@ -34,20 +34,29 @@ describe('gdjs.DraggableRuntimeBehavior', function () {
|
||||
instances: [],
|
||||
});
|
||||
|
||||
var object = new gdjs.RuntimeObject(runtimeScene, {
|
||||
var object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
behaviors: [{ name: 'Behavior1', type: 'DraggableBehavior::Draggable' }],
|
||||
variables: [],
|
||||
effects: [],
|
||||
});
|
||||
var object2 = new gdjs.RuntimeObject(runtimeScene, {
|
||||
object.setCustomWidthAndHeight(10, 10);
|
||||
var object2 = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
behaviors: [{ name: 'Behavior1', type: 'DraggableBehavior::Draggable' }],
|
||||
behaviors: [
|
||||
{
|
||||
name: 'Behavior1',
|
||||
type: 'DraggableBehavior::Draggable',
|
||||
// @ts-ignore - properties are not typed
|
||||
checkCollisionMask: true,
|
||||
},
|
||||
],
|
||||
variables: [],
|
||||
effects: [],
|
||||
});
|
||||
object2.setCustomWidthAndHeight(10, 10);
|
||||
runtimeScene.addObject(object);
|
||||
runtimeScene.addObject(object2);
|
||||
|
||||
@@ -96,6 +105,70 @@ describe('gdjs.DraggableRuntimeBehavior', function () {
|
||||
expect(object.getY()).to.be(700);
|
||||
});
|
||||
|
||||
it('can drag an object without collision mask check', function () {
|
||||
object.setPosition(450, 500);
|
||||
object.setAngle(45);
|
||||
|
||||
// Dragged point is in the bounding box but not in hitbox
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onMouseMove(450, 500);
|
||||
runtimeGame
|
||||
.getInputManager()
|
||||
.onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onMouseMove(750, 600);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame
|
||||
.getInputManager()
|
||||
.onMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
expect(object.getX()).to.be(750);
|
||||
expect(object.getY()).to.be(600);
|
||||
|
||||
object.setAngle(0);
|
||||
});
|
||||
|
||||
it('can drag an object with collision mask check', function () {
|
||||
object2.setPosition(450, 500);
|
||||
object2.setAngle(45);
|
||||
|
||||
// Dragged point is in the bounding box but not in hitbox
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onMouseMove(450, 500);
|
||||
runtimeGame
|
||||
.getInputManager()
|
||||
.onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onMouseMove(750, 600);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame
|
||||
.getInputManager()
|
||||
.onMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
expect(object2.getX()).to.be(450);
|
||||
expect(object2.getY()).to.be(500);
|
||||
|
||||
// Dragged point is in the bounding box and in hitbox
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onMouseMove(455, 505);
|
||||
runtimeGame
|
||||
.getInputManager()
|
||||
.onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onMouseMove(855, 705);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame
|
||||
.getInputManager()
|
||||
.onMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
expect(object2.getX()).to.be(850);
|
||||
expect(object2.getY()).to.be(700);
|
||||
object2.setAngle(0);
|
||||
});
|
||||
|
||||
[false, true].forEach((firstInFront) => {
|
||||
it(`must drag the object in front (${
|
||||
firstInFront ? '1st object' : '2nd object'
|
||||
@@ -184,6 +257,65 @@ describe('gdjs.DraggableRuntimeBehavior', function () {
|
||||
expect(object.getX()).to.be(850);
|
||||
expect(object.getY()).to.be(700);
|
||||
});
|
||||
|
||||
it('can drag an object without collision mask check', function () {
|
||||
object.setPosition(450, 500);
|
||||
object.setAngle(45);
|
||||
|
||||
// Dragged point is in the bounding box but not in hitbox
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onTouchStart(0, 450, 500);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
runtimeGame.getInputManager().onTouchMove(0, 750, 600);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
runtimeGame.getInputManager().onTouchEnd(0);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
|
||||
expect(object.getX()).to.be(750);
|
||||
expect(object.getY()).to.be(600);
|
||||
|
||||
object.setAngle(0);
|
||||
});
|
||||
|
||||
it('can drag an object with collision mask check', function () {
|
||||
object2.setPosition(450, 500);
|
||||
object2.setAngle(45);
|
||||
|
||||
// Dragged point is in the bounding box but not in hitbox
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onTouchStart(0, 450, 500);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
runtimeGame.getInputManager().onTouchMove(0, 750, 600);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
runtimeGame.getInputManager().onTouchEnd(0);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
|
||||
expect(object2.getX()).to.be(450);
|
||||
expect(object2.getY()).to.be(500);
|
||||
|
||||
// Dragged point is in the bounding box but not in hitbox
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onTouchStart(0, 455, 505);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
runtimeGame.getInputManager().onTouchMove(0, 855, 705);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
runtimeGame.getInputManager().onTouchEnd(0);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
runtimeGame.getInputManager().onFrameEnded();
|
||||
|
||||
expect(object2.getX()).to.be(850);
|
||||
expect(object2.getY()).to.be(700);
|
||||
object2.setAngle(0);
|
||||
});
|
||||
|
||||
it('can drag 2 objects with multitouch', function () {
|
||||
runtimeGame.getInputManager().touchSimulateMouse(false);
|
||||
object.setPosition(450, 500);
|
||||
|
@@ -249,7 +249,7 @@ module.exports = {
|
||||
extension
|
||||
.addAction(
|
||||
'CreateBasicAccount',
|
||||
_('Create account with with email'),
|
||||
_('Create account with email'),
|
||||
_('Create an account with email and password as credentials.'),
|
||||
_('Create account with email _PARAM0_ and password _PARAM1_'),
|
||||
_('Authentication'),
|
||||
|
@@ -294,8 +294,8 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
|
||||
obj.AddCondition("NoMoreParticles",
|
||||
_("No more particles"),
|
||||
_("Check if the object does not emit particles "
|
||||
"anylonger, so as to destroy it for example."),
|
||||
_("_PARAM0_ does not emit anylonger."),
|
||||
"any longer, so as to destroy it for example."),
|
||||
_("_PARAM0_ does not emit any longer"),
|
||||
_("Common"),
|
||||
"CppPlatform/Extensions/particleSystemicon24.png",
|
||||
"CppPlatform/Extensions/particleSystemicon16.png")
|
||||
|
@@ -45,7 +45,7 @@ void DeclarePathfindingBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
_("Move to a position"),
|
||||
_("Move the object to a position"),
|
||||
_("Move _PARAM0_ to _PARAM3_;_PARAM4_"),
|
||||
"Movement on the path",
|
||||
_("Movement on the path"),
|
||||
"CppPlatform/Extensions/AStaricon24.png",
|
||||
"CppPlatform/Extensions/AStaricon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
@@ -61,7 +61,7 @@ void DeclarePathfindingBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
_("Path found"),
|
||||
_("Check if a path has been found."),
|
||||
_("A path has been found for _PARAM0_"),
|
||||
"Movement on the path",
|
||||
_("Movement on the path"),
|
||||
"CppPlatform/Extensions/AStaricon24.png",
|
||||
"CppPlatform/Extensions/AStaricon16.png")
|
||||
|
||||
@@ -74,7 +74,7 @@ void DeclarePathfindingBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
_("Destination reached"),
|
||||
_("Check if the destination was reached."),
|
||||
_("_PARAM0_ reached its destination"),
|
||||
"Movement on the path",
|
||||
_("Movement on the path"),
|
||||
"CppPlatform/Extensions/AStaricon24.png",
|
||||
"CppPlatform/Extensions/AStaricon16.png")
|
||||
|
||||
|
@@ -43,6 +43,7 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
std::make_shared<PlatformerObjectBehavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>());
|
||||
|
||||
// Deprecated, use IsMovingEvenALittle instead
|
||||
aut.AddCondition("IsMoving",
|
||||
_("Is moving"),
|
||||
_("Check if the object is moving (whether it is on the "
|
||||
@@ -53,9 +54,22 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"CppPlatform/Extensions/platformerobjecticon.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "PlatformerObjectBehavior")
|
||||
.SetHidden()
|
||||
.MarkAsSimple()
|
||||
.SetFunctionName("IsMoving");
|
||||
|
||||
aut.AddScopedCondition("IsMovingEvenALittle",
|
||||
_("Is moving"),
|
||||
_("Check if the object is moving (whether it is on the "
|
||||
"floor or in the air)."),
|
||||
_("_PARAM0_ is moving"),
|
||||
"",
|
||||
"CppPlatform/Extensions/platformerobjecticon.png",
|
||||
"CppPlatform/Extensions/platformerobjecticon.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "PlatformerObjectBehavior")
|
||||
.MarkAsSimple();
|
||||
|
||||
aut.AddCondition("IsOnFloor",
|
||||
_("Is on floor"),
|
||||
_("Check if the object is on a platform."),
|
||||
|
@@ -53,6 +53,8 @@ class PlatformBehaviorJsExtension : public gd::PlatformExtension {
|
||||
"PlatformBehavior::PlatformerObjectBehavior");
|
||||
|
||||
autConditions["PlatformBehavior::IsMoving"].SetFunctionName("isMoving");
|
||||
autConditions["PlatformBehavior::PlatformerObjectBehavior::IsMovingEvenALittle"]
|
||||
.SetFunctionName("isMovingEvenALittle");
|
||||
autConditions["PlatformBehavior::IsOnFloor"].SetFunctionName("isOnFloor");
|
||||
autConditions["PlatformBehavior::IsOnLadder"].SetFunctionName(
|
||||
"isOnLadder");
|
||||
|
@@ -24,6 +24,11 @@ namespace gdjs {
|
||||
isCollidingAnyPlatform: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* A very small value compare to 1 pixel, yet very huge compare to rounding errors.
|
||||
*/
|
||||
private static readonly epsilon = 2 ** -20;
|
||||
|
||||
// Behavior configuration
|
||||
|
||||
/** To achieve pixel-perfect precision when positioning object on platform or
|
||||
@@ -107,6 +112,8 @@ namespace gdjs {
|
||||
_overlappedJumpThru: Array<gdjs.PlatformRuntimeBehavior>;
|
||||
|
||||
private _hasReallyMoved: boolean = false;
|
||||
/** @deprecated use _hasReallyMoved instead */
|
||||
private _hasMovedAtLeastOnePixel: boolean = false;
|
||||
private _manager: gdjs.PlatformObjectsManager;
|
||||
|
||||
constructor(
|
||||
@@ -312,6 +319,11 @@ namespace gdjs {
|
||||
|
||||
//5) Track the movement
|
||||
this._hasReallyMoved =
|
||||
Math.abs(object.getX() - oldX) >
|
||||
PlatformerObjectRuntimeBehavior.epsilon ||
|
||||
Math.abs(object.getY() - oldY) >
|
||||
PlatformerObjectRuntimeBehavior.epsilon;
|
||||
this._hasMovedAtLeastOnePixel =
|
||||
Math.abs(object.getX() - oldX) >= 1 ||
|
||||
Math.abs(object.getY() - oldY) >= 1;
|
||||
this._lastDeltaY = object.getY() - oldY;
|
||||
@@ -1543,9 +1555,27 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Check if the Platformer Object is moving.
|
||||
*
|
||||
* When walking or climbing on a ladder,
|
||||
* a speed of less than one pixel per frame won't be detected.
|
||||
*
|
||||
* @returns Returns true if it is moving and false if not.
|
||||
* @deprecated use isMovingEvenALittle instead
|
||||
*/
|
||||
isMoving(): boolean {
|
||||
return (
|
||||
(this._hasMovedAtLeastOnePixel &&
|
||||
(this._currentSpeed !== 0 || this._state === this._onLadder)) ||
|
||||
this._jumping.getCurrentJumpSpeed() !== 0 ||
|
||||
this._currentFallSpeed !== 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platformer Object is moving.
|
||||
* @returns Returns true if it is moving and false if not.
|
||||
*/
|
||||
isMovingEvenALittle(): boolean {
|
||||
return (
|
||||
(this._hasReallyMoved &&
|
||||
(this._currentSpeed !== 0 || this._state === this._onLadder)) ||
|
||||
|
@@ -169,4 +169,108 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
|
||||
// Clean up - not mandatory but to avoid overloading the testing browser.
|
||||
runtimeScene.unloadScene();
|
||||
});
|
||||
|
||||
it('hides the DOM element when the object or layer is hidden', async () => {
|
||||
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
loadScene(runtimeScene);
|
||||
|
||||
// Make sure the renderer is created (to test the real DOM element creation/update)
|
||||
const gameContainer = document.createElement('div');
|
||||
runtimeGame.getRenderer().createStandardCanvas(gameContainer);
|
||||
|
||||
const object = makeTextInputRuntimeObject(runtimeScene);
|
||||
runtimeScene.addObject(object);
|
||||
|
||||
// Check that the DOM element was created
|
||||
const gameDomElementContainer = runtimeGame
|
||||
.getRenderer()
|
||||
.getDomElementContainer();
|
||||
if (!gameDomElementContainer)
|
||||
throw new Error(
|
||||
'Expected getDomElementContainer to return a valid container.'
|
||||
);
|
||||
|
||||
const inputElement = gameDomElementContainer.querySelector('input');
|
||||
if (!inputElement) throw new Error('Expected input element to be found');
|
||||
|
||||
// Check visibility of the DOM element is visible by default, if it should be visible
|
||||
// on the screen.
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('initial');
|
||||
|
||||
// Check visibility of the DOM element is updated at each frame,
|
||||
// according to the object visibility.
|
||||
object.hide(true);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('none');
|
||||
|
||||
object.hide(false);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('initial');
|
||||
|
||||
// Check visibility of the DOM element is updated at each frame,
|
||||
// according to the layer visibility.
|
||||
runtimeScene.getLayer('').show(false);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('none');
|
||||
|
||||
runtimeScene.getLayer('').show(true);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('initial');
|
||||
|
||||
// Clean up - not mandatory but to avoid overloading the testing browser.
|
||||
runtimeScene.unloadScene();
|
||||
});
|
||||
|
||||
it('hides the DOM element when the object is far from the camera', async () => {
|
||||
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
loadScene(runtimeScene);
|
||||
|
||||
// Make sure the renderer is created (to test the real DOM element creation/update)
|
||||
const gameContainer = document.createElement('div');
|
||||
runtimeGame.getRenderer().createStandardCanvas(gameContainer);
|
||||
|
||||
const object = makeTextInputRuntimeObject(runtimeScene);
|
||||
runtimeScene.addObject(object);
|
||||
|
||||
// Check that the DOM element was created
|
||||
const gameDomElementContainer = runtimeGame
|
||||
.getRenderer()
|
||||
.getDomElementContainer();
|
||||
if (!gameDomElementContainer)
|
||||
throw new Error(
|
||||
'Expected getDomElementContainer to return a valid container.'
|
||||
);
|
||||
|
||||
const inputElement = gameDomElementContainer.querySelector('input');
|
||||
if (!inputElement) throw new Error('Expected input element to be found');
|
||||
|
||||
// Check visibility of the DOM element is visible by default, if it should be visible
|
||||
// on the screen.
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('initial');
|
||||
|
||||
// Check visibility of the DOM element is updated at each frame,
|
||||
// according to the object position of screen.
|
||||
object.setX(-500); // -500 + 300 (object default width) = -200, still outside the camera.
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('none');
|
||||
|
||||
object.setWidth(600); // -500 + 600 = 100, inside the camera
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('initial');
|
||||
|
||||
runtimeScene.getLayer('').setCameraX(900);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('none');
|
||||
|
||||
runtimeScene.getLayer('').setCameraX(400);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(inputElement.style.display).to.be('initial');
|
||||
|
||||
// Clean up - not mandatory but to avoid overloading the testing browser.
|
||||
runtimeScene.unloadScene();
|
||||
});
|
||||
});
|
||||
|
@@ -111,7 +111,23 @@ namespace gdjs {
|
||||
updatePreRender() {
|
||||
if (!this._input) return;
|
||||
|
||||
// Hide the input entirely if the object is hidden.
|
||||
// Because this object is rendered as a DOM element (and not part of the PixiJS
|
||||
// scene graph), we have to do this manually.
|
||||
if (this._object.isHidden()) {
|
||||
this._input.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the input entirely if the layer is not visible.
|
||||
// Because this object is rendered as a DOM element (and not part of the PixiJS
|
||||
// scene graph), we have to do this manually.
|
||||
const layer = this._runtimeScene.getLayer(this._object.getLayer());
|
||||
if (!layer.isVisible()) {
|
||||
this._input.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeGame = this._runtimeScene.getGame();
|
||||
const runtimeGameRenderer = runtimeGame.getRenderer();
|
||||
const topLeftCanvasCoordinates = layer.convertInverseCoords(
|
||||
@@ -125,7 +141,7 @@ namespace gdjs {
|
||||
0
|
||||
);
|
||||
|
||||
// Hide the input entirely if not visible at all
|
||||
// Hide the input entirely if not visible at all.
|
||||
const isOutsideCanvas =
|
||||
bottomRightCanvasCoordinates[0] < 0 ||
|
||||
bottomRightCanvasCoordinates[1] < 0 ||
|
||||
@@ -136,7 +152,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
// Position the input on the container on top of the canvas
|
||||
// Position the input on the container on top of the canvas.
|
||||
const topLeftPageCoordinates = runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
|
||||
topLeftCanvasCoordinates
|
||||
);
|
||||
|
@@ -135,6 +135,20 @@ void DeclareTiledSpriteObjectExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("GetHeight")
|
||||
.SetIncludeFile("TiledSpriteObject/TiledSpriteObject.h");
|
||||
|
||||
obj.AddAction("SetSize",
|
||||
_("Size"),
|
||||
_("Modify the size of a Tiled Sprite."),
|
||||
_("Change the size of _PARAM0_: set to _PARAM1_x_PARAM2_"),
|
||||
_("Size"),
|
||||
"res/actions/scale24.png",
|
||||
"res/actions/scale.png")
|
||||
|
||||
.AddParameter("object", _("Object"), "TiledSprite")
|
||||
.AddParameter("expression", _("Width"))
|
||||
.AddParameter("expression", _("Height"))
|
||||
.SetFunctionName("SetSize")
|
||||
.SetIncludeFile("TiledSpriteObject/TiledSpriteObject.h");
|
||||
|
||||
// Deprecated: now available for all objects.
|
||||
obj.AddAction("Angle",
|
||||
_("Angle"),
|
||||
|
@@ -77,6 +77,11 @@ class TiledSpriteObjectJsExtension : public gd::PlatformExtension {
|
||||
.SetFunctionName("getHeight")
|
||||
.SetIncludeFile(
|
||||
"Extensions/TiledSpriteObject/tiledspriteruntimeobject.js");
|
||||
GetAllActionsForObject(
|
||||
"TiledSpriteObject::TiledSprite")["TiledSpriteObject::SetSize"]
|
||||
.SetFunctionName("setSize")
|
||||
.SetIncludeFile(
|
||||
"Extensions/TiledSpriteObject/tiledspriteruntimeobject.js");
|
||||
|
||||
// Deprecated: now available for all objects.
|
||||
GetAllActionsForObject(
|
||||
|
@@ -162,6 +162,16 @@ namespace gdjs {
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the size of the Tiled Sprite object.
|
||||
* @param width The new width.
|
||||
* @param height The new height.
|
||||
*/
|
||||
setSize(width: float, height: float): void {
|
||||
this.setWidth(width);
|
||||
this.setHeight(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the offset on the X-axis when displaying the image of the Tiled Sprite object.
|
||||
* @param xOffset The new offset on the X-axis.
|
||||
|
@@ -69,12 +69,21 @@ gd::String EventsCodeGenerator::GenerateEventsListCompleteFunctionCode(
|
||||
codeGenerator.GenerateAllConditionsBooleanDeclarations();
|
||||
|
||||
gd::String output =
|
||||
codeGenerator.GetCodeNamespace() + " = {};\n" + globalDeclarations +
|
||||
globalObjectLists + "\n" + globalConditionsBooleans + "\n\n" +
|
||||
// clang-format off
|
||||
codeGenerator.GetCodeNamespace() + " = {};\n" +
|
||||
globalDeclarations +
|
||||
globalObjectLists + "\n" +
|
||||
globalConditionsBooleans + "\n\n" +
|
||||
codeGenerator.GetCustomCodeOutsideMain() + "\n\n" +
|
||||
fullyQualifiedFunctionName + " = function(" + functionArgumentsCode +
|
||||
") {\n" + functionPreEventsCode + "\n" + globalObjectListsReset + "\n" +
|
||||
wholeEventsCode + "\n" + functionReturnCode + "\n" + "}\n";
|
||||
fullyQualifiedFunctionName + " = function(" +
|
||||
functionArgumentsCode +
|
||||
") {\n" +
|
||||
functionPreEventsCode + "\n" +
|
||||
globalObjectListsReset + "\n" +
|
||||
wholeEventsCode + "\n" +
|
||||
functionReturnCode + "\n" +
|
||||
"}\n";
|
||||
// clang-format on
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -256,7 +265,7 @@ gd::String EventsCodeGenerator::GenerateBehaviorEventsFunctionContext(
|
||||
// optimized getter for it.
|
||||
behaviorNamesMap += ConvertToStringExplicit(thisBehaviorName) + ": " +
|
||||
thisBehaviorName + "\n";
|
||||
|
||||
|
||||
// Add required behaviors from properties
|
||||
for (size_t i = 0; i < eventsBasedBehavior.GetPropertyDescriptors().GetCount(); i++)
|
||||
{
|
||||
@@ -271,7 +280,7 @@ gd::String EventsCodeGenerator::GenerateBehaviorEventsFunctionContext(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return GenerateEventsFunctionContext(
|
||||
parameters,
|
||||
onceTriggersVariable,
|
||||
|
@@ -4,6 +4,7 @@
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#include "EventsFunctionsExtensionCodeGenerator.h"
|
||||
|
||||
#include "EventsCodeGenerator.h"
|
||||
#include "GDCore/Tools/Log.h"
|
||||
|
||||
@@ -14,6 +15,16 @@ EventsFunctionsExtensionCodeGenerator::GenerateFreeEventsFunctionCompleteCode(
|
||||
const gd::String& codeNamespace,
|
||||
std::set<gd::String>& includeFiles,
|
||||
bool compilationForRuntime) {
|
||||
gd::String lifecycleCleanupCode =
|
||||
gd::String(R"jscode_template(
|
||||
if (typeof CODE_NAMESPACE !== "undefined") {
|
||||
CODE_NAMESPACE.registeredGdjsCallbacks.forEach(callback =>
|
||||
gdjs._unregisterCallback(callback)
|
||||
);
|
||||
}
|
||||
)jscode_template")
|
||||
.FindAndReplace("CODE_NAMESPACE", codeNamespace);
|
||||
|
||||
gd::String eventsFunctionCode =
|
||||
EventsCodeGenerator::GenerateEventsFunctionCode(project,
|
||||
eventsFunction,
|
||||
@@ -22,13 +33,21 @@ EventsFunctionsExtensionCodeGenerator::GenerateFreeEventsFunctionCompleteCode(
|
||||
compilationForRuntime);
|
||||
|
||||
gd::String lifecycleRegistrationCode = "";
|
||||
lifecycleRegistrationCode +=
|
||||
gd::String("CODE_NAMESPACE.registeredGdjsCallbacks = [];")
|
||||
.FindAndReplace("CODE_NAMESPACE", codeNamespace);
|
||||
|
||||
if (gd::EventsFunctionsExtension::IsExtensionLifecycleEventsFunction(
|
||||
eventsFunction.GetName())) {
|
||||
lifecycleRegistrationCode = GenerateLifecycleFunctionRegistrationCode(
|
||||
lifecycleRegistrationCode += GenerateLifecycleFunctionRegistrationCode(
|
||||
eventsFunction, codeNamespace);
|
||||
}
|
||||
|
||||
return eventsFunctionCode + "\n" + lifecycleRegistrationCode;
|
||||
// clang-format off
|
||||
return lifecycleCleanupCode + "\n" +
|
||||
eventsFunctionCode + "\n" +
|
||||
lifecycleRegistrationCode;
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
gd::String EventsFunctionsExtensionCodeGenerator::
|
||||
@@ -44,49 +63,39 @@ gd::String EventsFunctionsExtensionCodeGenerator::
|
||||
return "";
|
||||
}
|
||||
|
||||
gd::String lifecycleRegistrationTemplateCode = "";
|
||||
auto generateCallbackRegistrationCode =
|
||||
[&](const gd::String& registerFunctionName) {
|
||||
return gd::String(R"jscode_template(
|
||||
CODE_NAMESPACE.registeredGdjsCallbacks.push((runtimeScene) => {
|
||||
CODE_NAMESPACE.func(runtimeScene, runtimeScene);
|
||||
})
|
||||
gdjs.REGISTER_FUNCTION_NAME(CODE_NAMESPACE.registeredGdjsCallbacks[CODE_NAMESPACE.registeredGdjsCallbacks.length - 1]);
|
||||
)jscode_template")
|
||||
.FindAndReplace("CODE_NAMESPACE", codeNamespace)
|
||||
.FindAndReplace("REGISTER_FUNCTION_NAME", registerFunctionName);
|
||||
};
|
||||
|
||||
if (eventsFunctionName == "onFirstSceneLoaded") {
|
||||
lifecycleRegistrationTemplateCode += gd::String(R"jscode_template(
|
||||
gdjs.registerFirstRuntimeSceneLoadedCallback(function(runtimeScene) {
|
||||
CODE_NAMESPACE.func(runtimeScene, runtimeScene);
|
||||
});
|
||||
)jscode_template");
|
||||
return generateCallbackRegistrationCode(
|
||||
"registerFirstRuntimeSceneLoadedCallback");
|
||||
} else if (eventsFunctionName == "onSceneLoaded") {
|
||||
lifecycleRegistrationTemplateCode += gd::String(R"jscode_template(
|
||||
gdjs.registerRuntimeSceneLoadedCallback(function(runtimeScene) {
|
||||
CODE_NAMESPACE.func(runtimeScene, runtimeScene);
|
||||
});
|
||||
)jscode_template");
|
||||
return generateCallbackRegistrationCode(
|
||||
"registerRuntimeSceneLoadedCallback");
|
||||
} else if (eventsFunctionName == "onScenePreEvents") {
|
||||
lifecycleRegistrationTemplateCode += gd::String(R"jscode_template(
|
||||
gdjs.registerRuntimeScenePreEventsCallback(function(runtimeScene) {
|
||||
CODE_NAMESPACE.func(runtimeScene, runtimeScene);
|
||||
});
|
||||
)jscode_template");
|
||||
return generateCallbackRegistrationCode(
|
||||
"registerRuntimeScenePreEventsCallback");
|
||||
} else if (eventsFunctionName == "onScenePostEvents") {
|
||||
lifecycleRegistrationTemplateCode += gd::String(R"jscode_template(
|
||||
gdjs.registerRuntimeScenePostEventsCallback(function(runtimeScene) {
|
||||
CODE_NAMESPACE.func(runtimeScene, runtimeScene);
|
||||
});
|
||||
)jscode_template");
|
||||
return generateCallbackRegistrationCode(
|
||||
"registerRuntimeScenePostEventsCallback");
|
||||
} else if (eventsFunctionName == "onScenePaused") {
|
||||
lifecycleRegistrationTemplateCode += gd::String(R"jscode_template(
|
||||
gdjs.registerRuntimeScenePausedCallback(function(runtimeScene) {
|
||||
CODE_NAMESPACE.func(runtimeScene, runtimeScene);
|
||||
});
|
||||
)jscode_template");
|
||||
return generateCallbackRegistrationCode(
|
||||
"registerRuntimeScenePausedCallback");
|
||||
} else if (eventsFunctionName == "onSceneResumed") {
|
||||
lifecycleRegistrationTemplateCode += gd::String(R"jscode_template(
|
||||
gdjs.registerRuntimeSceneResumedCallback(function(runtimeScene) {
|
||||
CODE_NAMESPACE.func(runtimeScene, runtimeScene);
|
||||
});
|
||||
)jscode_template");
|
||||
return generateCallbackRegistrationCode(
|
||||
"registerRuntimeSceneResumedCallback");
|
||||
} else if (eventsFunctionName == "onSceneUnloading") {
|
||||
lifecycleRegistrationTemplateCode += gd::String(R"jscode_template(
|
||||
gdjs.registerRuntimeSceneUnloadingCallback(function(runtimeScene) {
|
||||
CODE_NAMESPACE.func(runtimeScene, runtimeScene);
|
||||
});
|
||||
)jscode_template");
|
||||
return generateCallbackRegistrationCode(
|
||||
"registerRuntimeSceneUnloadingCallback");
|
||||
} else {
|
||||
gd::LogError(
|
||||
"The code generation for this lifecycle events function is not handled "
|
||||
@@ -94,9 +103,6 @@ gdjs.registerRuntimeSceneUnloadingCallback(function(runtimeScene) {
|
||||
eventsFunctionName + "\"");
|
||||
return "";
|
||||
}
|
||||
|
||||
return lifecycleRegistrationTemplateCode.FindAndReplace("CODE_NAMESPACE",
|
||||
codeNamespace);
|
||||
}
|
||||
|
||||
} // namespace gdjs
|
||||
|
@@ -73,7 +73,9 @@ CameraExtension::CameraExtension() {
|
||||
"gdjs.evtTools.camera.getCameraHeight");
|
||||
|
||||
GetAllActions()["FixCamera"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.centerCameraWithinLimits");
|
||||
"gdjs.evtTools.camera.centerCameraWithinLimits"); // Deprecated
|
||||
GetAllActions()["ClampCamera"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.clampCamera");
|
||||
GetAllActions()["CentreCamera"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.centerCamera");
|
||||
|
||||
|
@@ -69,9 +69,12 @@ SpriteExtension::SpriteExtension() {
|
||||
spriteActions["ChangeWidth"]
|
||||
.SetFunctionName("setWidth")
|
||||
.SetGetter("getWidth");
|
||||
spriteConditions["Width"].SetFunctionName("getWidth");
|
||||
spriteActions["ChangeHeight"]
|
||||
.SetFunctionName("setHeight")
|
||||
.SetGetter("getHeight");
|
||||
spriteConditions["Height"].SetFunctionName("getHeight");
|
||||
spriteActions["SetSize"].SetFunctionName("setSize");
|
||||
spriteActions["TourneVersPos"].SetFunctionName("rotateTowardPosition");
|
||||
spriteActions["TourneVers"].SetFunctionName("turnTowardObject");
|
||||
spriteActions["ChangeColor"].SetFunctionName("setColor");
|
||||
|
@@ -162,7 +162,6 @@ namespace gdjs {
|
||||
if (!runtimeScene.hasLayer(layerName) || object == null) {
|
||||
return;
|
||||
}
|
||||
const layer = runtimeScene.getLayer(layerName);
|
||||
let xOffset = 0;
|
||||
let yOffset = 0;
|
||||
if (anticipateMove && !object.hasNoForces()) {
|
||||
@@ -172,10 +171,14 @@ namespace gdjs {
|
||||
xOffset = objectAverageForce.getX() * elapsedTimeInSeconds;
|
||||
yOffset = objectAverageForce.getY() * elapsedTimeInSeconds;
|
||||
}
|
||||
layer.setCameraX(object.getDrawableX() + object.getCenterX(), cameraId);
|
||||
layer.setCameraY(object.getDrawableY() + object.getCenterY(), cameraId);
|
||||
const layer = runtimeScene.getLayer(layerName);
|
||||
layer.setCameraX(object.getCenterXInScene() + xOffset, cameraId);
|
||||
layer.setCameraY(object.getCenterYInScene() + yOffset, cameraId);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated prefer using centerCamera and clampCamera.
|
||||
*/
|
||||
export const centerCameraWithinLimits = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
object: gdjs.RuntimeObject | null,
|
||||
@@ -187,35 +190,60 @@ namespace gdjs {
|
||||
layerName: string,
|
||||
cameraId: integer
|
||||
) {
|
||||
if (!runtimeScene.hasLayer(layerName) || object == null) {
|
||||
centerCamera(runtimeScene, object, anticipateMove, layerName, cameraId);
|
||||
clampCamera(
|
||||
runtimeScene,
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
layerName,
|
||||
cameraId
|
||||
);
|
||||
};
|
||||
|
||||
export const clampCamera = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
left: float,
|
||||
top: float,
|
||||
right: float,
|
||||
bottom: float,
|
||||
layerName: string,
|
||||
cameraId: integer
|
||||
) {
|
||||
if (!runtimeScene.hasLayer(layerName)) {
|
||||
return;
|
||||
}
|
||||
const layer = runtimeScene.getLayer(layerName);
|
||||
let xOffset = 0;
|
||||
let yOffset = 0;
|
||||
if (anticipateMove && !object.hasNoForces()) {
|
||||
const objectAverageForce = object.getAverageForce();
|
||||
const elapsedTimeInSeconds =
|
||||
object.getElapsedTime(runtimeScene) / 1000;
|
||||
xOffset = objectAverageForce.getX() * elapsedTimeInSeconds;
|
||||
yOffset = objectAverageForce.getY() * elapsedTimeInSeconds;
|
||||
}
|
||||
let newX = object.getDrawableX() + object.getCenterX() + xOffset;
|
||||
if (newX < left + layer.getCameraWidth(cameraId) / 2) {
|
||||
newX = left + layer.getCameraWidth(cameraId) / 2;
|
||||
}
|
||||
if (newX > right - layer.getCameraWidth(cameraId) / 2) {
|
||||
newX = right - layer.getCameraWidth(cameraId) / 2;
|
||||
}
|
||||
let newY = object.getDrawableY() + object.getCenterY() + yOffset;
|
||||
if (newY < top + layer.getCameraHeight(cameraId) / 2) {
|
||||
newY = top + layer.getCameraHeight(cameraId) / 2;
|
||||
}
|
||||
if (newY > bottom - layer.getCameraHeight(cameraId) / 2) {
|
||||
newY = bottom - layer.getCameraHeight(cameraId) / 2;
|
||||
}
|
||||
layer.setCameraX(newX, cameraId);
|
||||
layer.setCameraY(newY, cameraId);
|
||||
const cameraHalfWidth = layer.getCameraWidth(cameraId) / 2;
|
||||
const cameraHalfHeight = layer.getCameraHeight(cameraId) / 2;
|
||||
|
||||
const centerLeftBound = left + cameraHalfWidth;
|
||||
const centerRightBound = right - cameraHalfWidth;
|
||||
const centerTopBound = top + cameraHalfHeight;
|
||||
const centerBottomBound = bottom - cameraHalfHeight;
|
||||
|
||||
const cameraX =
|
||||
centerLeftBound < centerRightBound
|
||||
? gdjs.evtTools.common.clamp(
|
||||
layer.getCameraX(cameraId),
|
||||
centerLeftBound,
|
||||
centerRightBound
|
||||
)
|
||||
: // Center on the bounds when they are too small to fit the viewport.
|
||||
(centerLeftBound + centerRightBound) / 2;
|
||||
const cameraY =
|
||||
centerTopBound < centerBottomBound
|
||||
? gdjs.evtTools.common.clamp(
|
||||
layer.getCameraY(cameraId),
|
||||
centerTopBound,
|
||||
centerBottomBound
|
||||
)
|
||||
: // Center on the bounds when they are too small to fit the viewport.
|
||||
(centerTopBound + centerBottomBound) / 2;
|
||||
|
||||
layer.setCameraX(cameraX, cameraId);
|
||||
layer.setCameraY(cameraY, cameraId);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -330,6 +330,36 @@ namespace gdjs {
|
||||
gdjs.callbacksObjectDeletedFromScene.push(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unregister a callback.
|
||||
* This should not be used apart from the code generated from extensions
|
||||
* events functions, to handle hot-reloading.
|
||||
* In any other case, a callback should be registered once, and only once.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const _unregisterCallback = function (callback: unknown): void {
|
||||
const filterArrayInPlace = (array: unknown[]) => {
|
||||
for (let i = 0; i < array.length; ) {
|
||||
if (array[i] === callback) {
|
||||
array.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
filterArrayInPlace(callbacksFirstRuntimeSceneLoaded);
|
||||
filterArrayInPlace(callbacksRuntimeSceneLoaded);
|
||||
filterArrayInPlace(callbacksRuntimeScenePreEvents);
|
||||
filterArrayInPlace(callbacksRuntimeScenePostEvents);
|
||||
filterArrayInPlace(callbacksRuntimeScenePaused);
|
||||
filterArrayInPlace(callbacksRuntimeSceneResumed);
|
||||
filterArrayInPlace(callbacksRuntimeSceneUnloading);
|
||||
filterArrayInPlace(callbacksRuntimeSceneUnloaded);
|
||||
filterArrayInPlace(callbacksObjectDeletedFromScene);
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep this function until we're sure now client is using it anymore.
|
||||
* @deprecated
|
||||
@@ -341,23 +371,6 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove all the global callbacks that were registered previously.
|
||||
*
|
||||
* Should only be used for testing - this should never be used at runtime.
|
||||
*/
|
||||
export const clearGlobalCallbacks = function (): void {
|
||||
gdjs.callbacksFirstRuntimeSceneLoaded.length = 0;
|
||||
gdjs.callbacksRuntimeSceneLoaded.length = 0;
|
||||
gdjs.callbacksRuntimeScenePreEvents.length = 0;
|
||||
gdjs.callbacksRuntimeScenePostEvents.length = 0;
|
||||
gdjs.callbacksRuntimeScenePaused.length = 0;
|
||||
gdjs.callbacksRuntimeSceneResumed.length = 0;
|
||||
gdjs.callbacksRuntimeSceneUnloading.length = 0;
|
||||
gdjs.callbacksRuntimeSceneUnloaded.length = 0;
|
||||
gdjs.callbacksObjectDeletedFromScene.length = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the constructor of an object.
|
||||
*
|
||||
|
@@ -13,7 +13,7 @@ namespace gdjs {
|
||||
export class LayerPixiRenderer {
|
||||
_pixiContainer: PIXI.Container;
|
||||
|
||||
_layer: any;
|
||||
_layer: gdjs.Layer;
|
||||
_renderTexture: PIXI.RenderTexture | null = null;
|
||||
_lightingSprite: PIXI.Sprite | null = null;
|
||||
_runtimeSceneRenderer: any;
|
||||
@@ -72,10 +72,26 @@ namespace gdjs {
|
||||
const centerY =
|
||||
this._layer.getCameraX() * zoomFactor * sinValue +
|
||||
this._layer.getCameraY() * zoomFactor * cosValue;
|
||||
this._pixiContainer.position.x = -centerX;
|
||||
this._pixiContainer.position.y = -centerY;
|
||||
this._pixiContainer.position.x += this._layer.getWidth() / 2;
|
||||
this._pixiContainer.position.y += this._layer.getHeight() / 2;
|
||||
this._pixiContainer.position.x = this._layer.getWidth() / 2 - centerX;
|
||||
this._pixiContainer.position.y = this._layer.getHeight() / 2 - centerY;
|
||||
|
||||
if (
|
||||
this._layer.getRuntimeScene().getGame().getPixelsRounding() &&
|
||||
(cosValue === 0 || sinValue === 0) &&
|
||||
Number.isInteger(zoomFactor)
|
||||
) {
|
||||
// Camera rounding is important for pixel perfect games.
|
||||
// Otherwise the camera position fractional part is added to
|
||||
// the sprite one and it changes in which direction sprites are rounded.
|
||||
// It makes sprites rounding inconsistent with each other
|
||||
// and they seems to move on pixel left and right.
|
||||
this._pixiContainer.position.x = Math.round(
|
||||
this._pixiContainer.position.x
|
||||
);
|
||||
this._pixiContainer.position.y = Math.round(
|
||||
this._pixiContainer.position.y
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateVisibility(visible: boolean): void {
|
||||
|
@@ -2339,6 +2339,7 @@ namespace gdjs {
|
||||
*
|
||||
* The position should be in "world" coordinates, i.e use gdjs.Layer.convertCoords
|
||||
* if you need to pass the mouse or a touch position that you get from gdjs.InputManager.
|
||||
* To check if a point is inside the object collision mask, you can use `isCollidingWithPoint` instead.
|
||||
*
|
||||
*/
|
||||
insideObject(x: float, y: float): boolean {
|
||||
|
@@ -681,11 +681,16 @@ namespace gdjs {
|
||||
this._runtimeGame
|
||||
.getEffectsManager()
|
||||
.updatePreRender(object.getRendererEffects(), object);
|
||||
}
|
||||
}
|
||||
|
||||
// Perform pre-render update.
|
||||
object.updatePreRender(this);
|
||||
// Perform pre-render update only if the object is visible
|
||||
// (including if there is no visibility AABB returned previously).
|
||||
object.updatePreRender(this);
|
||||
}
|
||||
} else {
|
||||
// Perform pre-render update, always for objects not having an
|
||||
// associated renderer object (so it must handle visibility on its own).
|
||||
object.updatePreRender(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1186,6 +1186,17 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the size of the object.
|
||||
*
|
||||
* @param newWidth The new width of the object, in pixels.
|
||||
* @param newHeight The new height of the object, in pixels.
|
||||
*/
|
||||
setSize(newWidth: float, newHeight: float): void {
|
||||
this.setWidth(newWidth);
|
||||
this.setHeight(newHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X and Y axis of the object.
|
||||
*
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* an example to start a new object, take a look at gdjs.DummyRuntimeObject
|
||||
* in the Extensions folder.
|
||||
*/
|
||||
gdjs.TestRuntimeObject = class TestRuntimeObject extends gdjs.RuntimeObject {
|
||||
gdjs.TestRuntimeObject = class TestRuntimeObject extends gdjs.RuntimeObject {
|
||||
/** @type {float} */
|
||||
_customWidth = 0;
|
||||
/** @type {float} */
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
|
||||
getRendererObject() {
|
||||
return { visible: true };
|
||||
return null;
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
|
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* A test object doing nothing, with a fake getRendererObject method.
|
||||
*
|
||||
* It's only used for testing: if you want
|
||||
* an example to start a new object, take a look at gdjs.DummyRuntimeObject
|
||||
* in the Extensions folder.
|
||||
*/
|
||||
gdjs.TestRuntimeObjectWithFakeRenderer = class TestRuntimeObjectWithFakeRenderer extends gdjs.RuntimeObject {
|
||||
/**
|
||||
* @param {gdjs.RuntimeScene} runtimeScene
|
||||
* @param {ObjectData} objectData
|
||||
*/
|
||||
constructor(runtimeScene, objectData) {
|
||||
// *ALWAYS* call the base gdjs.RuntimeObject constructor.
|
||||
super(runtimeScene, objectData);
|
||||
|
||||
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
getRendererObject() {
|
||||
return { visible: true };
|
||||
}
|
||||
};
|
||||
|
||||
gdjs.registerObject(
|
||||
'TestObjectWithFakeRenderer::TestObjectWithFakeRenderer',
|
||||
gdjs.TestRuntimeObjectWithFakeRenderer
|
||||
);
|
@@ -6,6 +6,22 @@ describe('gdjs', function() {
|
||||
it('should define gdjs', function() {
|
||||
expect(gdjs).to.be.ok();
|
||||
});
|
||||
|
||||
it('should allow to register scene callbacks (and unregister them)', () => {
|
||||
const fakeCb = () => {};
|
||||
const fakeCb2 = () => {};
|
||||
gdjs.registerFirstRuntimeSceneLoadedCallback(fakeCb);
|
||||
gdjs.registerRuntimeScenePreEventsCallback(fakeCb2);
|
||||
|
||||
expect(gdjs.callbacksFirstRuntimeSceneLoaded).to.contain(fakeCb);
|
||||
expect(gdjs.callbacksRuntimeScenePreEvents).to.contain(fakeCb2);
|
||||
|
||||
gdjs._unregisterCallback(fakeCb);
|
||||
expect(gdjs.callbacksFirstRuntimeSceneLoaded).not.to.contain(fakeCb);
|
||||
|
||||
gdjs._unregisterCallback(fakeCb2);
|
||||
expect(gdjs.callbacksRuntimeScenePreEvents).not.to.contain(fakeCb2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gdjs.evtTools.object.twoListsTest', function() {
|
||||
@@ -78,7 +94,7 @@ describe('gdjs.evtTools.object.pickRandomObject', function() {
|
||||
var obj1C = new gdjs.RuntimeObject(runtimeScene, {name: "obj1", type: "", behaviors: [], effects: []});
|
||||
var obj2A = new gdjs.RuntimeObject(runtimeScene, {name: "obj2", type: "", behaviors: [], effects: []});
|
||||
var obj2B = new gdjs.RuntimeObject(runtimeScene, {name: "obj2", type: "", behaviors: [], effects: []});
|
||||
|
||||
|
||||
var map1 = new Hashtable();
|
||||
var list1 = [obj1A, obj1B, obj1C];
|
||||
map1.put("obj1", list1);
|
||||
@@ -87,7 +103,7 @@ describe('gdjs.evtTools.object.pickRandomObject', function() {
|
||||
expect(list1).to.have.length(1);
|
||||
expect(gdjs.evtTools.object.pickRandomObject(runtimeScene, map1)).to.be.ok();
|
||||
expect(list1).to.have.length(1);
|
||||
|
||||
|
||||
list1.length = 0;
|
||||
expect(gdjs.evtTools.object.pickRandomObject(runtimeScene, map1)).to.not.be.ok();
|
||||
|
||||
@@ -108,7 +124,7 @@ describe('gdjs.evtTools.object.pickOnly', function() {
|
||||
var obj1C = new gdjs.RuntimeObject(runtimeScene, {name: "obj1", type: "", behaviors: [], effects: []});
|
||||
var obj2A = new gdjs.RuntimeObject(runtimeScene, {name: "obj2", type: "", behaviors: [], effects: []});
|
||||
var obj2B = new gdjs.RuntimeObject(runtimeScene, {name: "obj2", type: "", behaviors: [], effects: []});
|
||||
|
||||
|
||||
var map1 = new Hashtable();
|
||||
map1.put("obj1", [obj1A, obj1B, obj1C]);
|
||||
|
||||
|
2
GDJS/tests/tests/effects.js
vendored
2
GDJS/tests/tests/effects.js
vendored
@@ -10,7 +10,7 @@ describe('gdjs.EffectsManager', () => {
|
||||
|
||||
it('can add effects on a runtime object', () => {
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
const object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
const object = new gdjs.TestRuntimeObjectWithFakeRenderer(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
variables: [],
|
||||
|
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Tests for gdjs.SceneStack.
|
||||
*/
|
||||
describe('gdjs.SceneStack', function() {
|
||||
describe('gdjs.SceneStack', function () {
|
||||
var runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ windowWidth: number; windowHeight: number;... Remove this comment to see the full error message
|
||||
@@ -38,33 +38,39 @@ describe('gdjs.SceneStack', function() {
|
||||
variables: [],
|
||||
},
|
||||
],
|
||||
resources: { resources: [] }
|
||||
resources: { resources: [] },
|
||||
});
|
||||
var sceneStack = runtimeGame._sceneStack;
|
||||
|
||||
it('should support pushing, replacing and popping scenes', function() {
|
||||
// Set up some global callbacks
|
||||
var firstLoadedScene = null;
|
||||
var lastLoadedScene = null;
|
||||
var lastUnloadedScene = null;
|
||||
var lastPausedScene = null;
|
||||
var lastResumedScene = null;
|
||||
it('should support pushing, replacing and popping scenes', function () {
|
||||
// Set up some scene callbacks.
|
||||
let firstLoadedScene = null;
|
||||
let lastLoadedScene = null;
|
||||
let lastUnloadedScene = null;
|
||||
let lastPausedScene = null;
|
||||
let lastResumedScene = null;
|
||||
|
||||
gdjs.registerFirstRuntimeSceneLoadedCallback(function(runtimeScene) {
|
||||
const onFirstRuntimeSceneLoaded = (runtimeScene) => {
|
||||
firstLoadedScene = runtimeScene;
|
||||
});
|
||||
gdjs.registerRuntimeSceneLoadedCallback(function(runtimeScene) {
|
||||
};
|
||||
const onRuntimeSceneLoaded = (runtimeScene) => {
|
||||
lastLoadedScene = runtimeScene;
|
||||
});
|
||||
gdjs.registerRuntimeSceneUnloadedCallback(function(runtimeScene) {
|
||||
};
|
||||
const onRuntimeSceneUnloaded = (runtimeScene) => {
|
||||
lastUnloadedScene = runtimeScene;
|
||||
});
|
||||
gdjs.registerRuntimeScenePausedCallback(function(runtimeScene) {
|
||||
};
|
||||
const onRuntimeScenePaused = (runtimeScene) => {
|
||||
lastPausedScene = runtimeScene;
|
||||
});
|
||||
gdjs.registerRuntimeSceneResumedCallback(function(runtimeScene) {
|
||||
};
|
||||
const onRuntimeSceneResumed = (runtimeScene) => {
|
||||
lastResumedScene = runtimeScene;
|
||||
});
|
||||
};
|
||||
|
||||
gdjs.registerFirstRuntimeSceneLoadedCallback(onFirstRuntimeSceneLoaded);
|
||||
gdjs.registerRuntimeSceneLoadedCallback(onRuntimeSceneLoaded);
|
||||
gdjs.registerRuntimeSceneUnloadedCallback(onRuntimeSceneUnloaded);
|
||||
gdjs.registerRuntimeScenePausedCallback(onRuntimeScenePaused);
|
||||
gdjs.registerRuntimeSceneResumedCallback(onRuntimeSceneResumed);
|
||||
|
||||
// Test the stack
|
||||
expect(sceneStack.pop()).to.be(null);
|
||||
@@ -122,6 +128,10 @@ describe('gdjs.SceneStack', function() {
|
||||
expect(sceneStack.wasFirstSceneLoaded()).to.be(true);
|
||||
|
||||
// Remove all the global callbacks
|
||||
gdjs.clearGlobalCallbacks();
|
||||
gdjs._unregisterCallback(onFirstRuntimeSceneLoaded);
|
||||
gdjs._unregisterCallback(onRuntimeSceneLoaded);
|
||||
gdjs._unregisterCallback(onRuntimeSceneUnloaded);
|
||||
gdjs._unregisterCallback(onRuntimeScenePaused);
|
||||
gdjs._unregisterCallback(onRuntimeSceneResumed);
|
||||
});
|
||||
});
|
||||
|
@@ -364,6 +364,7 @@ interface Project {
|
||||
|
||||
void SetName([Const] DOMString name);
|
||||
[Const, Ref] DOMString GetName();
|
||||
[Ref] VectorString GetCategories();
|
||||
void SetDescription([Const] DOMString description);
|
||||
[Const, Ref] DOMString GetDescription();
|
||||
void SetVersion([Const] DOMString authorName);
|
||||
@@ -1924,7 +1925,7 @@ interface VectorEventsSearchResult {
|
||||
interface EventsRefactorer {
|
||||
void STATIC_RenameObjectInEvents([Const, Ref] Platform platform, [Ref] ObjectsContainer project, [Ref] ObjectsContainer layout, [Ref] EventsList events, [Const] DOMString oldName, [Const] DOMString newName);
|
||||
void STATIC_RemoveObjectInEvents([Const, Ref] Platform platform, [Ref] ObjectsContainer project, [Ref] ObjectsContainer layout, [Ref] EventsList events, [Const] DOMString name);
|
||||
void STATIC_ReplaceStringInEvents([Ref] ObjectsContainer project, [Ref] ObjectsContainer layout, [Ref] EventsList events, [Const] DOMString toReplace, [Const] DOMString newString, boolean matchCase, boolean inConditions, boolean inActions);
|
||||
void STATIC_ReplaceStringInEvents([Ref] ObjectsContainer project, [Ref] ObjectsContainer layout, [Ref] EventsList events, [Const] DOMString toReplace, [Const] DOMString newString, boolean matchCase, boolean inConditions, boolean inActions, boolean inEventStrings);
|
||||
[Value] VectorEventsSearchResult STATIC_SearchInEvents([Const, Ref] Platform platform, [Ref] EventsList events, [Const] DOMString search, boolean matchCase, boolean inConditions, boolean inActions, boolean inEventStrings, boolean inEventSentences);
|
||||
};
|
||||
|
||||
|
@@ -236,11 +236,14 @@ class RuntimeScene {
|
||||
|
||||
/**
|
||||
* Create a minimal mock of GDJS with a RuntimeScene (`gdjs.RuntimeScene`),
|
||||
* supporting setting a variable and using "Trigger Once" conditions
|
||||
* (just enough to validate events logic).
|
||||
* supporting setting a variable, using "Trigger Once" conditions
|
||||
* (just enough to validate events logic), registering a behavior and some
|
||||
* lifecycle callbacks.
|
||||
*/
|
||||
function makeMinimalGDJSMock() {
|
||||
const behaviorCtors = {};
|
||||
let runtimeScenePreEventsCallbacks = [];
|
||||
const runtimeScene = new RuntimeScene();
|
||||
|
||||
return {
|
||||
gdjs: {
|
||||
@@ -251,13 +254,26 @@ function makeMinimalGDJSMock() {
|
||||
registerBehavior: (behaviorTypeName, Ctor) => {
|
||||
behaviorCtors[behaviorTypeName] = Ctor;
|
||||
},
|
||||
registerRuntimeScenePreEventsCallback: (cb) => {
|
||||
runtimeScenePreEventsCallbacks.push(cb);
|
||||
},
|
||||
_unregisterCallback: (unregisteredCb) => {
|
||||
runtimeScenePreEventsCallbacks = runtimeScenePreEventsCallbacks.filter(
|
||||
(cb) => cb !== unregisteredCb
|
||||
);
|
||||
},
|
||||
copyArray,
|
||||
objectsListsToArray,
|
||||
RuntimeBehavior,
|
||||
OnceTriggers,
|
||||
Hashtable,
|
||||
},
|
||||
runtimeScene: new RuntimeScene(),
|
||||
mocks: {
|
||||
runRuntimeScenePreEventsCallbacks: () => {
|
||||
runtimeScenePreEventsCallbacks.forEach(cb => cb(runtimeScene))
|
||||
}
|
||||
},
|
||||
runtimeScene,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -602,6 +602,79 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
|
||||
eventsFunctionCopy.delete();
|
||||
project.delete();
|
||||
});
|
||||
|
||||
it('generates a lifecycle function that registers itself, and unregister itself if hot-reloaded', function () {
|
||||
// Event to create an object, then add
|
||||
const eventsSerializerElement = gd.Serializer.fromJSObject([
|
||||
{
|
||||
disabled: false,
|
||||
folded: false,
|
||||
type: 'BuiltinCommonInstructions::Standard',
|
||||
conditions: [],
|
||||
actions: [
|
||||
{
|
||||
type: { inverted: false, value: 'ModVarScene' },
|
||||
parameters: ['SuccessVariable', '+', '1'],
|
||||
subInstructions: [],
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const project = new gd.ProjectHelper.createNewGDJSProject();
|
||||
const eventsFunction = new gd.EventsFunction();
|
||||
eventsFunction.setName('onScenePreEvents');
|
||||
eventsFunction
|
||||
.getEvents()
|
||||
.unserializeFrom(project, eventsSerializerElement);
|
||||
|
||||
const runCompiledEvents = generateCompiledEventsForEventsFunction(
|
||||
gd,
|
||||
project,
|
||||
eventsFunction,
|
||||
{
|
||||
dontCallGeneratedFunction: true,
|
||||
}
|
||||
);
|
||||
|
||||
const { gdjs, runtimeScene, mocks } = makeMinimalGDJSMock();
|
||||
runCompiledEvents(
|
||||
gdjs,
|
||||
runtimeScene /*, Don't pass arguments to not run the function. */
|
||||
);
|
||||
mocks.runRuntimeScenePreEventsCallbacks();
|
||||
expect(
|
||||
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
|
||||
).toBe(1);
|
||||
mocks.runRuntimeScenePreEventsCallbacks();
|
||||
expect(
|
||||
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
|
||||
).toBe(2);
|
||||
|
||||
// Simulate a hot reloading by recompiling the function and running it again.
|
||||
const runHotReloadedCompiledEvents =
|
||||
generateCompiledEventsForEventsFunction(gd, project, eventsFunction, {
|
||||
dontCallGeneratedFunction: true,
|
||||
});
|
||||
runHotReloadedCompiledEvents(
|
||||
gdjs,
|
||||
runtimeScene /*, Don't pass arguments to not run the function. */
|
||||
);
|
||||
|
||||
// Ensure that when we call the callbacks, it's called only once (not registered twice).
|
||||
mocks.runRuntimeScenePreEventsCallbacks();
|
||||
expect(
|
||||
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
|
||||
).toBe(3);
|
||||
mocks.runRuntimeScenePreEventsCallbacks();
|
||||
expect(
|
||||
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
|
||||
).toBe(4);
|
||||
|
||||
eventsFunction.delete();
|
||||
project.delete();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -641,9 +714,11 @@ function generateCompiledEventsForEventsFunction(gd, project, eventsFunction) {
|
||||
`Hashtable = gdjs.Hashtable;` +
|
||||
'\n' +
|
||||
code +
|
||||
// Return the function for it to be called.
|
||||
// Return the function for it to be called (if arguments are passed).
|
||||
`;
|
||||
return functionNamespace.func.apply(functionNamespace.func, [runtimeScene, ...functionArguments, runtimeScene]);`
|
||||
return functionArguments ?
|
||||
functionNamespace.func.apply(functionNamespace.func, [runtimeScene, ...functionArguments, runtimeScene]) :
|
||||
null;`
|
||||
);
|
||||
|
||||
return runCompiledEventsFunction;
|
||||
|
@@ -2,7 +2,7 @@
|
||||
declare class gdEventsRefactorer {
|
||||
static renameObjectInEvents(platform: gdPlatform, project: gdObjectsContainer, layout: gdObjectsContainer, events: gdEventsList, oldName: string, newName: string): void;
|
||||
static removeObjectInEvents(platform: gdPlatform, project: gdObjectsContainer, layout: gdObjectsContainer, events: gdEventsList, name: string): void;
|
||||
static replaceStringInEvents(project: gdObjectsContainer, layout: gdObjectsContainer, events: gdEventsList, toReplace: string, newString: string, matchCase: boolean, inConditions: boolean, inActions: boolean): void;
|
||||
static replaceStringInEvents(project: gdObjectsContainer, layout: gdObjectsContainer, events: gdEventsList, toReplace: string, newString: string, matchCase: boolean, inConditions: boolean, inActions: boolean, inEventStrings: boolean): void;
|
||||
static searchInEvents(platform: gdPlatform, events: gdEventsList, search: string, matchCase: boolean, inConditions: boolean, inActions: boolean, inEventStrings: boolean, inEventSentences: boolean): gdVectorEventsSearchResult;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
|
@@ -3,6 +3,7 @@ declare class gdProject extends gdObjectsContainer {
|
||||
constructor(): void;
|
||||
setName(name: string): void;
|
||||
getName(): string;
|
||||
getCategories(): gdVectorString;
|
||||
setDescription(description: string): void;
|
||||
getDescription(): string;
|
||||
setVersion(authorName: string): void;
|
||||
|
@@ -23,8 +23,8 @@ npm start # or yarn start
|
||||
|
||||
This will open the app in your web browser.
|
||||
|
||||
Images resources, GDJS Runtime, extensions will be copied in resources, and [libGD.js](https://github.com/4ian/GDevelop.js) will be downloaded automatically. If you wish, you can
|
||||
[build libGD.js by yourself](https://github.com/4ian/GDevelop.js) (useful if you modified GDevelop native code like extensions).
|
||||
Images resources, GDJS Runtime, extensions will be copied in resources, and [libGD.js](https://github.com/4ian/GDevelop/tree/master/GDevelop.js) will be downloaded automatically. If you wish, you can
|
||||
[build libGD.js by yourself](https://github.com/4ian/GDevelop/tree/master/GDevelop.js) (useful if you modified GDevelop native code like extensions).
|
||||
|
||||
> Note for Linux: If you get an error message that looks like this:
|
||||
> `Error: watch GD/newIDE/app/some/file ENOSPC` then follow the instructions [here](https://stackoverflow.com/questions/22475849/node-js-error-enospc) to fix.
|
||||
|
9
newIDE/app/package-lock.json
generated
9
newIDE/app/package-lock.json
generated
@@ -36774,7 +36774,8 @@
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "3.1.3",
|
||||
@@ -54910,6 +54911,7 @@
|
||||
},
|
||||
"@lingui/react": {
|
||||
"version": "git+ssh://git@github.com/4ian/lingui-react.git#dc6b1e013470d952cf85f96cc4affdd28e29634a",
|
||||
"integrity": "sha512-eoYJ8TI+8IolPh4fue9aIwX2OVp0YrPnV86QBZLfGhxknodVeNmx+4Ic4ym7rI5/davbk9AUZHcssiH+YZWVxw==",
|
||||
"from": "@lingui/react@git://github.com/4ian/lingui-react.git#master",
|
||||
"requires": {
|
||||
"@lingui/core": "2.7.3",
|
||||
@@ -79076,7 +79078,8 @@
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.1.3",
|
||||
@@ -80354,6 +80357,7 @@
|
||||
},
|
||||
"pixi-simple-gesture": {
|
||||
"version": "git+ssh://git@github.com/4ian/pixi-simple-gesture.git#c84e0cc3c62edeca019e708d9897ef6b97a0d18a",
|
||||
"integrity": "sha512-DG1BxP8SK2iPMYWMOPGz5gKDXFmA8JPUpcyyNyIH55fpQraenuLYlosYFFMTRXEy0RZViTUu11H3VrYlfG2CgA==",
|
||||
"from": "pixi-simple-gesture@git://github.com/4ian/pixi-simple-gesture#v0.3.3"
|
||||
},
|
||||
"pixi.js": {
|
||||
@@ -83072,6 +83076,7 @@
|
||||
},
|
||||
"react-mosaic-component": {
|
||||
"version": "git+ssh://git@github.com/4ian/react-mosaic.git#d5ef155119d786c08c7c72e34997dcef2f01f98b",
|
||||
"integrity": "sha512-Izfw/EkG1g39nrZbOqzY52rqFkVFA1SUSv1TLwk7soS1Wy7iHm6zrUgzJdfwKRC2GaDn9WAfSe5ZQ2vIJ/mu5A==",
|
||||
"from": "react-mosaic-component@git://github.com/4ian/react-mosaic#v3.1.0",
|
||||
"requires": {
|
||||
"classnames": "^2.2.6",
|
||||
|
BIN
newIDE/app/public/GDevelop-editor-thumbnail.png
Normal file
BIN
newIDE/app/public/GDevelop-editor-thumbnail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 779 KiB |
@@ -8,6 +8,19 @@
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon-256.png">
|
||||
<title>GDevelop 5</title>
|
||||
|
||||
<meta name="title" content="GDevelop game making app" />
|
||||
<meta property="og:title" content="GDevelop game making app" />
|
||||
<meta name="description"
|
||||
content="Build your own game super fast and without programming. Publish on mobile, desktop and on the web." />
|
||||
<meta property="og:description"
|
||||
content="Build your own game super fast and without programming. Publish on mobile, desktop and on the web." />
|
||||
<meta property="og:url" content="https://gdevelop.io" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="%PUBLIC_URL%/GDevelop-editor-thumbnail.png" />
|
||||
<meta property="og:image:width" content="1920" />
|
||||
<meta property="og:image:height" content="1080" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
@@ -26,11 +39,14 @@
|
||||
|
||||
@keyframes text-focus-in {
|
||||
0% {
|
||||
-webkit-filter: blur(12px); filter: blur(12px);
|
||||
-webkit-filter: blur(12px);
|
||||
filter: blur(12px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-filter: blur(0px); filter: blur(0px);
|
||||
-webkit-filter: blur(0px);
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +74,7 @@
|
||||
<p>
|
||||
To use GDevelop 5, you must <b>activate JavaScript</b> in your browser or
|
||||
download the desktop version for Windows, macOS or Linux on
|
||||
<a href="https://gdevelop-app.com">https://gdevelop-app.com</a>.
|
||||
<a href="https://gdevelop.io">https://gdevelop.io</a>.
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
|
BIN
newIDE/app/public/res/hero.png
Normal file
BIN
newIDE/app/public/res/hero.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
@@ -244,9 +244,10 @@ export const addAssetToProject = async ({
|
||||
groupEvent.getSubEvents(),
|
||||
parameter.name,
|
||||
parameter.defaultValue,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
/*matchCase=*/ true,
|
||||
/*inConditions=*/ true,
|
||||
/*inActions=*/ true,
|
||||
/*inEventStrings=*/ false
|
||||
);
|
||||
});
|
||||
|
||||
|
@@ -1,29 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import Text from '../UI/Text';
|
||||
import { EmptyPlaceholder } from '../UI/EmptyPlaceholder';
|
||||
|
||||
const EmptyBehaviorsPlaceholder = () => (
|
||||
<EmptyPlaceholder
|
||||
renderButtons={() => <HelpButton helpPagePath="/behaviors" />}
|
||||
>
|
||||
<Text>
|
||||
<Trans>There are no behaviors here.</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>
|
||||
Behaviors are predefined actions that are assigned to objects. Behaviors
|
||||
can have no or multiple parameters.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>
|
||||
Add your first behavior using the button "Add a behavior to the object".
|
||||
</Trans>
|
||||
</Text>
|
||||
</EmptyPlaceholder>
|
||||
);
|
||||
|
||||
export default EmptyBehaviorsPlaceholder;
|
@@ -23,7 +23,7 @@ import DismissableTutorialMessage from '../Hints/DismissableTutorialMessage';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import { Accordion, AccordionHeader, AccordionBody } from '../UI/Accordion';
|
||||
import EmptyBehaviorsPlaceholder from './EmptyBehaviorsPlaceholder';
|
||||
import { EmptyPlaceholder } from '../UI/EmptyPlaceholder';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import { IconContainer } from '../UI/IconContainer';
|
||||
@@ -119,25 +119,92 @@ const BehaviorsEditor = (props: Props) => {
|
||||
<Column noMargin expand useFullHeight noOverflowParent>
|
||||
{allBehaviorNames.length === 0 ? (
|
||||
<Column noMargin expand justifyContent="center">
|
||||
<EmptyBehaviorsPlaceholder />
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first behavior</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
Behaviors add features to objects in a matter of clicks.
|
||||
</Trans>
|
||||
}
|
||||
actionLabel={<Trans>Add a behavior</Trans>}
|
||||
helpPagePath="/behaviors"
|
||||
actionButtonId="add-behavior-button"
|
||||
onAdd={() => setNewBehaviorDialogOpen(true)}
|
||||
/>
|
||||
</Column>
|
||||
) : (
|
||||
<ScrollView>
|
||||
{allBehaviorNames.map((behaviorName, index) => {
|
||||
const behaviorContent = object.getBehavior(behaviorName);
|
||||
const behaviorTypeName = behaviorContent.getTypeName();
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
{allBehaviorNames.map((behaviorName, index) => {
|
||||
const behaviorContent = object.getBehavior(behaviorName);
|
||||
const behaviorTypeName = behaviorContent.getTypeName();
|
||||
|
||||
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
|
||||
gd.JsPlatform.get(),
|
||||
behaviorTypeName
|
||||
);
|
||||
if (gd.MetadataProvider.isBadBehaviorMetadata(behaviorMetadata)) {
|
||||
return (
|
||||
<Accordion key={behaviorName} defaultExpanded>
|
||||
<AccordionHeader
|
||||
actions={[
|
||||
<IconButton
|
||||
key="delete"
|
||||
onClick={ev => {
|
||||
ev.stopPropagation();
|
||||
onRemoveBehavior(behaviorName);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>,
|
||||
]}
|
||||
>
|
||||
<MiniToolbarText firstChild>
|
||||
<Trans>Unknown behavior</Trans>{' '}
|
||||
</MiniToolbarText>
|
||||
<Column noMargin expand>
|
||||
<TextField
|
||||
margin="none"
|
||||
value={behaviorName}
|
||||
disabled
|
||||
/>
|
||||
</Column>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This behavior is unknown. It might be a behavior that
|
||||
was defined in an extension and that was later
|
||||
removed. You should delete it.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
const behavior = behaviorMetadata.get();
|
||||
const BehaviorComponent = BehaviorsEditorService.getEditor(
|
||||
behaviorTypeName
|
||||
);
|
||||
const tutorialIds = getBehaviorTutorialIds(behaviorTypeName);
|
||||
const enabledTutorialIds = tutorialIds.filter(
|
||||
tutorialId => !values.hiddenTutorialHints[tutorialId]
|
||||
);
|
||||
const iconUrl = behaviorMetadata.getIconFilename();
|
||||
|
||||
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
|
||||
gd.JsPlatform.get(),
|
||||
behaviorTypeName
|
||||
);
|
||||
if (gd.MetadataProvider.isBadBehaviorMetadata(behaviorMetadata)) {
|
||||
return (
|
||||
<Accordion key={behaviorName} defaultExpanded>
|
||||
<AccordionHeader
|
||||
actions={[
|
||||
<HelpIcon
|
||||
key="help"
|
||||
size="small"
|
||||
helpPagePath={behaviorMetadata.getHelpPath()}
|
||||
/>,
|
||||
<IconButton
|
||||
key="delete"
|
||||
size="small"
|
||||
onClick={ev => {
|
||||
ev.stopPropagation();
|
||||
onRemoveBehavior(behaviorName);
|
||||
@@ -147,127 +214,79 @@ const BehaviorsEditor = (props: Props) => {
|
||||
</IconButton>,
|
||||
]}
|
||||
>
|
||||
<MiniToolbarText firstChild>
|
||||
<Trans>Unknown behavior</Trans>{' '}
|
||||
</MiniToolbarText>
|
||||
<Column noMargin expand>
|
||||
<TextField margin="none" value={behaviorName} disabled />
|
||||
{iconUrl ? (
|
||||
<IconContainer
|
||||
src={iconUrl}
|
||||
alt={behaviorMetadata.getFullName()}
|
||||
size={20}
|
||||
/>
|
||||
) : null}
|
||||
<Column expand>
|
||||
<TextField
|
||||
value={behaviorName}
|
||||
hintText={t`Behavior name`}
|
||||
margin="none"
|
||||
fullWidth
|
||||
disabled
|
||||
onChange={(e, text) =>
|
||||
onChangeBehaviorName(behaviorContent, text)
|
||||
}
|
||||
id={`behavior-${behaviorName}-name-text-field`}
|
||||
/>
|
||||
</Column>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This behavior is unknown. It might be a behavior that
|
||||
was defined in an extension and that was later removed.
|
||||
You should delete it.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
<Column
|
||||
expand
|
||||
noMargin
|
||||
// Avoid Physics2 behavior overflow on small screens
|
||||
noOverflowParent
|
||||
>
|
||||
{enabledTutorialIds.length ? (
|
||||
<Line>
|
||||
<ColumnStackLayout expand>
|
||||
{tutorialIds.map(tutorialId => (
|
||||
<DismissableTutorialMessage
|
||||
key={tutorialId}
|
||||
tutorialId={tutorialId}
|
||||
/>
|
||||
))}
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
) : null}
|
||||
<Line>
|
||||
<BehaviorComponent
|
||||
behavior={behavior}
|
||||
behaviorContent={behaviorContent}
|
||||
project={project}
|
||||
object={object}
|
||||
resourceSources={props.resourceSources}
|
||||
onChooseResource={props.onChooseResource}
|
||||
resourceExternalEditors={
|
||||
props.resourceExternalEditors
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
const behavior = behaviorMetadata.get();
|
||||
const BehaviorComponent = BehaviorsEditorService.getEditor(
|
||||
behaviorTypeName
|
||||
);
|
||||
const tutorialIds = getBehaviorTutorialIds(behaviorTypeName);
|
||||
const enabledTutorialIds = tutorialIds.filter(
|
||||
tutorialId => !values.hiddenTutorialHints[tutorialId]
|
||||
);
|
||||
const iconUrl = behaviorMetadata.getIconFilename();
|
||||
|
||||
return (
|
||||
<Accordion key={behaviorName} defaultExpanded>
|
||||
<AccordionHeader
|
||||
actions={[
|
||||
<HelpIcon
|
||||
key="help"
|
||||
size="small"
|
||||
helpPagePath={behaviorMetadata.getHelpPath()}
|
||||
/>,
|
||||
<IconButton
|
||||
key="delete"
|
||||
size="small"
|
||||
onClick={ev => {
|
||||
ev.stopPropagation();
|
||||
onRemoveBehavior(behaviorName);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>,
|
||||
]}
|
||||
>
|
||||
{iconUrl ? (
|
||||
<IconContainer
|
||||
src={iconUrl}
|
||||
alt={behaviorMetadata.getFullName()}
|
||||
size={20}
|
||||
/>
|
||||
) : null}
|
||||
<Column expand>
|
||||
<TextField
|
||||
value={behaviorName}
|
||||
hintText={t`Behavior name`}
|
||||
margin="none"
|
||||
fullWidth
|
||||
disabled
|
||||
onChange={(e, text) =>
|
||||
onChangeBehaviorName(behaviorContent, text)
|
||||
}
|
||||
id={`behavior-${behaviorName}-name-text-field`}
|
||||
/>
|
||||
</Column>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Column
|
||||
expand
|
||||
noMargin
|
||||
// Avoid Physics2 behavior overflow on small screens
|
||||
noOverflowParent
|
||||
>
|
||||
{enabledTutorialIds.length ? (
|
||||
<Line>
|
||||
<ColumnStackLayout expand>
|
||||
{tutorialIds.map(tutorialId => (
|
||||
<DismissableTutorialMessage
|
||||
key={tutorialId}
|
||||
tutorialId={tutorialId}
|
||||
/>
|
||||
))}
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
) : null}
|
||||
<Line>
|
||||
<BehaviorComponent
|
||||
behavior={behavior}
|
||||
behaviorContent={behaviorContent}
|
||||
project={project}
|
||||
object={object}
|
||||
resourceSources={props.resourceSources}
|
||||
onChooseResource={props.onChooseResource}
|
||||
resourceExternalEditors={props.resourceExternalEditors}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
})}
|
||||
</ScrollView>
|
||||
<Column>
|
||||
<Line justifyContent="flex-end" expand>
|
||||
<RaisedButton
|
||||
key="add-behavior-line"
|
||||
label={<Trans>Add a behavior</Trans>}
|
||||
primary
|
||||
onClick={() => setNewBehaviorDialogOpen(true)}
|
||||
icon={<Add />}
|
||||
id="add-behavior-button"
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Column>
|
||||
<Line justifyContent="flex-end" expand>
|
||||
<RaisedButton
|
||||
key="add-behavior-line"
|
||||
label={<Trans>Add a behavior to the object</Trans>}
|
||||
primary
|
||||
onClick={() => setNewBehaviorDialogOpen(true)}
|
||||
icon={<Add />}
|
||||
id="add-behavior-button"
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
|
||||
{newBehaviorDialogOpen && (
|
||||
<NewBehaviorDialog
|
||||
|
@@ -52,6 +52,7 @@ export type CommandName =
|
||||
| 'ADD_STANDARD_EVENT'
|
||||
| 'ADD_SUBEVENT'
|
||||
| 'ADD_COMMENT_EVENT'
|
||||
| 'TOGGLE_EVENT_DISABLED'
|
||||
| 'CHOOSE_AND_ADD_EVENT'
|
||||
| 'EVENTS_EDITOR_UNDO'
|
||||
| 'EVENTS_EDITOR_REDO'
|
||||
@@ -279,6 +280,10 @@ const commandsList: { [CommandName]: CommandMetadata } = {
|
||||
displayText: t`Add a sub-event to the selected event`,
|
||||
},
|
||||
ADD_COMMENT_EVENT: { area: 'EVENTS', displayText: t`Add a comment` },
|
||||
TOGGLE_EVENT_DISABLED: {
|
||||
area: 'EVENTS',
|
||||
displayText: t`Toggle disabled event`,
|
||||
},
|
||||
CHOOSE_AND_ADD_EVENT: {
|
||||
area: 'EVENTS',
|
||||
displayText: t`Choose and add an event...`,
|
||||
|
@@ -1,37 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import Text from '../UI/Text';
|
||||
import { EmptyPlaceholder } from '../UI/EmptyPlaceholder';
|
||||
|
||||
type Props = {|
|
||||
target: 'object' | 'layer',
|
||||
|};
|
||||
|
||||
export const EmptyEffectsPlaceholder = (props: Props) => (
|
||||
<EmptyPlaceholder
|
||||
renderButtons={() => (
|
||||
<HelpButton
|
||||
helpPagePath={
|
||||
props.target === 'object'
|
||||
? '/objects/effects'
|
||||
: '/interface/scene-editor/layer-effects'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Text>
|
||||
<Trans>
|
||||
Effects can change how layers or objects are rendered on screen.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>
|
||||
After adding an effect, set up its parameters. Launch a preview to see
|
||||
the result. Using the events and the name of the effect, you can change
|
||||
the parameters during the game.
|
||||
</Trans>
|
||||
</Text>
|
||||
</EmptyPlaceholder>
|
||||
);
|
@@ -33,7 +33,7 @@ import {
|
||||
} from '../ResourcesList/ResourceSource';
|
||||
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import { EmptyEffectsPlaceholder } from './EmptyEffectsPlaceholder';
|
||||
import { EmptyPlaceholder } from '../UI/EmptyPlaceholder';
|
||||
import {
|
||||
addCreateBadgePreHookIfNotClaimed,
|
||||
TRIVIAL_FIRST_EFFECT,
|
||||
@@ -124,153 +124,167 @@ export default function EffectsList(props: Props) {
|
||||
{({ i18n }) => (
|
||||
<Column noMargin expand useFullHeight>
|
||||
{effectsContainer.getEffectsCount() !== 0 ? (
|
||||
<ScrollView>
|
||||
{effectsContainer.getEffectsCount() > 3 && (
|
||||
<Line>
|
||||
<Column>
|
||||
<DismissableAlertMessage
|
||||
identifier="too-much-effects"
|
||||
kind="warning"
|
||||
>
|
||||
<Trans>
|
||||
Using a lot of effects can have a severe negative impact
|
||||
on the rendering performance, especially on low-end or
|
||||
mobile devices. Consider using less effects if possible.
|
||||
You can also disable and re-enable effects as needed
|
||||
using events.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
</Column>
|
||||
</Line>
|
||||
)}
|
||||
{mapFor(0, effectsContainer.getEffectsCount(), (i: number) => {
|
||||
const effect: gdEffect = effectsContainer.getEffectAt(i);
|
||||
const effectType = effect.getEffectType();
|
||||
const effectMetadata = getEnumeratedEffectMetadata(
|
||||
allEffectMetadata,
|
||||
effectType
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<MiniToolbar>
|
||||
<MiniToolbarText firstChild>
|
||||
<Trans>Effect name:</Trans>
|
||||
</MiniToolbarText>
|
||||
<SemiControlledTextField
|
||||
margin="none"
|
||||
commitOnBlur
|
||||
hintText={t`Enter the effect name`}
|
||||
value={effect.getName()}
|
||||
onChange={newName => {
|
||||
if (newName === effect.getName()) return;
|
||||
|
||||
effect.setName(newName);
|
||||
forceUpdate();
|
||||
onEffectsUpdated();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<MiniToolbarText>
|
||||
<Trans>Type:</Trans>
|
||||
</MiniToolbarText>
|
||||
<SelectField
|
||||
margin="none"
|
||||
value={effectType}
|
||||
onChange={(e, i, newEffectType: string) =>
|
||||
chooseEffectType(effect, newEffectType)
|
||||
}
|
||||
fullWidth
|
||||
hintText={t`Choose the effect to apply`}
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
{effectsContainer.getEffectsCount() > 3 && (
|
||||
<Line>
|
||||
<Column>
|
||||
<DismissableAlertMessage
|
||||
identifier="too-much-effects"
|
||||
kind="warning"
|
||||
>
|
||||
{allEffectMetadata.map(effectMetadata => (
|
||||
<SelectOption
|
||||
key={effectMetadata.type}
|
||||
value={effectMetadata.type}
|
||||
primaryText={effectMetadata.fullName}
|
||||
disabled={
|
||||
props.target === 'object' &&
|
||||
effectMetadata.isMarkedAsNotWorkingForObjects
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SelectField>
|
||||
<ElementWithMenu
|
||||
element={
|
||||
<IconButton>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
}
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
click: () => removeEffect(effect.getName()),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: i18n._(t`Show Parameter Names`),
|
||||
checked: showEffectParameterNames,
|
||||
click: () =>
|
||||
setShowEffectParameterNames(
|
||||
!showEffectParameterNames
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MiniToolbar>
|
||||
<Line expand noMargin>
|
||||
<Column expand>
|
||||
{!!effectType && effectMetadata ? (
|
||||
<React.Fragment>
|
||||
<Line>
|
||||
<BackgroundText>
|
||||
<MarkdownText
|
||||
source={effectMetadata.description}
|
||||
/>
|
||||
</BackgroundText>
|
||||
</Line>
|
||||
<PropertiesEditor
|
||||
instances={[effect]}
|
||||
schema={effectMetadata.parametersSchema}
|
||||
project={props.project}
|
||||
resourceSources={props.resourceSources}
|
||||
onChooseResource={props.onChooseResource}
|
||||
resourceExternalEditors={
|
||||
props.resourceExternalEditors
|
||||
}
|
||||
renderExtraDescriptionText={
|
||||
showEffectParameterNames
|
||||
? parameterName =>
|
||||
i18n._(
|
||||
t`Parameter name in events: \`${parameterName}\` `
|
||||
)
|
||||
: undefined
|
||||
<Trans>
|
||||
Using a lot of effects can have a severe negative
|
||||
impact on the rendering performance, especially on
|
||||
low-end or mobile devices. Consider using less effects
|
||||
if possible. You can also disable and re-enable
|
||||
effects as needed using events.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
</Column>
|
||||
</Line>
|
||||
)}
|
||||
{mapFor(0, effectsContainer.getEffectsCount(), (i: number) => {
|
||||
const effect: gdEffect = effectsContainer.getEffectAt(i);
|
||||
const effectType = effect.getEffectType();
|
||||
const effectMetadata = getEnumeratedEffectMetadata(
|
||||
allEffectMetadata,
|
||||
effectType
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<MiniToolbar>
|
||||
<MiniToolbarText firstChild>
|
||||
<Trans>Effect name:</Trans>
|
||||
</MiniToolbarText>
|
||||
<SemiControlledTextField
|
||||
margin="none"
|
||||
commitOnBlur
|
||||
hintText={t`Enter the effect name`}
|
||||
value={effect.getName()}
|
||||
onChange={newName => {
|
||||
if (newName === effect.getName()) return;
|
||||
|
||||
effect.setName(newName);
|
||||
forceUpdate();
|
||||
onEffectsUpdated();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<MiniToolbarText>
|
||||
<Trans>Type:</Trans>
|
||||
</MiniToolbarText>
|
||||
<SelectField
|
||||
margin="none"
|
||||
value={effectType}
|
||||
onChange={(e, i, newEffectType: string) =>
|
||||
chooseEffectType(effect, newEffectType)
|
||||
}
|
||||
fullWidth
|
||||
hintText={t`Choose the effect to apply`}
|
||||
>
|
||||
{allEffectMetadata.map(effectMetadata => (
|
||||
<SelectOption
|
||||
key={effectMetadata.type}
|
||||
value={effectMetadata.type}
|
||||
primaryText={effectMetadata.fullName}
|
||||
disabled={
|
||||
props.target === 'object' &&
|
||||
effectMetadata.isMarkedAsNotWorkingForObjects
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</Column>
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
))}
|
||||
</SelectField>
|
||||
<ElementWithMenu
|
||||
element={
|
||||
<IconButton>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
}
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
click: () => removeEffect(effect.getName()),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: i18n._(t`Show Parameter Names`),
|
||||
checked: showEffectParameterNames,
|
||||
click: () =>
|
||||
setShowEffectParameterNames(
|
||||
!showEffectParameterNames
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MiniToolbar>
|
||||
<Line expand noMargin>
|
||||
<Column expand>
|
||||
{!!effectType && effectMetadata ? (
|
||||
<React.Fragment>
|
||||
<Line>
|
||||
<BackgroundText>
|
||||
<MarkdownText
|
||||
source={effectMetadata.description}
|
||||
/>
|
||||
</BackgroundText>
|
||||
</Line>
|
||||
<PropertiesEditor
|
||||
instances={[effect]}
|
||||
schema={effectMetadata.parametersSchema}
|
||||
project={props.project}
|
||||
resourceSources={props.resourceSources}
|
||||
onChooseResource={props.onChooseResource}
|
||||
resourceExternalEditors={
|
||||
props.resourceExternalEditors
|
||||
}
|
||||
renderExtraDescriptionText={
|
||||
showEffectParameterNames
|
||||
? parameterName =>
|
||||
i18n._(
|
||||
t`Parameter name in events: \`${parameterName}\` `
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</Column>
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
<Column>
|
||||
<Line justifyContent="flex-end" expand>
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Add an effect</Trans>}
|
||||
onClick={addEffect}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Column noMargin expand justifyContent="center">
|
||||
<EmptyEffectsPlaceholder target={props.target} />
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first effect</Trans>}
|
||||
description={
|
||||
<Trans>Effects create visual changes to the object.</Trans>
|
||||
}
|
||||
actionLabel={<Trans>Add an effect</Trans>}
|
||||
helpPagePath={
|
||||
props.target === 'object'
|
||||
? '/objects/effects'
|
||||
: '/interface/scene-editor/layer-effects'
|
||||
}
|
||||
onAdd={addEffect}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
<Column>
|
||||
<Line justifyContent="flex-end" expand>
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Add an effect</Trans>}
|
||||
onClick={addEffect}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</Column>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -208,6 +208,10 @@ export const ExtensionOptionsEditor = ({
|
||||
text: 'Ads',
|
||||
value: 'Ads',
|
||||
},
|
||||
{
|
||||
text: 'Audio',
|
||||
value: 'Audio',
|
||||
},
|
||||
{
|
||||
text: 'Advanced',
|
||||
value: 'Advanced',
|
||||
|
@@ -139,9 +139,8 @@ export default class EventsSearcher extends React.Component<Props, State> {
|
||||
replaceText,
|
||||
matchCase,
|
||||
searchInConditions,
|
||||
searchInActions
|
||||
// TODO: add capability to replace in event strings
|
||||
// searchInEventStrings
|
||||
searchInActions,
|
||||
searchInEventStrings
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -1,26 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import Text from '../../UI/Text';
|
||||
import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder';
|
||||
|
||||
const EmptyEventsPlaceholder = () => (
|
||||
<EmptyPlaceholder renderButtons={() => <HelpButton helpPagePath="/events" />}>
|
||||
<Text>
|
||||
<Trans>There are no events here.</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>
|
||||
Events are composed of conditions (on the left of an event) and actions
|
||||
(on the right). When conditions are fulfilled, the actions are executed.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>Add your first event using the button "Add a new event".</Trans>
|
||||
</Text>
|
||||
</EmptyPlaceholder>
|
||||
);
|
||||
|
||||
export default EmptyEventsPlaceholder;
|
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component, type Node } from 'react';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import {
|
||||
@@ -29,7 +30,7 @@ import 'react-sortable-tree/style.css';
|
||||
import './style.css';
|
||||
import ThemeConsumer from '../../UI/Theme/ThemeConsumer';
|
||||
import BottomButtons from './BottomButtons';
|
||||
import EmptyEventsPlaceholder from './EmptyEventsPlaceholder';
|
||||
import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder';
|
||||
import { CorsAwareImage } from '../../UI/CorsAwareImage';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -383,7 +384,20 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
|
||||
: null,
|
||||
depth === 0 && eventsList.getEventsCount() === 0
|
||||
? {
|
||||
title: () => <EmptyEventsPlaceholder />,
|
||||
title: () => (
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first event</Trans>}
|
||||
description={<Trans>Events define the rules of a game.</Trans>}
|
||||
actionLabel={<Trans>Add an event</Trans>}
|
||||
helpPagePath="/events"
|
||||
onAdd={() =>
|
||||
this.props.onAddNewEvent(
|
||||
'BuiltinCommonInstructions::Standard',
|
||||
this.props.events
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
event: null,
|
||||
indexInList: eventsList.getEventsCount() + 1,
|
||||
disabled: false,
|
||||
|
@@ -179,7 +179,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
{
|
||||
...sharedFuseConfiguration,
|
||||
keys: [
|
||||
{ name: 'displayedName', weight: 2 },
|
||||
{ name: 'displayedName', weight: 5 },
|
||||
{ name: 'fullGroupName', weight: 1 },
|
||||
],
|
||||
}
|
||||
|
@@ -62,16 +62,12 @@ export default class GlobalVariableField extends React.Component<
|
||||
this.setState({ editorOpen: false });
|
||||
if (this._field) this._field.updateAutocompletions();
|
||||
}}
|
||||
emptyExplanationMessage={
|
||||
<Trans>
|
||||
Global variables are variables that are shared amongst all the
|
||||
scenes of the game.
|
||||
</Trans>
|
||||
emptyPlaceholderTitle={
|
||||
<Trans>Add your first global variable</Trans>
|
||||
}
|
||||
emptyExplanationSecondMessage={
|
||||
emptyPlaceholderDescription={
|
||||
<Trans>
|
||||
For example, you can have a variable called UnlockedLevelsCount
|
||||
representing the number of levels unlocked by the player.
|
||||
These variables hold additional information on a project.
|
||||
</Trans>
|
||||
}
|
||||
helpPagePath={'/all-features/variables/global-variables'}
|
||||
|
@@ -89,17 +89,12 @@ export default class ObjectVariableField extends React.Component<
|
||||
title={<Trans>Object Variables</Trans>}
|
||||
open={this.state.editorOpen}
|
||||
variablesContainer={variablesContainer}
|
||||
emptyExplanationMessage={
|
||||
<Trans>
|
||||
When you add variables to an object, any instance of the object
|
||||
put on the scene or created during the game will have these
|
||||
variables attached to it.
|
||||
</Trans>
|
||||
emptyPlaceholderTitle={
|
||||
<Trans>Add your first object variable</Trans>
|
||||
}
|
||||
emptyExplanationSecondMessage={
|
||||
emptyPlaceholderDescription={
|
||||
<Trans>
|
||||
For example, you can have a variable called Life representing
|
||||
the health of the object.
|
||||
These variables hold additional information on an object.
|
||||
</Trans>
|
||||
}
|
||||
helpPagePath={'/all-features/variables/object-variables'}
|
||||
|
@@ -64,16 +64,10 @@ export default class SceneVariableField extends React.Component<
|
||||
this.setState({ editorOpen: false });
|
||||
if (this._field) this._field.updateAutocompletions();
|
||||
}}
|
||||
emptyExplanationMessage={
|
||||
emptyPlaceholderTitle={<Trans>Add your first scene variable</Trans>}
|
||||
emptyPlaceholderDescription={
|
||||
<Trans>
|
||||
Scene variables can be used to store any value or text during
|
||||
the game.
|
||||
</Trans>
|
||||
}
|
||||
emptyExplanationSecondMessage={
|
||||
<Trans>
|
||||
For example, you can have a variable called Score representing
|
||||
the current score of the player.
|
||||
These variables hold additional information on a scene.
|
||||
</Trans>
|
||||
}
|
||||
helpPagePath={'/all-features/variables/scene-variables'}
|
||||
|
@@ -15,6 +15,8 @@ type Props = {|
|
||||
onAddCommentEvent: () => void,
|
||||
allEventsMetadata: Array<EventMetadata>,
|
||||
onAddEvent: (eventType: string) => Array<gdBaseEvent>,
|
||||
onToggleDisabledEvent: () => void,
|
||||
canToggleEventDisabled: boolean,
|
||||
onRemove: () => void,
|
||||
canRemove: boolean,
|
||||
undo: () => void,
|
||||
|
@@ -13,6 +13,8 @@ type Props = {|
|
||||
onAddCommentEvent: () => void,
|
||||
allEventsMetadata: Array<EventMetadata>,
|
||||
onAddEvent: (eventType: string) => Array<gdBaseEvent>,
|
||||
onToggleDisabledEvent: () => void,
|
||||
canToggleEventDisabled: boolean,
|
||||
onRemove: () => void,
|
||||
canRemove: boolean,
|
||||
undo: () => void,
|
||||
@@ -38,6 +40,10 @@ const ToolbarCommands = (props: Props) => {
|
||||
handler: props.onAddCommentEvent,
|
||||
});
|
||||
|
||||
useCommand('TOGGLE_EVENT_DISABLED', props.canToggleEventDisabled, {
|
||||
handler: props.onToggleDisabledEvent,
|
||||
});
|
||||
|
||||
useCommandWithOptions('CHOOSE_AND_ADD_EVENT', true, {
|
||||
generateOptions: React.useCallback(
|
||||
() =>
|
||||
|
@@ -13,8 +13,9 @@ import EventTextDialog, {
|
||||
} from './InstructionEditor/EventTextDialog';
|
||||
import Toolbar from './Toolbar';
|
||||
import KeyboardShortcuts from '../UI/KeyboardShortcuts';
|
||||
import { getShortcutDisplayName } from '../KeyboardShortcuts';
|
||||
import InlineParameterEditor from './InlineParameterEditor';
|
||||
import ContextMenu from '../UI/Menu/ContextMenu';
|
||||
import ContextMenu, { type ContextMenuInterface } from '../UI/Menu/ContextMenu';
|
||||
import { serializeToJSObject } from '../Utils/Serializer';
|
||||
import {
|
||||
type HistoryState,
|
||||
@@ -209,8 +210,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
},
|
||||
});
|
||||
|
||||
eventContextMenu: ContextMenu;
|
||||
instructionContextMenu: ContextMenu;
|
||||
eventContextMenu: ?ContextMenuInterface;
|
||||
instructionContextMenu: ?ContextMenuInterface;
|
||||
addNewEvent: (
|
||||
type: string,
|
||||
context: ?EventInsertionContext
|
||||
@@ -289,8 +290,10 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
onAddStandardEvent={this._addStandardEvent}
|
||||
onAddSubEvent={this.addSubEvents}
|
||||
canAddSubEvent={hasEventSelected(this.state.selection)}
|
||||
canToggleEventDisabled={hasEventSelected(this.state.selection)}
|
||||
onAddCommentEvent={this._addCommentEvent}
|
||||
onAddEvent={this.addNewEvent}
|
||||
onToggleDisabledEvent={this.toggleDisabled}
|
||||
canRemove={hasSomethingSelected(this.state.selection)}
|
||||
onRemove={this.deleteSelection}
|
||||
canUndo={canUndo(this.state.history)}
|
||||
@@ -638,11 +641,18 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
label: i18n._(t`Toggle disabled`),
|
||||
click: () => this.toggleDisabled(),
|
||||
enabled: this._selectionCanToggleDisabled(),
|
||||
accelerator: getShortcutDisplayName(
|
||||
this.props.preferences.values.userShortcutMap[
|
||||
'TOGGLE_EVENT_DISABLED'
|
||||
] || 'KeyD'
|
||||
),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Add New Event Below`),
|
||||
click: () => this.addNewEvent('BuiltinCommonInstructions::Standard'),
|
||||
click: () => {
|
||||
this.addNewEvent('BuiltinCommonInstructions::Standard');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Add Sub Event`),
|
||||
@@ -654,7 +664,9 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
submenu: this.state.allEventsMetadata.map(metadata => {
|
||||
return {
|
||||
label: metadata.fullName,
|
||||
click: () => this.addNewEvent(metadata.type),
|
||||
click: () => {
|
||||
this.addNewEvent(metadata.type);
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
@@ -730,7 +742,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
},
|
||||
() => {
|
||||
this.updateToolbar();
|
||||
this.eventContextMenu.open(x, y);
|
||||
if (this.eventContextMenu) this.eventContextMenu.open(x, y);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -751,7 +763,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
},
|
||||
() => {
|
||||
this.updateToolbar();
|
||||
this.instructionContextMenu.open(x, y);
|
||||
if (this.instructionContextMenu) this.instructionContextMenu.open(x, y);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -827,12 +839,18 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
};
|
||||
|
||||
toggleDisabled = () => {
|
||||
getSelectedEvents(this.state.selection).forEach(event =>
|
||||
event.setDisabled(!event.isDisabled())
|
||||
);
|
||||
this._saveChangesToHistory(() => {
|
||||
if (this._eventsTree) this._eventsTree.forceEventsUpdate();
|
||||
let shouldBeSaved = false;
|
||||
getSelectedEvents(this.state.selection).forEach(event => {
|
||||
if (event.isExecutable()) {
|
||||
event.setDisabled(!event.isDisabled());
|
||||
shouldBeSaved = true;
|
||||
}
|
||||
});
|
||||
if (shouldBeSaved) {
|
||||
this._saveChangesToHistory(() => {
|
||||
if (this._eventsTree) this._eventsTree.forceEventsUpdate();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
deleteSelection = ({
|
||||
|
@@ -76,9 +76,15 @@ export const browserOnlineWebExportPipeline: ExportPipeline<
|
||||
renderCustomStepsProgress: (
|
||||
build: ?Build,
|
||||
errored: boolean,
|
||||
exportStep: BuildStep
|
||||
exportStep: BuildStep,
|
||||
getGameThumbnailUrl: (buildId: string) => ?string
|
||||
) => (
|
||||
<WebProjectLink build={build} errored={errored} exportStep={exportStep} />
|
||||
<WebProjectLink
|
||||
build={build}
|
||||
errored={errored}
|
||||
exportStep={exportStep}
|
||||
getGameThumbnailUrl={getGameThumbnailUrl}
|
||||
/>
|
||||
),
|
||||
|
||||
prepareExporter: (
|
||||
|
@@ -89,7 +89,7 @@ const ExportHome = ({
|
||||
<div style={styles.titleContainer}>
|
||||
<Line>
|
||||
<Text size="title">
|
||||
<Trans>Publish your game on other stores</Trans>
|
||||
<Trans>Export and publish on other platforms</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</div>
|
||||
@@ -110,7 +110,7 @@ const ExportHome = ({
|
||||
</Text>
|
||||
</Line>
|
||||
<RaisedButton
|
||||
label={<Trans>Publish on stores</Trans>}
|
||||
label={<Trans>Export to other platforms</Trans>}
|
||||
onClick={() => {
|
||||
setChosenExporterSection('automated');
|
||||
setChosenExporterKey('webexport');
|
||||
|
@@ -26,7 +26,7 @@ import {
|
||||
updateGame,
|
||||
type Game,
|
||||
setGameUserAcls,
|
||||
getAclsFromAuthorIds,
|
||||
getAclsFromUserIds,
|
||||
} from '../../Utils/GDevelopServices/Game';
|
||||
import { type ExportPipeline } from '../ExportPipeline.flow';
|
||||
import { GameRegistration } from '../../GameDashboard/GameRegistration';
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
addCreateBadgePreHookIfNotClaimed,
|
||||
TRIVIAL_FIRST_WEB_EXPORT,
|
||||
} from '../../Utils/GDevelopServices/Badge';
|
||||
import { getWebBuildThumbnailUrl } from '../../Utils/GDevelopServices/Build';
|
||||
|
||||
type State = {|
|
||||
exportStep: BuildStep,
|
||||
@@ -138,8 +139,8 @@ export default class ExportLauncher extends Component<Props, State> {
|
||||
tryUpdateAuthors = async () => {
|
||||
const profile = this.props.authenticatedUser.profile;
|
||||
if (profile) {
|
||||
const authorAcls = getAclsFromAuthorIds(
|
||||
this.props.project.getAuthorIds()
|
||||
const authorAcls = getAclsFromUserIds(
|
||||
this.props.project.getAuthorIds().toJSArray()
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -147,7 +148,7 @@ export default class ExportLauncher extends Component<Props, State> {
|
||||
this.props.authenticatedUser.getAuthorizationHeader,
|
||||
profile.id,
|
||||
this.props.project.getProjectUuid(),
|
||||
authorAcls
|
||||
{ author: authorAcls }
|
||||
);
|
||||
} catch (e) {
|
||||
// Best effort call, do not prevent exporting the game.
|
||||
@@ -410,7 +411,14 @@ export default class ExportLauncher extends Component<Props, State> {
|
||||
)}
|
||||
{authenticatedUser.authenticated &&
|
||||
(exportPipeline.renderCustomStepsProgress ? (
|
||||
exportPipeline.renderCustomStepsProgress(build, errored, exportStep)
|
||||
exportPipeline.renderCustomStepsProgress(
|
||||
build,
|
||||
errored,
|
||||
exportStep,
|
||||
buildId =>
|
||||
this.props.project &&
|
||||
getWebBuildThumbnailUrl(this.props.project, buildId)
|
||||
)
|
||||
) : (
|
||||
<Line expand>
|
||||
<BuildStepsProgress
|
||||
|
@@ -13,6 +13,7 @@ import AlertMessage from '../../UI/AlertMessage';
|
||||
import { Tab, Tabs } from '../../UI/Tabs';
|
||||
import ExportHome from './ExportHome';
|
||||
import { getGame, type Game } from '../../Utils/GDevelopServices/Game';
|
||||
import { showWarningBox } from '../../UI/Messages/MessageBox';
|
||||
|
||||
const styles = {
|
||||
icon: { width: 40, height: 40 },
|
||||
@@ -85,6 +86,16 @@ const ExportDialog = ({
|
||||
const onlineStatus = useOnlineStatus();
|
||||
const cantExportBecauseOffline = !!allExportersRequireOnline && !onlineStatus;
|
||||
|
||||
const openBuildDialog = () => {
|
||||
if (!game) {
|
||||
showWarningBox(
|
||||
"Either this game is not registered or you are not its owner, so you can't see the builds or publish a build to the game page on Liluo.io."
|
||||
);
|
||||
return;
|
||||
}
|
||||
setBuildsDialogOpen(true);
|
||||
};
|
||||
|
||||
const loadGame = React.useCallback(
|
||||
async () => {
|
||||
if (!profile || !project) return;
|
||||
@@ -164,8 +175,8 @@ const ExportDialog = ({
|
||||
<FlatButton
|
||||
key="builds"
|
||||
label={<Trans>See this game builds</Trans>}
|
||||
onClick={() => setBuildsDialogOpen(true)}
|
||||
disabled={isNavigationDisabled || !game}
|
||||
onClick={openBuildDialog}
|
||||
disabled={isNavigationDisabled}
|
||||
/>,
|
||||
]}
|
||||
open
|
||||
|
@@ -49,7 +49,8 @@ export type ExportPipeline<
|
||||
renderCustomStepsProgress?: (
|
||||
build: ?Build,
|
||||
errored: boolean,
|
||||
exportStep: BuildStep
|
||||
exportStep: BuildStep,
|
||||
getGameThumbnailUrl: (buildId: string) => ?string
|
||||
) => React.Node,
|
||||
|
||||
prepareExporter: (
|
||||
|
@@ -64,12 +64,14 @@ type WebProjectLinkProps = {|
|
||||
build: ?Build,
|
||||
errored: boolean,
|
||||
exportStep: BuildStep,
|
||||
getGameThumbnailUrl: (buildId: string) => ?string,
|
||||
|};
|
||||
|
||||
export const WebProjectLink = ({
|
||||
build,
|
||||
errored,
|
||||
exportStep,
|
||||
getGameThumbnailUrl,
|
||||
}: WebProjectLinkProps) => {
|
||||
const [showCopiedInfoBar, setShowCopiedInfoBar] = React.useState<boolean>(
|
||||
false
|
||||
@@ -174,6 +176,7 @@ export const WebProjectLink = ({
|
||||
game.id,
|
||||
{
|
||||
publicWebBuildId: build.id,
|
||||
thumbnailUrl: build.id ? getGameThumbnailUrl(build.id) : undefined,
|
||||
}
|
||||
);
|
||||
setGame(updatedGame);
|
||||
@@ -183,7 +186,7 @@ export const WebProjectLink = ({
|
||||
setIsGameLoading(false);
|
||||
}
|
||||
},
|
||||
[game, getAuthorizationHeader, profile, build]
|
||||
[game, getAuthorizationHeader, profile, build, getGameThumbnailUrl]
|
||||
);
|
||||
|
||||
if (!build && !exportStep) return null;
|
||||
|
@@ -70,9 +70,15 @@ export const localOnlineWebExportPipeline: ExportPipeline<
|
||||
renderCustomStepsProgress: (
|
||||
build: ?Build,
|
||||
errored: boolean,
|
||||
exportStep: BuildStep
|
||||
exportStep: BuildStep,
|
||||
getGameThumbnailUrl: (buildId: string) => ?string
|
||||
) => (
|
||||
<WebProjectLink build={build} errored={errored} exportStep={exportStep} />
|
||||
<WebProjectLink
|
||||
build={build}
|
||||
errored={errored}
|
||||
exportStep={exportStep}
|
||||
getGameThumbnailUrl={getGameThumbnailUrl}
|
||||
/>
|
||||
),
|
||||
|
||||
prepareExporter: (
|
||||
|
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Card, CardActions, CardHeader, Chip } from '@material-ui/core';
|
||||
import { Card, CardActions, CardHeader, Chip, Paper } from '@material-ui/core';
|
||||
import * as React from 'react';
|
||||
import { Line, Spacer } from '../UI/Grid';
|
||||
import { Column, Line, Spacer } from '../UI/Grid';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import { getGameUrl, type Game } from '../Utils/GDevelopServices/Game';
|
||||
import TimelineIcon from '@material-ui/icons/Timeline';
|
||||
@@ -12,6 +12,18 @@ import TuneIcon from '@material-ui/icons/Tune';
|
||||
import { ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
import Window from '../Utils/Window';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
|
||||
const styles = {
|
||||
image: {
|
||||
display: 'block',
|
||||
objectFit: 'cover',
|
||||
},
|
||||
thumbnail: {
|
||||
width: 240,
|
||||
height: 135,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
game: Game,
|
||||
@@ -33,68 +45,95 @@ export const GameCard = ({
|
||||
if (!url) return;
|
||||
Window.openExternalURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Card key={game.id}>
|
||||
<CardHeader
|
||||
title={game.gameName}
|
||||
subheader={
|
||||
<Line alignItems="center" noMargin>
|
||||
<Trans>Created on {i18n.date(game.createdAt * 1000)}</Trans>
|
||||
{isCurrentGame && (
|
||||
<>
|
||||
<Spacer />
|
||||
<Chip
|
||||
size="small"
|
||||
label={<Trans>Currently edited</Trans>}
|
||||
color="primary"
|
||||
<Line>
|
||||
{game.thumbnailUrl ? (
|
||||
<img
|
||||
src={game.thumbnailUrl}
|
||||
style={{
|
||||
...styles.image,
|
||||
...styles.thumbnail,
|
||||
}}
|
||||
alt={game.gameName}
|
||||
title={game.gameName}
|
||||
/>
|
||||
) : (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
style={{
|
||||
...styles.thumbnail,
|
||||
whiteSpace: 'normal',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<EmptyMessage>
|
||||
<Trans>No thumbnail set</Trans>
|
||||
</EmptyMessage>
|
||||
</Paper>
|
||||
)}
|
||||
<Column expand>
|
||||
<CardHeader
|
||||
title={game.gameName}
|
||||
subheader={
|
||||
<Line alignItems="center" noMargin>
|
||||
<Trans>Created on {i18n.date(game.createdAt * 1000)}</Trans>
|
||||
{isCurrentGame && (
|
||||
<>
|
||||
<Spacer />
|
||||
<Chip
|
||||
size="small"
|
||||
label={<Trans>Currently edited</Trans>}
|
||||
color="primary"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{game.publicWebBuildId && (
|
||||
<>
|
||||
<Spacer />
|
||||
<Chip
|
||||
size="small"
|
||||
label={<Trans>Published on Liluo</Trans>}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Line>
|
||||
}
|
||||
/>
|
||||
<CardActions>
|
||||
<ResponsiveLineStackLayout
|
||||
expand
|
||||
noMargin
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
{game.publicWebBuildId && (
|
||||
<RaisedButton
|
||||
label={<Trans>Open</Trans>}
|
||||
onClick={openGameUrl}
|
||||
primary
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{game.publicWebBuildId && (
|
||||
<>
|
||||
<Spacer />
|
||||
<Chip
|
||||
size="small"
|
||||
label={<Trans>Published on Liluo</Trans>}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Line>
|
||||
}
|
||||
/>
|
||||
<CardActions>
|
||||
<ResponsiveLineStackLayout
|
||||
expand
|
||||
noMargin
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
{game.publicWebBuildId && (
|
||||
<RaisedButton
|
||||
label={<Trans>Open</Trans>}
|
||||
onClick={openGameUrl}
|
||||
primary
|
||||
/>
|
||||
)}
|
||||
<FlatButton
|
||||
icon={<TuneIcon />}
|
||||
label={<Trans>Details</Trans>}
|
||||
onClick={onOpenDetails}
|
||||
/>
|
||||
<FlatButton
|
||||
icon={<PlaylistPlayIcon />}
|
||||
label={<Trans>Builds</Trans>}
|
||||
onClick={onOpenBuilds}
|
||||
/>
|
||||
<FlatButton
|
||||
icon={<TimelineIcon />}
|
||||
label={<Trans>Analytics</Trans>}
|
||||
onClick={onOpenAnalytics}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</CardActions>
|
||||
)}
|
||||
<FlatButton
|
||||
icon={<TuneIcon />}
|
||||
label={<Trans>Details</Trans>}
|
||||
onClick={onOpenDetails}
|
||||
/>
|
||||
<FlatButton
|
||||
icon={<PlaylistPlayIcon />}
|
||||
label={<Trans>Builds</Trans>}
|
||||
onClick={onOpenBuilds}
|
||||
/>
|
||||
<FlatButton
|
||||
icon={<TimelineIcon />}
|
||||
label={<Trans>Analytics</Trans>}
|
||||
onClick={onOpenAnalytics}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</CardActions>
|
||||
</Column>
|
||||
</Line>
|
||||
</Card>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -1,8 +1,10 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import * as React from 'react';
|
||||
import { format, formatISO } from 'date-fns';
|
||||
import { formatISO } from 'date-fns';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { Line, Spacer } from '../UI/Grid';
|
||||
import {
|
||||
@@ -11,7 +13,8 @@ import {
|
||||
deleteGame,
|
||||
getPublicGame,
|
||||
setGameUserAcls,
|
||||
getAclsFromAuthorIds,
|
||||
getAclsFromUserIds,
|
||||
getCategoryName,
|
||||
} from '../Utils/GDevelopServices/Game';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import { Tab, Tabs } from '../UI/Tabs';
|
||||
@@ -35,11 +38,16 @@ import Window from '../Utils/Window';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import { type PublicGame } from '../Utils/GDevelopServices/Game';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PublicGamePropertiesDialog from '../ProjectManager/PublicGamePropertiesDialog';
|
||||
import {
|
||||
PublicGamePropertiesDialog,
|
||||
type PartialGameChange,
|
||||
} from '../ProjectManager/PublicGamePropertiesDialog';
|
||||
import TextField from '../UI/TextField';
|
||||
import KeyboardIcon from '@material-ui/icons/Keyboard';
|
||||
import SportsEsportsIcon from '@material-ui/icons/SportsEsports';
|
||||
import SmartphoneIcon from '@material-ui/icons/Smartphone';
|
||||
import Crown from '../UI/CustomSvgIcons/Crown';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
|
||||
const styles = {
|
||||
tableRowStatColumn: {
|
||||
@@ -144,28 +152,59 @@ export const GameDetailsDialog = ({
|
||||
[loadPublicGame]
|
||||
);
|
||||
|
||||
const updateGameFromProject = async () => {
|
||||
const updateGameFromProject = async (
|
||||
partialGameChange: PartialGameChange,
|
||||
i18n: I18nType
|
||||
) => {
|
||||
if (!project || !profile) return;
|
||||
const { id } = profile;
|
||||
|
||||
// Set public game to null as it will be refetched automatically by the callback above.
|
||||
setPublicGame(null);
|
||||
try {
|
||||
// Set public game to null as it will be refetched automatically by the callback above.
|
||||
setPublicGame(null);
|
||||
const gameId = project.getProjectUuid();
|
||||
const updatedGame = await updateGame(getAuthorizationHeader, id, gameId, {
|
||||
authorName: project.getAuthor() || 'Unspecified publisher',
|
||||
gameName: project.getName() || 'Untitle game',
|
||||
categories: project.getCategories().toJSArray() || [],
|
||||
description: project.getDescription() || '',
|
||||
playWithKeyboard: project.isPlayableWithKeyboard(),
|
||||
playWithGamepad: project.isPlayableWithGamepad(),
|
||||
playWithMobile: project.isPlayableWithMobile(),
|
||||
orientation: project.getOrientation(),
|
||||
// The thumbnailUrl is updated only when a build is made public.
|
||||
});
|
||||
const authorAcls = getAclsFromAuthorIds(project.getAuthorIds());
|
||||
await setGameUserAcls(getAuthorizationHeader, id, gameId, authorAcls);
|
||||
try {
|
||||
const authorAcls = getAclsFromUserIds(
|
||||
project.getAuthorIds().toJSArray()
|
||||
);
|
||||
const ownerAcls = getAclsFromUserIds(partialGameChange.ownerIds);
|
||||
await setGameUserAcls(getAuthorizationHeader, id, gameId, {
|
||||
ownership: ownerAcls,
|
||||
author: authorAcls,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Unable to update the game owners or authors:', error);
|
||||
showErrorBox({
|
||||
message:
|
||||
i18n._(t`Unable to update the game owners or authors.`) +
|
||||
' ' +
|
||||
i18n._(t`Verify your internet connection or try again later.`),
|
||||
rawError: error,
|
||||
errorId: 'game-acls-update-error',
|
||||
});
|
||||
}
|
||||
onGameUpdated(updatedGame);
|
||||
} catch (error) {
|
||||
console.error('Unable to update the game:', error);
|
||||
showErrorBox({
|
||||
message:
|
||||
i18n._(t`Unable to update the game details.`) +
|
||||
' ' +
|
||||
i18n._(t`Verify your internet connection or try again later.`),
|
||||
rawError: error,
|
||||
errorId: 'game-details-update-error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -206,362 +245,423 @@ export const GameDetailsDialog = ({
|
||||
);
|
||||
|
||||
const authorUsernames =
|
||||
publicGame && publicGame.authors
|
||||
? publicGame.authors
|
||||
.map(author => (author ? author.username : null))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
publicGame &&
|
||||
publicGame.authors.map(author => author.username).filter(Boolean);
|
||||
|
||||
const ownerUsernames =
|
||||
publicGame &&
|
||||
publicGame.owners.map(owner => owner.username).filter(Boolean);
|
||||
|
||||
const isGameOpenedAsProject =
|
||||
!!project && project.getProjectUuid() === game.id;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={
|
||||
<span>
|
||||
{game.gameName}
|
||||
{' - '}
|
||||
<Trans>Dashboard</Trans>
|
||||
</span>
|
||||
}
|
||||
open
|
||||
noMargin
|
||||
onRequestClose={onClose}
|
||||
maxWidth="md"
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Close</Trans>}
|
||||
onClick={onClose}
|
||||
key="close"
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath="/interface/games-dashboard" />,
|
||||
]}
|
||||
>
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
<Tab label={<Trans>Details</Trans>} value="details" />
|
||||
<Tab label={<Trans>Builds</Trans>} value="builds" />
|
||||
<Tab label={<Trans>Analytics</Trans>} value="analytics" />
|
||||
</Tabs>
|
||||
<Line>
|
||||
{currentTab === 'details' ? (
|
||||
publicGameError ? (
|
||||
<PlaceholderError onRetry={loadPublicGame}>
|
||||
<Trans>There was an issue getting the game details.</Trans>{' '}
|
||||
<Trans>Verify your internet connection or try again later.</Trans>
|
||||
</PlaceholderError>
|
||||
) : !publicGame ? (
|
||||
<PlaceholderLoader />
|
||||
) : (
|
||||
<ColumnStackLayout expand>
|
||||
{!isGameOpenedAsProject && (
|
||||
<AlertMessage kind="info">
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Dialog
|
||||
title={
|
||||
<span>
|
||||
{game.gameName}
|
||||
{' - '}
|
||||
<Trans>Dashboard</Trans>
|
||||
</span>
|
||||
}
|
||||
open
|
||||
noMargin
|
||||
onRequestClose={onClose}
|
||||
maxWidth="md"
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Close</Trans>}
|
||||
onClick={onClose}
|
||||
key="close"
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath="/interface/games-dashboard" />,
|
||||
]}
|
||||
>
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
<Tab label={<Trans>Details</Trans>} value="details" />
|
||||
<Tab label={<Trans>Builds</Trans>} value="builds" />
|
||||
<Tab label={<Trans>Analytics</Trans>} value="analytics" />
|
||||
</Tabs>
|
||||
<Line>
|
||||
{currentTab === 'details' ? (
|
||||
publicGameError ? (
|
||||
<PlaceholderError onRetry={loadPublicGame}>
|
||||
<Trans>There was an issue getting the game details.</Trans>{' '}
|
||||
<Trans>
|
||||
In order to update these details you have to open the game's
|
||||
project.
|
||||
Verify your internet connection or try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
<Line alignItems="center">
|
||||
<Line expand justifyContent="flex-start" alignItems="center">
|
||||
{authorUsernames && (
|
||||
<>
|
||||
<Text>
|
||||
<Trans>Authors:</Trans>
|
||||
</Text>
|
||||
<Line>
|
||||
{authorUsernames.map((username, index) => (
|
||||
<React.Fragment key={username}>
|
||||
<Spacer />
|
||||
<Chip
|
||||
size="small"
|
||||
label={username}
|
||||
color={index === 0 ? 'primary' : 'default'}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Line>
|
||||
</>
|
||||
</PlaceholderError>
|
||||
) : !publicGame ? (
|
||||
<PlaceholderLoader />
|
||||
) : (
|
||||
<ColumnStackLayout expand>
|
||||
{!isGameOpenedAsProject && (
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
In order to update these details you have to open the
|
||||
game's project.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
</Line>
|
||||
<Line expand justifyContent="flex-end">
|
||||
<Text>
|
||||
<Trans>
|
||||
Created on{' '}
|
||||
{format(game.createdAt * 1000 /* TODO */, 'yyyy-MM-dd')}
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</Line>
|
||||
{(publicGame.playWithKeyboard ||
|
||||
publicGame.playWithGamepad ||
|
||||
publicGame.playWithMobile) && (
|
||||
<Line expand justifyContent="flex-start" alignItems="center">
|
||||
{publicGame.playWithKeyboard && <KeyboardIcon />}
|
||||
{publicGame.playWithGamepad && <SportsEsportsIcon />}
|
||||
{publicGame.playWithMobile && <SmartphoneIcon />}
|
||||
</Line>
|
||||
)}
|
||||
<TextField
|
||||
value={publicGame.gameName}
|
||||
readOnly
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Game name</Trans>}
|
||||
floatingLabelFixed={true}
|
||||
/>
|
||||
<TextField
|
||||
value={publicGame.description || ''}
|
||||
readOnly
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Game description</Trans>}
|
||||
floatingLabelFixed={true}
|
||||
hintText={t`No description set.`}
|
||||
multiline
|
||||
rows={5}
|
||||
/>
|
||||
<SelectField
|
||||
disabled
|
||||
fullWidth
|
||||
floatingLabelText={
|
||||
<Trans>Device orientation (for mobile)</Trans>
|
||||
}
|
||||
value={publicGame.orientation}
|
||||
>
|
||||
<SelectOption
|
||||
value="default"
|
||||
primaryText={t`Platform default`}
|
||||
/>
|
||||
<SelectOption value="landscape" primaryText={t`Landscape`} />
|
||||
<SelectOption value="portrait" primaryText={t`Portrait`} />
|
||||
</SelectField>
|
||||
<Line noMargin justifyContent="flex-end">
|
||||
<FlatButton
|
||||
onClick={() => {
|
||||
const answer = Window.showConfirmDialog(
|
||||
"Are you sure you want to unregister this game? \n\nIt will disappear from your games dashboard and you won't get access to analytics, unless you register it again."
|
||||
);
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
unregisterGame();
|
||||
}}
|
||||
label={<Trans>Unregister this game</Trans>}
|
||||
/>
|
||||
<Spacer />
|
||||
{publicGame.publicWebBuildId && (
|
||||
<>
|
||||
<RaisedButton
|
||||
<Line alignItems="center" noMargin>
|
||||
<Line
|
||||
expand
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
{authorUsernames && (
|
||||
<>
|
||||
<Text>
|
||||
<Trans>Authors:</Trans>
|
||||
</Text>
|
||||
<Line noMargin>
|
||||
{authorUsernames.map((username, index) => (
|
||||
<React.Fragment key={username}>
|
||||
<Spacer />
|
||||
<Chip
|
||||
size="small"
|
||||
icon={
|
||||
ownerUsernames &&
|
||||
ownerUsernames.includes(username) ? (
|
||||
<Crown />
|
||||
) : (
|
||||
undefined
|
||||
)
|
||||
}
|
||||
label={username}
|
||||
color={index === 0 ? 'primary' : 'default'}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
</Line>
|
||||
<Line expand justifyContent="flex-end" noMargin>
|
||||
<Text>
|
||||
<Trans>
|
||||
Created on {i18n.date(game.createdAt * 1000)}
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</Line>
|
||||
{(publicGame.playWithKeyboard ||
|
||||
publicGame.playWithGamepad ||
|
||||
publicGame.playWithMobile ||
|
||||
publicGame.categories) && (
|
||||
<Line alignItems="center" noMargin>
|
||||
<Line
|
||||
expand
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
{publicGame.categories &&
|
||||
!!publicGame.categories.length && (
|
||||
<>
|
||||
<Text>
|
||||
<Trans>Genres:</Trans>
|
||||
</Text>
|
||||
<Line noMargin>
|
||||
{publicGame.categories.map(
|
||||
(category, index) => (
|
||||
<React.Fragment key={category}>
|
||||
<Spacer />
|
||||
<Chip
|
||||
size="small"
|
||||
label={getCategoryName(category, i18n)}
|
||||
color={
|
||||
index === 0 ? 'primary' : 'default'
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
</Line>
|
||||
<Line expand justifyContent="flex-end" noMargin>
|
||||
{publicGame.playWithKeyboard && <KeyboardIcon />}
|
||||
{publicGame.playWithGamepad && <SportsEsportsIcon />}
|
||||
{publicGame.playWithMobile && <SmartphoneIcon />}
|
||||
</Line>
|
||||
</Line>
|
||||
)}
|
||||
<TextField
|
||||
value={publicGame.gameName}
|
||||
readOnly
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Game name</Trans>}
|
||||
floatingLabelFixed={true}
|
||||
/>
|
||||
<TextField
|
||||
value={publicGame.description || ''}
|
||||
readOnly
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Game description</Trans>}
|
||||
floatingLabelFixed={true}
|
||||
hintText={t`No description set.`}
|
||||
multiline
|
||||
rows={5}
|
||||
/>
|
||||
<SelectField
|
||||
disabled
|
||||
fullWidth
|
||||
floatingLabelText={
|
||||
<Trans>Device orientation (for mobile)</Trans>
|
||||
}
|
||||
value={publicGame.orientation}
|
||||
>
|
||||
<SelectOption
|
||||
value="default"
|
||||
primaryText={t`Platform default`}
|
||||
/>
|
||||
<SelectOption
|
||||
value="landscape"
|
||||
primaryText={t`Landscape`}
|
||||
/>
|
||||
<SelectOption value="portrait" primaryText={t`Portrait`} />
|
||||
</SelectField>
|
||||
<Line noMargin justifyContent="flex-end">
|
||||
<FlatButton
|
||||
onClick={() => {
|
||||
const answer = Window.showConfirmDialog(
|
||||
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo unique game URL not accessible anymore. \n\nYou can decide anytime to publish it again.'
|
||||
"Are you sure you want to unregister this game? \n\nIt will disappear from your games dashboard and you won't get access to analytics, unless you register it again."
|
||||
);
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
unpublishGame();
|
||||
unregisterGame();
|
||||
}}
|
||||
label={<Trans>Unpublish from Liluo</Trans>}
|
||||
label={<Trans>Unregister this game</Trans>}
|
||||
/>
|
||||
<Spacer />
|
||||
</>
|
||||
)}
|
||||
<RaisedButton
|
||||
primary
|
||||
onClick={() => setIsPublicGamePropertiesDialogOpen(true)}
|
||||
label={<Trans>Edit game details</Trans>}
|
||||
disabled={!isGameOpenedAsProject}
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
) : null}
|
||||
{currentTab === 'builds' ? (
|
||||
<Builds
|
||||
game={game}
|
||||
authenticatedUser={authenticatedUser}
|
||||
onGameUpdated={onGameUpdated}
|
||||
/>
|
||||
) : null}
|
||||
{currentTab === 'analytics' ? (
|
||||
gameRollingMetricsError ? (
|
||||
<PlaceholderError
|
||||
onRetry={() => {
|
||||
loadGameMetrics();
|
||||
}}
|
||||
>
|
||||
<Trans>There was an issue getting the game analytics.</Trans>{' '}
|
||||
<Trans>Verify your internet connection or try again later.</Trans>
|
||||
</PlaceholderError>
|
||||
) : (
|
||||
<ColumnStackLayout expand>
|
||||
<Line noMargin alignItems="center">
|
||||
<Text size="title">
|
||||
<Trans>Consolidated metrics</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{!publicGame && <CircularProgress size={20} />}
|
||||
</Line>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Last week sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame &&
|
||||
publicGame.metrics &&
|
||||
publicGame.metrics.lastWeekSessionsCount
|
||||
? publicGame.metrics.lastWeekSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Last year sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame &&
|
||||
publicGame.metrics &&
|
||||
publicGame.metrics.lastYearSessionsCount
|
||||
? publicGame.metrics.lastYearSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Line noMargin alignItems="center">
|
||||
<Text size="title">
|
||||
<Trans>Daily metrics</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{isGameMetricsLoading && <CircularProgress size={20} />}
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Day</Trans>}
|
||||
value={analyticsDate}
|
||||
onChange={(_, _index, newIsoDate) => {
|
||||
setAnalyticsDate(newIsoDate);
|
||||
{publicGame.publicWebBuildId && (
|
||||
<>
|
||||
<RaisedButton
|
||||
onClick={() => {
|
||||
const answer = Window.showConfirmDialog(
|
||||
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo unique game URL not accessible anymore. \n\nYou can decide anytime to publish it again.'
|
||||
);
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
unpublishGame();
|
||||
}}
|
||||
label={<Trans>Unpublish from Liluo</Trans>}
|
||||
/>
|
||||
<Spacer />
|
||||
</>
|
||||
)}
|
||||
<RaisedButton
|
||||
primary
|
||||
onClick={() => setIsPublicGamePropertiesDialogOpen(true)}
|
||||
label={<Trans>Edit game details</Trans>}
|
||||
disabled={!isGameOpenedAsProject}
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
) : null}
|
||||
{currentTab === 'builds' ? (
|
||||
<Builds
|
||||
game={game}
|
||||
authenticatedUser={authenticatedUser}
|
||||
onGameUpdated={onGameUpdated}
|
||||
/>
|
||||
) : null}
|
||||
{currentTab === 'analytics' ? (
|
||||
gameRollingMetricsError ? (
|
||||
<PlaceholderError
|
||||
onRetry={() => {
|
||||
loadGameMetrics();
|
||||
}}
|
||||
>
|
||||
{Array(5)
|
||||
.fill('')
|
||||
.map((_, index) => {
|
||||
const isoDate = formatISO(
|
||||
subDays(new Date(), index + 2),
|
||||
{
|
||||
representation: 'date',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<SelectOption
|
||||
key={isoDate}
|
||||
value={isoDate}
|
||||
primaryText={isoDate}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.reverse()}
|
||||
<SelectOption
|
||||
value={yesterdayIsoDate}
|
||||
primaryText={t`Yesterday`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={formatISO(new Date(), { representation: 'date' })}
|
||||
primaryText={t`Today (so far, in real time)`}
|
||||
/>
|
||||
</SelectField>
|
||||
</Line>
|
||||
{!isGameMetricsLoading && !gameRollingMetrics ? (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>There was an issue getting the game analytics.</Trans>{' '}
|
||||
<Trans>
|
||||
There were no players or stored metrics for this day. Be
|
||||
sure to publish your game and get players to try it to see
|
||||
the collected anonymous analytics.
|
||||
Verify your internet connection or try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Players count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.players
|
||||
? gameRollingMetrics.players.d0Players
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.sessions
|
||||
? gameRollingMetrics.sessions.d0Sessions
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>New players count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.players
|
||||
? gameRollingMetrics.players.d0NewPlayers
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{gameRollingMetrics &&
|
||||
(!gameRollingMetrics.retention || !gameRollingMetrics.players) ? (
|
||||
<AlertMessage kind="info">
|
||||
Upgrade your account with a subscription to unlock all the
|
||||
metrics for your game.
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableBody>
|
||||
{[1, 2, 3, 4, 5, 6, 7].map(dayIndex => (
|
||||
<TableRow key={dayIndex}>
|
||||
<TableRowColumn>
|
||||
<Trans>Day {dayIndex} retained players</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics &&
|
||||
gameRollingMetrics.retention &&
|
||||
gameRollingMetrics.retention[
|
||||
`d${dayIndex}RetainedPlayers`
|
||||
] != null
|
||||
? gameRollingMetrics.retention[
|
||||
</PlaceholderError>
|
||||
) : (
|
||||
<ColumnStackLayout expand>
|
||||
<Line noMargin alignItems="center">
|
||||
<Text size="title">
|
||||
<Trans>Consolidated metrics</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{!publicGame && <CircularProgress size={20} />}
|
||||
</Line>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Last week sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame &&
|
||||
publicGame.metrics &&
|
||||
publicGame.metrics.lastWeekSessionsCount
|
||||
? publicGame.metrics.lastWeekSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Last year sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame &&
|
||||
publicGame.metrics &&
|
||||
publicGame.metrics.lastYearSessionsCount
|
||||
? publicGame.metrics.lastYearSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Line noMargin alignItems="center">
|
||||
<Text size="title">
|
||||
<Trans>Daily metrics</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{isGameMetricsLoading && <CircularProgress size={20} />}
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Day</Trans>}
|
||||
value={analyticsDate}
|
||||
onChange={(_, _index, newIsoDate) => {
|
||||
setAnalyticsDate(newIsoDate);
|
||||
}}
|
||||
>
|
||||
{Array(5)
|
||||
.fill('')
|
||||
.map((_, index) => {
|
||||
const isoDate = formatISO(
|
||||
subDays(new Date(), index + 2),
|
||||
{
|
||||
representation: 'date',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<SelectOption
|
||||
key={isoDate}
|
||||
value={isoDate}
|
||||
primaryText={isoDate}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.reverse()}
|
||||
<SelectOption
|
||||
value={yesterdayIsoDate}
|
||||
primaryText={t`Yesterday`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={formatISO(new Date(), {
|
||||
representation: 'date',
|
||||
})}
|
||||
primaryText={t`Today (so far, in real time)`}
|
||||
/>
|
||||
</SelectField>
|
||||
</Line>
|
||||
{!isGameMetricsLoading && !gameRollingMetrics ? (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
There were no players or stored metrics for this day. Be
|
||||
sure to publish your game and get players to try it to
|
||||
see the collected anonymous analytics.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Players count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.players
|
||||
? gameRollingMetrics.players.d0Players
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.sessions
|
||||
? gameRollingMetrics.sessions.d0Sessions
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>New players count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.players
|
||||
? gameRollingMetrics.players.d0NewPlayers
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{gameRollingMetrics &&
|
||||
(!gameRollingMetrics.retention ||
|
||||
!gameRollingMetrics.players) ? (
|
||||
<AlertMessage kind="info">
|
||||
Upgrade your account with a subscription to unlock all the
|
||||
metrics for your game.
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableBody>
|
||||
{[1, 2, 3, 4, 5, 6, 7].map(dayIndex => (
|
||||
<TableRow key={dayIndex}>
|
||||
<TableRowColumn>
|
||||
<Trans>Day {dayIndex} retained players</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics &&
|
||||
gameRollingMetrics.retention &&
|
||||
gameRollingMetrics.retention[
|
||||
`d${dayIndex}RetainedPlayers`
|
||||
]
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
) : null}
|
||||
</Line>
|
||||
{publicGame && project && (
|
||||
<PublicGamePropertiesDialog
|
||||
open={isPublicGamePropertiesDialogOpen}
|
||||
project={project}
|
||||
game={publicGame}
|
||||
onApply={() => {
|
||||
setIsPublicGamePropertiesDialogOpen(false);
|
||||
updateGameFromProject();
|
||||
}}
|
||||
onClose={() => setIsPublicGamePropertiesDialogOpen(false)}
|
||||
/>
|
||||
] != null
|
||||
? gameRollingMetrics.retention[
|
||||
`d${dayIndex}RetainedPlayers`
|
||||
]
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
) : null}
|
||||
</Line>
|
||||
{publicGame && project && (
|
||||
<PublicGamePropertiesDialog
|
||||
open={isPublicGamePropertiesDialogOpen}
|
||||
project={project}
|
||||
publicGame={publicGame}
|
||||
onApply={partialGameChange => {
|
||||
setIsPublicGamePropertiesDialogOpen(false);
|
||||
updateGameFromProject(partialGameChange, i18n);
|
||||
}}
|
||||
onClose={() => setIsPublicGamePropertiesDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
)}
|
||||
</Dialog>
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
@@ -37,7 +37,7 @@ export const getExtraObjectsInformation = (): {
|
||||
'BitmapText::BitmapTextObject': [
|
||||
{
|
||||
kind: 'warning',
|
||||
message: t`This object is experimental and not yet complete. It might have bugs or incomplete support in GDevelop, be sure to read the wiki by clicking on help button bellow.`,
|
||||
message: t`This object is experimental and not yet complete. It might have bugs or incomplete support in GDevelop, be sure to read the wiki by clicking on help button below.`,
|
||||
},
|
||||
{
|
||||
kind: 'info',
|
||||
@@ -90,6 +90,12 @@ export const getExtraInstructionInformation = (type: string): ?Hint => {
|
||||
message: t`To start a timer, don't forget to use the action "Start (or reset) an object timer" in another event.`,
|
||||
};
|
||||
}
|
||||
if (type === 'FixCamera') {
|
||||
return {
|
||||
kind: 'info',
|
||||
message: t`Please prefer using the new action "Enforce camera boundaries" which is more flexible.`,
|
||||
};
|
||||
}
|
||||
if (type === 'BitmapText::Scale') {
|
||||
return {
|
||||
kind: 'info',
|
||||
|
@@ -35,15 +35,23 @@ export default class InstancePropertiesEditor extends React.Component<Props> {
|
||||
_instanceVariablesList: { current: null | VariablesList } = React.createRef();
|
||||
schema: Schema = [
|
||||
{
|
||||
name: this.props.i18n._(t`Object name`),
|
||||
valueType: 'string',
|
||||
disabled: true,
|
||||
name: this.props.i18n._(t`Object`),
|
||||
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
|
||||
setValue: (instance: gdInitialInstance, newValue: string) =>
|
||||
instance.setObjectName(newValue),
|
||||
onEditButtonClick: (instance: gdInitialInstance) =>
|
||||
nonFieldType: 'sectionTitle',
|
||||
defaultValue: this.props.i18n._(t`Different objects`),
|
||||
},
|
||||
{
|
||||
label: this.props.i18n._(t`Edit object`),
|
||||
disabled: 'onValuesDifferent',
|
||||
nonFieldType: 'button',
|
||||
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
|
||||
onClick: (instance: gdInitialInstance) =>
|
||||
this.props.onEditObjectByName(instance.getObjectName()),
|
||||
},
|
||||
{
|
||||
name: this.props.i18n._(t`Instance`),
|
||||
nonFieldType: 'sectionTitle',
|
||||
},
|
||||
{
|
||||
name: this.props.i18n._(t`Position`),
|
||||
type: 'row',
|
||||
|
@@ -56,7 +56,11 @@ export type InstancesEditorPropsWithoutSizeAndScroll = {|
|
||||
onInstancesResized: (instances: Array<gdInitialInstance>) => void,
|
||||
onInstancesRotated: (instances: Array<gdInitialInstance>) => void,
|
||||
selectedObjectNames: Array<string>,
|
||||
onContextMenu: (x: number, y: number) => void,
|
||||
onContextMenu: (
|
||||
x: number,
|
||||
y: number,
|
||||
ignoreSelectedObjectNamesForContextMenu?: boolean
|
||||
) => void,
|
||||
onCopy: () => void,
|
||||
onCut: () => void,
|
||||
onPaste: () => void,
|
||||
@@ -208,6 +212,7 @@ export default class InstancesEditor extends Component<Props> {
|
||||
offsetY: event.offsetY,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
ignoreSelectedObjectNamesForContextMenu: true,
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -337,7 +342,7 @@ export default class InstancesEditor extends Component<Props> {
|
||||
onDownInstance: this._onDownInstance,
|
||||
onOutInstance: this._onOutInstance,
|
||||
onInstanceClicked: this._onInstanceClicked,
|
||||
onInstanceRightClicked: this._onRightClicked,
|
||||
onInstanceRightClicked: this._onInstanceRightClicked,
|
||||
onInstanceDoubleClicked: this._onInstanceDoubleClicked,
|
||||
});
|
||||
this.selectionRectangle = new SelectionRectangle({
|
||||
@@ -604,20 +609,36 @@ export default class InstancesEditor extends Component<Props> {
|
||||
this.pixiRenderer.view.focus();
|
||||
};
|
||||
|
||||
_onRightClicked = ({
|
||||
offsetX,
|
||||
offsetY,
|
||||
x,
|
||||
y,
|
||||
}: {|
|
||||
_onInstanceRightClicked = (coordinates: {|
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
x: number,
|
||||
y: number,
|
||||
|}) => {
|
||||
this._onRightClicked({
|
||||
...coordinates,
|
||||
ignoreSelectedObjectNamesForContextMenu: false,
|
||||
});
|
||||
};
|
||||
|
||||
_onRightClicked = ({
|
||||
offsetX,
|
||||
offsetY,
|
||||
x,
|
||||
y,
|
||||
ignoreSelectedObjectNamesForContextMenu,
|
||||
}: {|
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
x: number,
|
||||
y: number,
|
||||
ignoreSelectedObjectNamesForContextMenu?: boolean,
|
||||
|}) => {
|
||||
this.lastContextMenuX = offsetX;
|
||||
this.lastContextMenuY = offsetY;
|
||||
if (this.props.onContextMenu) this.props.onContextMenu(x, y);
|
||||
if (this.props.onContextMenu) {
|
||||
this.props.onContextMenu(x, y, !!ignoreSelectedObjectNamesForContextMenu);
|
||||
}
|
||||
};
|
||||
|
||||
_onInstanceDoubleClicked = (instance: gdInitialInstance) => {
|
||||
|
@@ -51,6 +51,7 @@ const defaultShortcuts: ShortcutMap = {
|
||||
ADD_STANDARD_EVENT: 'Shift+KeyA',
|
||||
ADD_SUBEVENT: 'Shift+KeyD',
|
||||
ADD_COMMENT_EVENT: '',
|
||||
TOGGLE_EVENT_DISABLED: 'KeyD',
|
||||
CHOOSE_AND_ADD_EVENT: 'Shift+KeyW',
|
||||
OPEN_EXTENSION_SETTINGS: '',
|
||||
};
|
||||
|
@@ -272,7 +272,7 @@ export default class AboutDialog extends PureComponent<Props, State> {
|
||||
key="website"
|
||||
label={<Trans>GDevelop Website</Trans>}
|
||||
primary={false}
|
||||
onClick={() => Window.openExternalURL('http://gdevelop-app.com')}
|
||||
onClick={() => Window.openExternalURL('https://gdevelop.io')}
|
||||
/>,
|
||||
<FlatButton
|
||||
key="close"
|
||||
|
@@ -6,50 +6,47 @@ import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import Changelog from '.';
|
||||
import Text from '../../UI/Text';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
|
||||
type Props = {|
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
|};
|
||||
|
||||
type State = {||};
|
||||
|
||||
export default class ChangelogDialog extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { open, onClose } = this.props;
|
||||
if (!open) {
|
||||
// Don't render anything, to avoid in particular sending useless requests.
|
||||
return null;
|
||||
}
|
||||
|
||||
const actions = [
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={true}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>What's new in GDevelop?</Trans>}
|
||||
actions={actions}
|
||||
open={open}
|
||||
onRequestClose={onClose}
|
||||
cannotBeDismissed={false}
|
||||
>
|
||||
<Text>
|
||||
<Trans>
|
||||
GDevelop was upgraded to a new version! Check out the changes.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Changelog
|
||||
onUpdated={() => {
|
||||
this.forceUpdate(); /*Force update to ensure dialog is properly positionned*/
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
const ChangelogDialog = ({ open, onClose }: Props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
if (!open) {
|
||||
// Don't render anything, to avoid in particular sending useless requests.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const actions = [
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={true}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>What's new in GDevelop?</Trans>}
|
||||
actions={actions}
|
||||
open={open}
|
||||
onRequestClose={onClose}
|
||||
cannotBeDismissed={false}
|
||||
>
|
||||
<Text>
|
||||
<Trans>
|
||||
GDevelop was upgraded to a new version! Check out the changes.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Changelog
|
||||
onUpdated={forceUpdate} // Force update to ensure dialog is properly positionned
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangelogDialog;
|
||||
|
@@ -7,32 +7,11 @@ type ContainerProps = {|
|
||||
defaultOpen: boolean,
|
||||
|};
|
||||
|
||||
type ContainerState = {|
|
||||
open: boolean,
|
||||
|};
|
||||
const ChangelogDialogContainer = ({ defaultOpen }: ContainerProps) => {
|
||||
const [open, setOpen] = React.useState(defaultOpen);
|
||||
|
||||
class ChangelogDialogContainer extends React.Component<
|
||||
ContainerProps,
|
||||
ContainerState
|
||||
> {
|
||||
state = {
|
||||
open: this.props.defaultOpen,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open } = this.state;
|
||||
return (
|
||||
<ChangelogDialog
|
||||
open={open}
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
open: false,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <ChangelogDialog open={open} onClose={() => setOpen(false)} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* The container showing the ChangelogDialog only if a a new version
|
||||
|
@@ -7,11 +7,6 @@ import {
|
||||
import ChangelogRenderer from './ChangelogRenderer';
|
||||
import { getIDEVersion } from '../../Version';
|
||||
|
||||
type State = {|
|
||||
releases: ?Array<Release>,
|
||||
error: ?Error,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
onUpdated?: () => void,
|
||||
|};
|
||||
@@ -19,45 +14,37 @@ type Props = {|
|
||||
/**
|
||||
* Load information about latest releases and display them.
|
||||
*/
|
||||
export default class Changelog extends React.Component<Props, State> {
|
||||
state = {
|
||||
releases: null,
|
||||
error: null,
|
||||
};
|
||||
const Changelog = ({ onUpdated }: Props) => {
|
||||
const [releases, setReleases] = React.useState<?Array<Release>>(null);
|
||||
const [error, setError] = React.useState<?Error>(null);
|
||||
|
||||
_onUpdated = () => {
|
||||
if (this.props.onUpdated) this.props.onUpdated();
|
||||
};
|
||||
React.useEffect(
|
||||
() => {
|
||||
getReleases()
|
||||
.then(releases => {
|
||||
setError(null);
|
||||
setReleases(releases);
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
})
|
||||
.catch((error: ?Error) => {
|
||||
setError(error);
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
});
|
||||
},
|
||||
[onUpdated]
|
||||
);
|
||||
|
||||
componentDidMount() {
|
||||
getReleases()
|
||||
.then(releases =>
|
||||
this.setState(
|
||||
{
|
||||
releases,
|
||||
error: null,
|
||||
},
|
||||
this._onUpdated
|
||||
)
|
||||
)
|
||||
.catch((error: ?Error) =>
|
||||
this.setState(
|
||||
{
|
||||
error,
|
||||
},
|
||||
this._onUpdated
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChangelogRenderer
|
||||
releases={releases}
|
||||
error={error}
|
||||
currentReleaseName={getIDEVersion()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { releases, error } = this.state;
|
||||
return (
|
||||
<ChangelogRenderer
|
||||
releases={releases}
|
||||
error={error}
|
||||
currentReleaseName={getIDEVersion()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Changelog;
|
||||
|
@@ -403,6 +403,14 @@ export const HomePage = React.memo<Props>(
|
||||
: null
|
||||
}
|
||||
displayItemTitles
|
||||
additionalAction={
|
||||
<RaisedButton
|
||||
label={<Trans>Play on Liluo.io</Trans>}
|
||||
onClick={() =>
|
||||
Window.openExternalURL('https://liluo.io')
|
||||
}
|
||||
/>
|
||||
}
|
||||
onBrowseAllClick={onOpenGamesShowcase}
|
||||
browseAllLabel={<Trans>Browse all</Trans>}
|
||||
error={
|
||||
|
@@ -31,6 +31,15 @@ export class ResourcesEditorContainer extends React.Component<RenderEditorContai
|
||||
// No updates to be done.
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: RenderEditorContainerProps) {
|
||||
if (
|
||||
this.editor &&
|
||||
this.props.isActive &&
|
||||
prevProps.isActive !== this.props.isActive
|
||||
)
|
||||
this.editor.refreshResourcesList();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { project } = this.props;
|
||||
if (!project) return null;
|
||||
|
149
newIDE/app/src/MainFrame/EditorTabs/DraggableEditorTabs.js
Normal file
149
newIDE/app/src/MainFrame/EditorTabs/DraggableEditorTabs.js
Normal file
@@ -0,0 +1,149 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
|
||||
import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndDropTarget';
|
||||
import { ScreenTypeMeasurer } from '../../UI/Reponsive/ScreenTypeMeasurer';
|
||||
import { ColumnDropIndicator } from './DropIndicator';
|
||||
import {
|
||||
type EditorTabsState,
|
||||
type EditorTab,
|
||||
getEditors,
|
||||
getCurrentTabIndex,
|
||||
} from './EditorTabsHandler';
|
||||
import {
|
||||
ClosableTabs,
|
||||
ClosableTab,
|
||||
type ClosableTabProps,
|
||||
} from '../../UI/ClosableTabs';
|
||||
|
||||
type DraggableClosableTabsProps = {|
|
||||
hideLabels?: boolean,
|
||||
editorTabs: EditorTabsState,
|
||||
onClickTab: (index: number) => void,
|
||||
onCloseTab: (editor: EditorTab) => void,
|
||||
onCloseOtherTabs: (editor: EditorTab) => void,
|
||||
onCloseAll: () => void,
|
||||
onTabActived: (editor: EditorTab) => void,
|
||||
onDropTab: (fromIndex: number, toHoveredIndex: number) => void,
|
||||
|};
|
||||
|
||||
export function DraggableClosableTabs({
|
||||
hideLabels,
|
||||
editorTabs,
|
||||
onClickTab,
|
||||
onCloseTab,
|
||||
onCloseOtherTabs,
|
||||
onCloseAll,
|
||||
onTabActived,
|
||||
onDropTab,
|
||||
}: DraggableClosableTabsProps) {
|
||||
let draggedTabIndex: ?number = null;
|
||||
|
||||
return (
|
||||
<ClosableTabs hideLabels={hideLabels}>
|
||||
{getEditors(editorTabs).map((editorTab, id) => {
|
||||
const isCurrentTab = getCurrentTabIndex(editorTabs) === id;
|
||||
return (
|
||||
<DraggableClosableTab
|
||||
index={id}
|
||||
label={editorTab.label}
|
||||
key={editorTab.key}
|
||||
id={`tab-${editorTab.key.replace(/\s/g, '-')}`}
|
||||
active={isCurrentTab}
|
||||
onClick={() => onClickTab(id)}
|
||||
onClose={() => onCloseTab(editorTab)}
|
||||
onCloseOthers={() => onCloseOtherTabs(editorTab)}
|
||||
onCloseAll={onCloseAll}
|
||||
onActivated={() => onTabActived(editorTab)}
|
||||
closable={editorTab.closable}
|
||||
onBeginDrag={() => {
|
||||
draggedTabIndex = id;
|
||||
return editorTab;
|
||||
}}
|
||||
onDrop={toHoveredIndex => {
|
||||
if (typeof draggedTabIndex === 'number') {
|
||||
onDropTab(draggedTabIndex, id);
|
||||
draggedTabIndex = null;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ClosableTabs>
|
||||
);
|
||||
}
|
||||
|
||||
type DraggableClosableTabProps = {|
|
||||
index: number,
|
||||
onBeginDrag: () => EditorTab,
|
||||
onDrop: (toIndex: number) => void,
|
||||
...ClosableTabProps,
|
||||
|};
|
||||
|
||||
export function DraggableClosableTab({
|
||||
index,
|
||||
id,
|
||||
active,
|
||||
onClose,
|
||||
onCloseOthers,
|
||||
onCloseAll,
|
||||
label,
|
||||
closable,
|
||||
onClick,
|
||||
onActivated,
|
||||
onBeginDrag,
|
||||
onDrop,
|
||||
}: DraggableClosableTabProps) {
|
||||
const DragSourceAndDropTarget = makeDragSourceAndDropTarget<EditorTab>(
|
||||
'draggable-closable-tab'
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenTypeMeasurer>
|
||||
{screenType => (
|
||||
<DragSourceAndDropTarget
|
||||
beginDrag={onBeginDrag}
|
||||
canDrag={() => {
|
||||
// On touchscreens, we disable drag and drop.
|
||||
if (screenType === 'touch') return false;
|
||||
// We want "Home" tab to stay on the left.
|
||||
return index !== 0;
|
||||
}}
|
||||
canDrop={() => true}
|
||||
drop={() => onDrop(index)}
|
||||
>
|
||||
{({ connectDragSource, connectDropTarget, isOver, canDrop }) => {
|
||||
// Add an extra div because connectDropTarget/connectDragSource can
|
||||
// only be used on native elements.
|
||||
const dropTarget = connectDropTarget(
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<ClosableTab
|
||||
id={id}
|
||||
active={active}
|
||||
onClose={onClose}
|
||||
onCloseOthers={onCloseOthers}
|
||||
onCloseAll={onCloseAll}
|
||||
label={label}
|
||||
closable={closable}
|
||||
onClick={onClick}
|
||||
onActivated={onActivated}
|
||||
key={id}
|
||||
/>
|
||||
{isOver && <ColumnDropIndicator />}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!dropTarget) return null;
|
||||
|
||||
return connectDragSource(dropTarget);
|
||||
}}
|
||||
</DragSourceAndDropTarget>
|
||||
)}
|
||||
</ScreenTypeMeasurer>
|
||||
);
|
||||
}
|
27
newIDE/app/src/MainFrame/EditorTabs/DropIndicator.js
Normal file
27
newIDE/app/src/MainFrame/EditorTabs/DropIndicator.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
|
||||
|
||||
const styles = {
|
||||
columnDropIndicator: {
|
||||
borderRight: '1px solid',
|
||||
borderLeft: '1px solid',
|
||||
width: 7,
|
||||
marginLeft: '-1px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
};
|
||||
|
||||
export function ColumnDropIndicator() {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...styles.columnDropIndicator,
|
||||
backgroundColor: gdevelopTheme.closableTabs.selectedBackgroundColor,
|
||||
borderColor: gdevelopTheme.closableTabs.backgroundColor,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import { EventsEditorContainer } from './EditorContainers/EventsEditorContainer';
|
||||
import { DebuggerEditorContainer } from './EditorContainers/DebuggerEditorContainer';
|
||||
import { EventsFunctionsExtensionEditorContainer } from './EditorContainers/EventsFunctionsExtensionEditorContainer';
|
||||
import { ExternalEventsEditorContainer } from './EditorContainers/ExternalEventsEditorContainer';
|
||||
import { ExternalLayoutEditorContainer } from './EditorContainers/ExternalLayoutEditorContainer';
|
||||
import { ResourcesEditorContainer } from './EditorContainers/ResourcesEditorContainer';
|
||||
import { SceneEditorContainer } from './EditorContainers/SceneEditorContainer';
|
||||
import { EventsEditorContainer } from '../EditorContainers/EventsEditorContainer';
|
||||
import { DebuggerEditorContainer } from '../EditorContainers/DebuggerEditorContainer';
|
||||
import { EventsFunctionsExtensionEditorContainer } from '../EditorContainers/EventsFunctionsExtensionEditorContainer';
|
||||
import { ExternalEventsEditorContainer } from '../EditorContainers/ExternalEventsEditorContainer';
|
||||
import { ExternalLayoutEditorContainer } from '../EditorContainers/ExternalLayoutEditorContainer';
|
||||
import { ResourcesEditorContainer } from '../EditorContainers/ResourcesEditorContainer';
|
||||
import { SceneEditorContainer } from '../EditorContainers/SceneEditorContainer';
|
||||
import {
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type EditorContainerExtraProps,
|
||||
} from './EditorContainers/BaseEditor';
|
||||
} from '../EditorContainers/BaseEditor';
|
||||
|
||||
// Supported editors
|
||||
type EditorRef =
|
||||
@@ -289,3 +289,42 @@ export const getEventsFunctionsExtensionEditor = (
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const moveTabToTheRightOfHoveredTab = (
|
||||
editorTabsState: EditorTabsState,
|
||||
movingTabIndex: number,
|
||||
hoveredTabIndex: number
|
||||
): EditorTabsState => {
|
||||
// If the tab is dragged backward, we want it to be placed on the right
|
||||
// of the hovered tab so as to match the position of the drop indicator.
|
||||
const destinationIndex =
|
||||
movingTabIndex > hoveredTabIndex ? hoveredTabIndex + 1 : hoveredTabIndex;
|
||||
|
||||
return moveTabToPosition(editorTabsState, movingTabIndex, destinationIndex);
|
||||
};
|
||||
|
||||
export const moveTabToPosition = (
|
||||
editorTabsState: EditorTabsState,
|
||||
fromIndex: number,
|
||||
toIndex: number
|
||||
): EditorTabsState => {
|
||||
const currentEditorTabs = [...getEditors(editorTabsState)];
|
||||
const movingTab = currentEditorTabs[fromIndex];
|
||||
currentEditorTabs.splice(fromIndex, 1);
|
||||
currentEditorTabs.splice(toIndex, 0, movingTab);
|
||||
|
||||
let currentTabIndex = getCurrentTabIndex(editorTabsState);
|
||||
let currentTabNewIndex = currentTabIndex;
|
||||
|
||||
const movingTabIsCurrentTab = fromIndex === currentTabIndex;
|
||||
const tabIsMovedFromLeftToRightOfCurrentTab =
|
||||
fromIndex < currentTabIndex && toIndex >= currentTabIndex;
|
||||
const tabIsMovedFromRightToLeftOfCurrentTab =
|
||||
fromIndex > currentTabIndex && toIndex <= currentTabIndex;
|
||||
|
||||
if (movingTabIsCurrentTab) currentTabNewIndex = toIndex;
|
||||
else if (tabIsMovedFromLeftToRightOfCurrentTab) currentTabNewIndex -= 1;
|
||||
else if (tabIsMovedFromRightToLeftOfCurrentTab) currentTabNewIndex += 1;
|
||||
|
||||
return { editors: currentEditorTabs, currentTab: currentTabNewIndex };
|
||||
};
|
146
newIDE/app/src/MainFrame/Onboarding/OnboardingDialog.js
Normal file
146
newIDE/app/src/MainFrame/Onboarding/OnboardingDialog.js
Normal file
@@ -0,0 +1,146 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import userflow from 'userflow.js';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import { getProgramOpeningCount } from '../../Utils/Analytics/LocalStats';
|
||||
import { isMobile } from '../../Utils/Platform';
|
||||
import Window from '../../Utils/Window';
|
||||
import { loadPreferencesFromLocalStorage } from '../Preferences/PreferencesProvider';
|
||||
import { getUserUUID } from '../../Utils/Analytics/UserUUID';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { Line } from '../../UI/Grid';
|
||||
import { MarkdownText } from '../../UI/MarkdownText';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
|
||||
const electron = optionalRequire('electron');
|
||||
const isDev = Window.isDev();
|
||||
let isUserflowInitialized = false;
|
||||
export let isUserflowRunning = false;
|
||||
|
||||
const styles = {
|
||||
imgContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
};
|
||||
|
||||
const onboardingText = `
|
||||
In 5 minutes, you will have:
|
||||
- Created a game
|
||||
- Learned the fundamentals of GDevelop
|
||||
|
||||
(🇬🇧 The tour is only available in English)
|
||||
We highly recommend it!
|
||||
`;
|
||||
|
||||
const OnboardingDialog = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const initializeUserflow = React.useCallback(() => {
|
||||
if (isUserflowInitialized) return;
|
||||
if (isDev) {
|
||||
userflow.init('ct_y5qogyfo6zbahjejcbo3dybnta');
|
||||
} else {
|
||||
userflow.init('ct_paaz6o2t2bhlrlyi7a3toojn7e');
|
||||
}
|
||||
userflow.on(
|
||||
// Undocumented legacy userflow event that is fired
|
||||
// "when a flow either becomes active or removed"
|
||||
// (tip given by a tech member of Userflow - it shouldn't be removed
|
||||
// in the near future given the fact that some of their users still use it).
|
||||
'flowvisibilitychange',
|
||||
isRunning => {
|
||||
isUserflowRunning = isRunning;
|
||||
}
|
||||
);
|
||||
isUserflowInitialized = true;
|
||||
}, []);
|
||||
|
||||
const startUserflow = React.useCallback(
|
||||
async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
initializeUserflow();
|
||||
const userPreferences = loadPreferencesFromLocalStorage();
|
||||
const appLanguage = userPreferences
|
||||
? userPreferences.language
|
||||
: undefined;
|
||||
await userflow.identify(getUserUUID(), { language: appLanguage });
|
||||
await userflow.start('b1611206-2fae-41ac-b08c-0f8ad72d8c39');
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
// Something wrong happened, allow the user to retry.
|
||||
console.error(
|
||||
'An error happened while starting the onboarding flow',
|
||||
e
|
||||
);
|
||||
showErrorBox({
|
||||
message: `There was an error while starting the onboarding flow. Verify your internet connection or try again later.`,
|
||||
rawError: e,
|
||||
errorId: 'onboarding-start-error',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[initializeUserflow]
|
||||
);
|
||||
|
||||
// Open modal if this is the first time the user opens the web app.
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (
|
||||
!electron &&
|
||||
getProgramOpeningCount() <= 1 &&
|
||||
!isMobile() &&
|
||||
!isDev // Uncomment this condition to see the onboarding in dev, as we are not tracking the opening count, we disable it.
|
||||
) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, 3000); // Timeout to avoid showing the dialog while the app is still loading.
|
||||
}, []);
|
||||
|
||||
const actions = [
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>No thanks, I'm good</Trans>}
|
||||
onClick={() => setOpen(false)}
|
||||
/>,
|
||||
<RaisedButton
|
||||
key="start"
|
||||
label={<Trans>Let's go!</Trans>}
|
||||
primary
|
||||
onClick={startUserflow}
|
||||
disabled={loading}
|
||||
/>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>Take a quick tour?</Trans>}
|
||||
actions={actions}
|
||||
open={open}
|
||||
onRequestClose={() => setOpen(false)}
|
||||
cannotBeDismissed={false}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Line alignItems="center" justifyContent="center" noMargin>
|
||||
<div style={styles.imgContainer}>
|
||||
<img alt="hero" src="res/hero.png" width={48} height={48} />
|
||||
</div>
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<MarkdownText source={onboardingText} />
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingDialog;
|
@@ -17,11 +17,8 @@ import CloseConfirmDialog from '../UI/CloseConfirmDialog';
|
||||
import ProfileDialog from '../Profile/ProfileDialog';
|
||||
import Window from '../Utils/Window';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import {
|
||||
ClosableTabs,
|
||||
ClosableTab,
|
||||
TabContentContainer,
|
||||
} from '../UI/ClosableTabs';
|
||||
import { TabContentContainer } from '../UI/ClosableTabs';
|
||||
import { DraggableClosableTabs } from './EditorTabs/DraggableEditorTabs';
|
||||
import {
|
||||
getEditorTabsInitialState,
|
||||
openEditorTab,
|
||||
@@ -42,7 +39,8 @@ import {
|
||||
type EditorTab,
|
||||
getEventsFunctionsExtensionEditor,
|
||||
notifyPreviewWillStart,
|
||||
} from './EditorTabsHandler';
|
||||
moveTabToTheRightOfHoveredTab,
|
||||
} from './EditorTabs/EditorTabsHandler';
|
||||
import { timePromise } from '../Utils/TimeFunction';
|
||||
import HelpFinder from '../HelpFinder';
|
||||
import { renderDebuggerEditorContainer } from './EditorContainers/DebuggerEditorContainer';
|
||||
@@ -133,6 +131,7 @@ import {
|
||||
TRIVIAL_FIRST_PREVIEW,
|
||||
} from '../Utils/GDevelopServices/Badge';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import OnboardingDialog from './Onboarding/OnboardingDialog';
|
||||
|
||||
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
|
||||
|
||||
@@ -1082,6 +1081,10 @@ const MainFrame = (props: Props) => {
|
||||
newName
|
||||
);
|
||||
eventsFunctionsExtension.setName(newName);
|
||||
eventsFunctionsExtensionsState.unloadProjectEventsFunctionsExtension(
|
||||
currentProject,
|
||||
oldName
|
||||
);
|
||||
eventsFunctionsExtensionsState.reloadProjectEventsFunctionsExtensions(
|
||||
currentProject
|
||||
);
|
||||
@@ -1848,11 +1851,11 @@ const MainFrame = (props: Props) => {
|
||||
...state,
|
||||
editorTabs: changeCurrentTab(state.editorTabs, value),
|
||||
})).then(state =>
|
||||
_onEditorTabActive(getCurrentTab(state.editorTabs), state)
|
||||
_onEditorTabActived(getCurrentTab(state.editorTabs), state)
|
||||
);
|
||||
};
|
||||
|
||||
const _onEditorTabActive = (
|
||||
const _onEditorTabActived = (
|
||||
editorTab: EditorTab,
|
||||
newState: State = state
|
||||
) => {
|
||||
@@ -1888,6 +1891,19 @@ const MainFrame = (props: Props) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const onDropEditorTab = (fromIndex: number, toHoveredIndex: number) => {
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
editorTabs: moveTabToTheRightOfHoveredTab(
|
||||
state.editorTabs,
|
||||
fromIndex,
|
||||
toHoveredIndex
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onChooseResource: ChooseResourceFunction = (
|
||||
options: ChooseResourceOptions
|
||||
) => {
|
||||
@@ -2134,25 +2150,18 @@ const MainFrame = (props: Props) => {
|
||||
}
|
||||
previewState={previewState}
|
||||
/>
|
||||
<ClosableTabs hideLabels={!!props.integratedEditor}>
|
||||
{getEditors(state.editorTabs).map((editorTab, id) => {
|
||||
const isCurrentTab = getCurrentTabIndex(state.editorTabs) === id;
|
||||
return (
|
||||
<ClosableTab
|
||||
label={editorTab.label}
|
||||
key={editorTab.key}
|
||||
id={`tab-${editorTab.key.replace(/\s/g, '-')}`}
|
||||
active={isCurrentTab}
|
||||
onClick={() => _onChangeEditorTab(id)}
|
||||
onClose={() => _onCloseEditorTab(editorTab)}
|
||||
onCloseOthers={() => _onCloseOtherEditorTabs(editorTab)}
|
||||
onCloseAll={_onCloseAllEditorTabs}
|
||||
onActivated={() => _onEditorTabActive(editorTab)}
|
||||
closable={editorTab.closable}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ClosableTabs>
|
||||
<DraggableClosableTabs
|
||||
hideLabels={!!props.integratedEditor}
|
||||
editorTabs={state.editorTabs}
|
||||
onClickTab={(id: number) => _onChangeEditorTab(id)}
|
||||
onCloseTab={(editorTab: EditorTab) => _onCloseEditorTab(editorTab)}
|
||||
onCloseOtherTabs={(editorTab: EditorTab) =>
|
||||
_onCloseOtherEditorTabs(editorTab)
|
||||
}
|
||||
onCloseAll={_onCloseAllEditorTabs}
|
||||
onTabActived={(editorTab: EditorTab) => _onEditorTabActived(editorTab)}
|
||||
onDropTab={onDropEditorTab}
|
||||
/>
|
||||
{getEditors(state.editorTabs).map((editorTab, id) => {
|
||||
const isCurrentTab = getCurrentTabIndex(state.editorTabs) === id;
|
||||
return (
|
||||
@@ -2388,6 +2397,7 @@ const MainFrame = (props: Props) => {
|
||||
hasUnsavedChanges={!!unsavedChanges && unsavedChanges.hasUnsavedChanges}
|
||||
/>
|
||||
<ChangelogDialogContainer />
|
||||
<OnboardingDialog />
|
||||
{state.gdjsDevelopmentWatcherEnabled &&
|
||||
renderGDJSDevelopmentWatcher &&
|
||||
renderGDJSDevelopmentWatcher()}
|
||||
|
@@ -14,10 +14,11 @@ import { mapFor } from '../../../Utils/MapFor';
|
||||
import SemiControlledTextField from '../../../UI/SemiControlledTextField';
|
||||
import Dialog from '../../../UI/Dialog';
|
||||
import HelpButton from '../../../UI/HelpButton';
|
||||
import EmptyMessage from '../../../UI/EmptyMessage';
|
||||
import MiniToolbar, { MiniToolbarText } from '../../../UI/MiniToolbar';
|
||||
import DragHandle from '../../../UI/DragHandle';
|
||||
import ContextMenu from '../../../UI/Menu/ContextMenu';
|
||||
import ContextMenu, {
|
||||
type ContextMenuInterface,
|
||||
} from '../../../UI/Menu/ContextMenu';
|
||||
import { showWarningBox } from '../../../UI/Messages/MessageBox';
|
||||
import ResourcesLoader from '../../../ResourcesLoader';
|
||||
import PointsEditor from './PointsEditor';
|
||||
@@ -38,6 +39,7 @@ import { ResponsiveLineStackLayout } from '../../../UI/Layout';
|
||||
import ScrollView from '../../../UI/ScrollView';
|
||||
import Checkbox from '../../../UI/Checkbox';
|
||||
import useForceUpdate from '../../../Utils/UseForceUpdate';
|
||||
import { EmptyPlaceholder } from '../../../UI/EmptyPlaceholder';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -210,7 +212,7 @@ class AnimationsListContainer extends React.Component<
|
||||
state = {
|
||||
selectedSprites: {},
|
||||
};
|
||||
spriteContextMenu: ?ContextMenu;
|
||||
spriteContextMenu: ?ContextMenuInterface;
|
||||
|
||||
onSortEnd = ({ oldIndex, newIndex }) => {
|
||||
this.props.spriteObject.moveAnimation(oldIndex, newIndex);
|
||||
@@ -309,63 +311,68 @@ class AnimationsListContainer extends React.Component<
|
||||
return (
|
||||
<Column noMargin expand useFullHeight>
|
||||
{this.props.spriteObject.getAnimationsCount() === 0 ? (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This object has no animations containing images. Start by adding
|
||||
an animation.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
) : (
|
||||
<SortableAnimationsList
|
||||
spriteObject={this.props.spriteObject}
|
||||
objectName={this.props.objectName}
|
||||
helperClass="sortable-helper"
|
||||
project={this.props.project}
|
||||
onSortEnd={this.onSortEnd}
|
||||
onChangeAnimationName={this.changeAnimationName}
|
||||
onRemoveAnimation={this.removeAnimation}
|
||||
onReplaceDirection={this.replaceDirection}
|
||||
onSpriteContextMenu={this.openSpriteContextMenu}
|
||||
selectedSprites={this.state.selectedSprites}
|
||||
onSelectSprite={this.selectSprite}
|
||||
resourcesLoader={this.props.resourcesLoader}
|
||||
resourceSources={this.props.resourceSources}
|
||||
resourceExternalEditors={this.props.resourceExternalEditors}
|
||||
onChooseResource={this.props.onChooseResource}
|
||||
useDragHandle
|
||||
lockAxis="y"
|
||||
axis="y"
|
||||
/>
|
||||
)}
|
||||
<Column>
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
noColumnMargin
|
||||
>
|
||||
{this.props.extraBottomTools}
|
||||
<RaisedButton
|
||||
label={<Trans>Add an animation</Trans>}
|
||||
primary
|
||||
onClick={this.addAnimation}
|
||||
icon={<Add />}
|
||||
<Column noMargin expand justifyContent="center">
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first animation</Trans>}
|
||||
description={<Trans>Animations are a sequence of images.</Trans>}
|
||||
actionLabel={<Trans>Add an animation</Trans>}
|
||||
helpPagePath="/objects/sprite"
|
||||
onAdd={this.addAnimation}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
<ContextMenu
|
||||
ref={spriteContextMenu =>
|
||||
(this.spriteContextMenu = spriteContextMenu)
|
||||
}
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Delete selection`),
|
||||
click: () => this.deleteSelection(),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Duplicate selection`),
|
||||
click: () => this.duplicateSelection(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Column>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<SortableAnimationsList
|
||||
spriteObject={this.props.spriteObject}
|
||||
objectName={this.props.objectName}
|
||||
helperClass="sortable-helper"
|
||||
project={this.props.project}
|
||||
onSortEnd={this.onSortEnd}
|
||||
onChangeAnimationName={this.changeAnimationName}
|
||||
onRemoveAnimation={this.removeAnimation}
|
||||
onReplaceDirection={this.replaceDirection}
|
||||
onSpriteContextMenu={this.openSpriteContextMenu}
|
||||
selectedSprites={this.state.selectedSprites}
|
||||
onSelectSprite={this.selectSprite}
|
||||
resourcesLoader={this.props.resourcesLoader}
|
||||
resourceSources={this.props.resourceSources}
|
||||
resourceExternalEditors={this.props.resourceExternalEditors}
|
||||
onChooseResource={this.props.onChooseResource}
|
||||
useDragHandle
|
||||
lockAxis="y"
|
||||
axis="y"
|
||||
/>
|
||||
<Column>
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
noColumnMargin
|
||||
>
|
||||
{this.props.extraBottomTools}
|
||||
<RaisedButton
|
||||
label={<Trans>Add an animation</Trans>}
|
||||
primary
|
||||
onClick={this.addAnimation}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
<ContextMenu
|
||||
ref={spriteContextMenu =>
|
||||
(this.spriteContextMenu = spriteContextMenu)
|
||||
}
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Delete selection`),
|
||||
click: () => this.deleteSelection(),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Duplicate selection`),
|
||||
click: () => this.duplicateSelection(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
@@ -27,6 +27,12 @@ import VariablesList from '../VariablesList/index';
|
||||
import { sendBehaviorsEditorShown } from '../Utils/Analytics/EventSender';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
export type ObjectEditorTab =
|
||||
| 'properties'
|
||||
| 'behaviors'
|
||||
| 'variables'
|
||||
| 'effects';
|
||||
|
||||
type Props = {|
|
||||
open: boolean,
|
||||
object: ?gdObject,
|
||||
@@ -46,7 +52,7 @@ type Props = {|
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
unsavedChanges?: UnsavedChanges,
|
||||
onUpdateBehaviorsSharedData: () => void,
|
||||
initialTab: ?string,
|
||||
initialTab: ?ObjectEditorTab,
|
||||
|
||||
// Preview:
|
||||
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|
||||
@@ -61,7 +67,7 @@ type InnerDialogProps = {|
|
||||
|};
|
||||
|
||||
const InnerDialog = (props: InnerDialogProps) => {
|
||||
const [currentTab, setCurrentTab] = React.useState(
|
||||
const [currentTab, setCurrentTab] = React.useState<ObjectEditorTab>(
|
||||
props.initialTab || 'properties'
|
||||
);
|
||||
const [newObjectName, setNewObjectName] = React.useState(props.objectName);
|
||||
@@ -225,17 +231,10 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
{currentTab === 'variables' && (
|
||||
<VariablesList
|
||||
variablesContainer={props.object.getVariables()}
|
||||
emptyExplanationMessage={
|
||||
emptyPlaceholderTitle={<Trans>Add your first object variable</Trans>}
|
||||
emptyPlaceholderDescription={
|
||||
<Trans>
|
||||
When you add variables to an object, any instance of the object
|
||||
put on the scene or created during the game will have these
|
||||
variables attached to it.
|
||||
</Trans>
|
||||
}
|
||||
emptyExplanationSecondMessage={
|
||||
<Trans>
|
||||
For example, you can have a variable called Life representing the
|
||||
health of the object.
|
||||
These variables hold additional information on an object.
|
||||
</Trans>
|
||||
}
|
||||
helpPagePath={'/all-features/variables/object-variables'}
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
filterObjectsList,
|
||||
isSameObjectWithContext,
|
||||
} from './EnumerateObjects';
|
||||
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
|
||||
import type {
|
||||
ObjectWithContextList,
|
||||
ObjectWithContext,
|
||||
@@ -121,11 +122,12 @@ type Props = {|
|
||||
getAllObjectTags: () => Tags,
|
||||
onChangeSelectedObjectTags: SelectedTags => void,
|
||||
|
||||
onEditObject: (object: gdObject, initialTab: ?string) => void,
|
||||
onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
|
||||
onObjectCreated: gdObject => void,
|
||||
onObjectSelected: string => void,
|
||||
onObjectPasted?: gdObject => void,
|
||||
canRenameObject: (newName: string) => boolean,
|
||||
onAddObjectInstance: (objectName: string) => void,
|
||||
|
||||
getThumbnail: (project: gdProject, object: Object) => string,
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
@@ -473,6 +475,24 @@ export default class ObjectsList extends React.Component<Props, State> {
|
||||
object.getType()
|
||||
);
|
||||
return [
|
||||
{
|
||||
label: i18n._(t`Copy`),
|
||||
click: () => this._copyObject(objectWithContext),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Cut`),
|
||||
click: () => this._cutObject(i18n, objectWithContext),
|
||||
},
|
||||
{
|
||||
label: getPasteLabel(objectWithContext.global),
|
||||
enabled: Clipboard.has(CLIPBOARD_KIND),
|
||||
click: () => this._paste(objectWithContext),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Duplicate`),
|
||||
click: () => this._duplicateObject(objectWithContext),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Edit object`),
|
||||
click: () => this.props.onEditObject(object),
|
||||
@@ -491,6 +511,15 @@ export default class ObjectsList extends React.Component<Props, State> {
|
||||
enabled: !objectMetadata.isUnsupportedBaseObjectCapability('effect'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Rename`),
|
||||
click: () => this._editName(objectWithContext),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Set as a global object`),
|
||||
enabled: !isObjectWithContextGlobal(objectWithContext),
|
||||
click: () => this._setAsGlobalObject(objectWithContext),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Tags`),
|
||||
submenu: buildTagsMenuTemplate({
|
||||
@@ -504,41 +533,19 @@ export default class ObjectsList extends React.Component<Props, State> {
|
||||
onEditTags: () => this._openEditTagDialog(object),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Rename`),
|
||||
click: () => this._editName(objectWithContext),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Set as a global object`),
|
||||
enabled: !isObjectWithContextGlobal(objectWithContext),
|
||||
click: () => this._setAsGlobalObject(objectWithContext),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
click: () => this._deleteObject(i18n, objectWithContext),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Add a new object...`),
|
||||
click: () => this.onAddNewObject(),
|
||||
label: i18n._(t`Add instance to the scene`),
|
||||
click: () => this.props.onAddObjectInstance(object.getName()),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Copy`),
|
||||
click: () => this._copyObject(objectWithContext),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Cut`),
|
||||
click: () => this._cutObject(i18n, objectWithContext),
|
||||
},
|
||||
{
|
||||
label: getPasteLabel(objectWithContext.global),
|
||||
enabled: Clipboard.has(CLIPBOARD_KIND),
|
||||
click: () => this._paste(objectWithContext),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Duplicate`),
|
||||
click: () => this._duplicateObject(objectWithContext),
|
||||
label: i18n._(t`Add a new object...`),
|
||||
click: () => this.onAddNewObject(),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
@@ -32,6 +32,7 @@ type Props = {|
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
thumbnailResourceName: string,
|
||||
desktopIconResourceNames: Array<string>,
|
||||
androidIconResourceNames: Array<string>,
|
||||
iosIconResourceNames: Array<string>,
|
||||
@@ -72,6 +73,9 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
|
||||
_loadFrom(project: gdProject): State {
|
||||
return {
|
||||
thumbnailResourceName: project
|
||||
.getPlatformSpecificAssets()
|
||||
.get('liluo', `thumbnail`),
|
||||
desktopIconResourceNames: desktopSizes.map(size =>
|
||||
project.getPlatformSpecificAssets().get('desktop', `icon-${size}`)
|
||||
),
|
||||
@@ -202,11 +206,16 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
onApply = () => {
|
||||
const { project } = this.props;
|
||||
const {
|
||||
thumbnailResourceName,
|
||||
desktopIconResourceNames,
|
||||
androidIconResourceNames,
|
||||
iosIconResourceNames,
|
||||
} = this.state;
|
||||
|
||||
project
|
||||
.getPlatformSpecificAssets()
|
||||
.set('liluo', `thumbnail`, thumbnailResourceName);
|
||||
|
||||
desktopSizes.forEach((size, index) => {
|
||||
project
|
||||
.getPlatformSpecificAssets()
|
||||
@@ -249,6 +258,7 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
resourceExternalEditors,
|
||||
} = this.props;
|
||||
const {
|
||||
thumbnailResourceName,
|
||||
desktopIconResourceNames,
|
||||
androidIconResourceNames,
|
||||
iosIconResourceNames,
|
||||
@@ -264,6 +274,23 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
onRequestClose={this.props.onClose}
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Text>
|
||||
<Trans>Liluo.io thumbnail:</Trans>
|
||||
</Text>
|
||||
<ResourceSelectorWithThumbnail
|
||||
floatingLabelText={`Liluo.io thumbnail (1920x1080 px)`}
|
||||
project={project}
|
||||
resourceSources={resourceSources}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
resourceKind="image"
|
||||
resourceName={thumbnailResourceName}
|
||||
onChange={resourceName => {
|
||||
this.setState({
|
||||
thumbnailResourceName: resourceName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Line justifyContent="center">
|
||||
{isResizeSupported() ? (
|
||||
<RaisedButton
|
||||
|
@@ -20,7 +20,12 @@ const findEmptyPath = (basePath: string) => {
|
||||
};
|
||||
|
||||
export const findEmptyPathInDefaultFolder = (electronApp: any): string => {
|
||||
return findEmptyPath(
|
||||
path.join(electronApp.getPath('documents'), 'GDevelop projects')
|
||||
);
|
||||
let documentsPath = '';
|
||||
try {
|
||||
documentsPath = electronApp.getPath('documents');
|
||||
} catch (ex) {
|
||||
// A user may not have the Documents folder defined on Windows.
|
||||
documentsPath = electronApp.getPath('home');
|
||||
}
|
||||
return findEmptyPath(path.join(documentsPath, 'GDevelop projects'));
|
||||
};
|
||||
|
@@ -8,15 +8,25 @@ import Checkbox from '../UI/Checkbox';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import { t } from '@lingui/macro';
|
||||
import SemiControlledMultiAutoComplete from '../UI/SemiControlledMultiAutoComplete';
|
||||
import {
|
||||
allGameCategories,
|
||||
getCategoryName,
|
||||
} from '../Utils/GDevelopServices/Game';
|
||||
import { I18n } from '@lingui/react';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
setName: string => void,
|
||||
name: string,
|
||||
setCategories?: (string[]) => void,
|
||||
categories?: string[],
|
||||
setDescription: string => void,
|
||||
description: ?string,
|
||||
setAuthorIds: (string[]) => void,
|
||||
authorIds: string[],
|
||||
setOwnerIds?: (string[]) => void,
|
||||
ownerIds?: string[],
|
||||
setPlayableWithKeyboard?: boolean => void,
|
||||
playWithKeyboard?: boolean,
|
||||
setPlayableWithGamepad?: boolean => void,
|
||||
@@ -31,10 +41,14 @@ function PublicGameProperties({
|
||||
project,
|
||||
setName,
|
||||
name,
|
||||
categories,
|
||||
setCategories,
|
||||
setDescription,
|
||||
description,
|
||||
setAuthorIds,
|
||||
authorIds,
|
||||
setOwnerIds,
|
||||
ownerIds,
|
||||
setPlayableWithKeyboard,
|
||||
playWithKeyboard,
|
||||
setPlayableWithGamepad,
|
||||
@@ -44,72 +58,129 @@ function PublicGameProperties({
|
||||
setOrientation,
|
||||
orientation,
|
||||
}: Props) {
|
||||
const [categoryInput, setCategoryInput] = React.useState('');
|
||||
|
||||
return (
|
||||
<ColumnStackLayout noMargin>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Game name</Trans>}
|
||||
fullWidth
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
autoFocus
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Game description</Trans>}
|
||||
fullWidth
|
||||
type="text"
|
||||
value={description || ''}
|
||||
onChange={setDescription}
|
||||
autoFocus
|
||||
multiline
|
||||
rows={5}
|
||||
/>
|
||||
<UsersAutocomplete
|
||||
userIds={authorIds}
|
||||
onChange={setAuthorIds}
|
||||
floatingLabelText={<Trans>Authors</Trans>}
|
||||
helperText={
|
||||
<Trans>
|
||||
Select the usernames of the authors of this project. They will be
|
||||
displayed in the selected order, if you publish this game as an
|
||||
example or in the community.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Device orientation (for mobile)</Trans>}
|
||||
value={orientation}
|
||||
onChange={(e, i, value: string) => setOrientation(value)}
|
||||
>
|
||||
<SelectOption value="default" primaryText={t`Platform default`} />
|
||||
<SelectOption value="landscape" primaryText={t`Landscape`} />
|
||||
<SelectOption value="portrait" primaryText={t`Portrait`} />
|
||||
</SelectField>
|
||||
{// This view is used for public game properties as well as project properties.
|
||||
// The following properties are not shown in project properties.
|
||||
setPlayableWithKeyboard &&
|
||||
setPlayableWithGamepad &&
|
||||
setPlayableWithMobile && (
|
||||
<React.Fragment>
|
||||
<Checkbox
|
||||
label={<Trans>Playable with a keyboard</Trans>}
|
||||
checked={!!playWithKeyboard}
|
||||
onCheck={(e, checked) => setPlayableWithKeyboard(checked)}
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<ColumnStackLayout noMargin>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Game name</Trans>}
|
||||
fullWidth
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
autoFocus
|
||||
/>
|
||||
{setCategories && (
|
||||
<SemiControlledMultiAutoComplete
|
||||
hintText={t`Select a genre`}
|
||||
floatingLabelText={<Trans>Genres</Trans>}
|
||||
helperText={
|
||||
<Trans>
|
||||
Select up to 4 genres, the first one will define the game's
|
||||
main genre
|
||||
</Trans>
|
||||
}
|
||||
value={
|
||||
categories
|
||||
? categories.map(category => ({
|
||||
value: category,
|
||||
text: getCategoryName(category, i18n),
|
||||
}))
|
||||
: []
|
||||
}
|
||||
onChange={(event, values) => {
|
||||
setCategories(
|
||||
values ? values.map(category => category.value) : []
|
||||
);
|
||||
}}
|
||||
inputValue={categoryInput}
|
||||
onInputChange={(event, value) => {
|
||||
setCategoryInput(value);
|
||||
}}
|
||||
dataSource={allGameCategories.map(category => ({
|
||||
value: category,
|
||||
text: getCategoryName(category, i18n),
|
||||
}))}
|
||||
fullWidth
|
||||
optionsLimit={4}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Playable with a gamepad</Trans>}
|
||||
checked={!!playWithGamepad}
|
||||
onCheck={(e, checked) => setPlayableWithGamepad(checked)}
|
||||
)}
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Game description</Trans>}
|
||||
fullWidth
|
||||
type="text"
|
||||
value={description || ''}
|
||||
onChange={setDescription}
|
||||
autoFocus
|
||||
multiline
|
||||
rows={5}
|
||||
/>
|
||||
<UsersAutocomplete
|
||||
userIds={authorIds}
|
||||
onChange={setAuthorIds}
|
||||
floatingLabelText={<Trans>Authors</Trans>}
|
||||
helperText={
|
||||
<Trans>
|
||||
Select the usernames of the authors of this project. They will
|
||||
be displayed in the selected order, if you publish this game as
|
||||
an example or in the community.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{// This view is used for public game properties as well as project properties.
|
||||
// This property is not shown in project properties.
|
||||
setOwnerIds && (
|
||||
<UsersAutocomplete
|
||||
userIds={ownerIds || []}
|
||||
onChange={setOwnerIds}
|
||||
floatingLabelText={<Trans>Owners</Trans>}
|
||||
helperText={
|
||||
<Trans>
|
||||
Select the usernames of the owners of this project to let them
|
||||
manage this game builds. Be aware that owners can revoke your
|
||||
ownership.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Playable on mobile</Trans>}
|
||||
checked={!!playWithMobile}
|
||||
onCheck={(e, checked) => setPlayableWithMobile(checked)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Device orientation (for mobile)</Trans>}
|
||||
value={orientation}
|
||||
onChange={(e, i, value: string) => setOrientation(value)}
|
||||
>
|
||||
<SelectOption value="default" primaryText={t`Platform default`} />
|
||||
<SelectOption value="landscape" primaryText={t`Landscape`} />
|
||||
<SelectOption value="portrait" primaryText={t`Portrait`} />
|
||||
</SelectField>
|
||||
{// This view is used for public game properties as well as project properties.
|
||||
// The following properties are not shown in project properties.
|
||||
setPlayableWithKeyboard &&
|
||||
setPlayableWithGamepad &&
|
||||
setPlayableWithMobile && (
|
||||
<React.Fragment>
|
||||
<Checkbox
|
||||
label={<Trans>Playable with a keyboard</Trans>}
|
||||
checked={!!playWithKeyboard}
|
||||
onCheck={(e, checked) => setPlayableWithKeyboard(checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Playable with a gamepad</Trans>}
|
||||
checked={!!playWithGamepad}
|
||||
onCheck={(e, checked) => setPlayableWithGamepad(checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Playable on mobile</Trans>}
|
||||
checked={!!playWithMobile}
|
||||
onCheck={(e, checked) => setPlayableWithMobile(checked)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -12,11 +12,19 @@ import FlatButton from '../UI/FlatButton';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import { type PublicGame } from '../Utils/GDevelopServices/Game';
|
||||
|
||||
/**
|
||||
* Changes that are not stored in the Project.
|
||||
*/
|
||||
export type PartialGameChange = {|
|
||||
ownerIds: Array<string>,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Public game properties that are shared with the project file ones.
|
||||
*/
|
||||
type PublicProjectProperties = {|
|
||||
name: string,
|
||||
categories: string[],
|
||||
description: string,
|
||||
authorIds: string[],
|
||||
playWithKeyboard: boolean,
|
||||
@@ -30,8 +38,11 @@ function applyPublicPropertiesToProject(
|
||||
newProperties: PublicProjectProperties
|
||||
) {
|
||||
const t = str => str; //TODO
|
||||
const { name, authorIds, description } = newProperties;
|
||||
const { name, authorIds, description, categories } = newProperties;
|
||||
project.setName(name);
|
||||
const projectCategories = project.getCategories();
|
||||
projectCategories.clear();
|
||||
categories.forEach(category => projectCategories.push_back(category));
|
||||
project.setDescription(description);
|
||||
const projectAuthorIds = project.getAuthorIds();
|
||||
projectAuthorIds.clear();
|
||||
@@ -46,37 +57,38 @@ function applyPublicPropertiesToProject(
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
game: PublicGame,
|
||||
publicGame: PublicGame,
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onApply: () => void,
|
||||
onApply: (partialGameChange: PartialGameChange) => void,
|
||||
|};
|
||||
|
||||
const PublicGamePropertiesDialog = ({
|
||||
export const PublicGamePropertiesDialog = ({
|
||||
project,
|
||||
game,
|
||||
publicGame,
|
||||
open,
|
||||
onClose,
|
||||
onApply,
|
||||
}: Props) => {
|
||||
const publicGameAuthorIds = game.authors
|
||||
.map(author => (author ? author.id : null))
|
||||
.filter(Boolean);
|
||||
const [name, setName] = React.useState(game.gameName);
|
||||
const [description, setDescription] = React.useState(game.description);
|
||||
const publicGameAuthorIds = publicGame.authors.map(author => author.id);
|
||||
const publicGameOwnerIds = publicGame.owners.map(owner => owner.id);
|
||||
const [name, setName] = React.useState(publicGame.gameName);
|
||||
const [categories, setCategories] = React.useState(publicGame.categories);
|
||||
const [description, setDescription] = React.useState(publicGame.description);
|
||||
const [authorIds, setAuthorIds] = React.useState<string[]>(
|
||||
publicGameAuthorIds
|
||||
);
|
||||
const [ownerIds, setOwnerIds] = React.useState<string[]>(publicGameOwnerIds);
|
||||
const [playWithKeyboard, setPlayableWithKeyboard] = React.useState(
|
||||
game.playWithKeyboard
|
||||
publicGame.playWithKeyboard
|
||||
);
|
||||
const [playWithGamepad, setPlayableWithGamepad] = React.useState(
|
||||
game.playWithGamepad
|
||||
publicGame.playWithGamepad
|
||||
);
|
||||
const [playWithMobile, setPlayableWithMobile] = React.useState(
|
||||
game.playWithMobile
|
||||
publicGame.playWithMobile
|
||||
);
|
||||
const [orientation, setOrientation] = React.useState(game.orientation);
|
||||
const [orientation, setOrientation] = React.useState(publicGame.orientation);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
@@ -84,6 +96,7 @@ const PublicGamePropertiesDialog = ({
|
||||
if (
|
||||
applyPublicPropertiesToProject(project, {
|
||||
name,
|
||||
categories: categories || [],
|
||||
description: description || '',
|
||||
authorIds,
|
||||
playWithKeyboard: !!playWithKeyboard,
|
||||
@@ -91,8 +104,9 @@ const PublicGamePropertiesDialog = ({
|
||||
playWithMobile: !!playWithMobile,
|
||||
orientation: orientation || 'default',
|
||||
})
|
||||
)
|
||||
onApply();
|
||||
) {
|
||||
onApply({ ownerIds });
|
||||
}
|
||||
};
|
||||
|
||||
const actions = [
|
||||
@@ -121,11 +135,15 @@ const PublicGamePropertiesDialog = ({
|
||||
<PublicGameProperties
|
||||
name={name}
|
||||
setName={setName}
|
||||
categories={categories}
|
||||
setCategories={setCategories}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
project={project}
|
||||
authorIds={authorIds}
|
||||
setAuthorIds={setAuthorIds}
|
||||
ownerIds={ownerIds}
|
||||
setOwnerIds={setOwnerIds}
|
||||
setPlayableWithKeyboard={setPlayableWithKeyboard}
|
||||
playWithKeyboard={playWithKeyboard}
|
||||
setPlayableWithGamepad={setPlayableWithGamepad}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user