Compare commits

..

37 Commits

Author SHA1 Message Date
Clément Pasteau
a2f0b3384e Bump newIDE version to 5.0.129 2022-03-17 14:57:32 +01:00
github-actions[bot]
432ffebf80 Update translations [skip ci] (#3695)
Co-authored-by: Bouh <Bouh@users.noreply.github.com>
2022-03-17 14:40:24 +01:00
Aurélien Vivet
165470d9c1 Add D as shortcut to toggle disabled event, also added in right click context menu in the event sheet (#3751) 2022-03-17 11:35:38 +01:00
D8H
53eab0ed62 Fix the "Is moving" condition of platformer characters by removing the 1 pixel per frame detection limit (#3730)
This can be important for games with low resolution.
2022-03-16 19:09:14 +01:00
Florian Rival
649a36ac30 Fix text input wrongly staying visible even if hidden or layer visibility turned off (#3755) 2022-03-16 18:56:11 +01:00
Aurélien Vivet
a3afd983ba Add OG meta tags to the web-app (#3756)
Don't show in changelog
2022-03-16 11:01:24 +01:00
D8H
f8c0417210 Allow game owners to share their ownership with other users (#3744) 2022-03-16 10:15:06 +01:00
D8H
8f5fe159d7 Fix rendering so that sprites no longer bounce by one pixel when the camera is moving and pixel perfect is activated (#3754) 2022-03-15 22:26:57 +01:00
D8H
3daebaeb05 Add an action to ensure the camera is inside specified boundaries (#3703) 2022-03-13 12:55:09 +00:00
Florian Rival
1ba1e8a2b7 Update name of project manager "Icons and thumbnail" item
Don't show in changelog
2022-03-11 23:11:18 +00:00
Fannie Yan
36679eb76b Add size actions and conditions for sprite and tiled sprite objects (#3748)
* Add condition on height and width for sprite objects
* Add action to set the size of sprite object
* Add action to set the size of tiled sprite object
2022-03-11 16:53:26 +01:00
Aurélien Vivet
c8132ba864 Fix typo (#3747)
Don't show in changelog
2022-03-11 12:49:52 +00:00
Fannie Yan
141a3d2b61 Allow users to reorder editor tabs (#3745)
Allow users to reorder editor tabs by dragging them (not available on touchscreens)
2022-03-11 10:55:42 +01:00
Florian Rival
dbe0343c67 Fix extensions scene lifecycle events launched more than once after a hot reloading (#3742) 2022-03-10 12:42:09 +00:00
Aurélien Vivet
0983ac59a8 Allow extensions to use the Audio category (#3741) 2022-03-10 12:38:10 +00:00
MyNameIsRinax
9710360180 Fix typo in firebase action
Do not show in changelog
2022-03-10 09:31:57 +01:00
D8H
43af1f34df Make Liluo.io thumbnail configurable in the icons dialog (#3732) 2022-03-09 15:16:07 +01:00
Fannie Yan
fd193e89fc Add possibility to use custom collision masks for draggable behavior (#3738)
* Added a toggle in draggable behavior parameters so that users can chose to use custom collision mask or not
2022-03-09 13:39:57 +01:00
Clément Pasteau
19dcaacafb Improve wording for builds dialog when game is not registered (#3737)
do not show in changelog
2022-03-08 11:18:38 +01:00
Fannie Yan
90721c4059 Add possibility to replace text in text events (Comments and Groups) (#3735)
* Add possibility to replace text in text events (Comments and Group names) with search and replace
2022-03-08 09:35:32 +01:00
Florian Rival
6c118eaa53 Make clearer that the publishing is an export (#3733)
Don't show in changelog
2022-03-07 15:27:58 +00:00
D8H
2b00c90356 Fix some missing translations in the Pathfinding behavior (#3728) 2022-03-07 08:57:25 +00:00
Florian Rival
efe37cc3bf Fix warning
Don't show in changelog
2022-03-04 15:52:14 +00:00
Fannie Yan
bcedbdc0c3 Fix resource list not updated when adding some with tab opened (#3723)
Refresh resource list in Resource tab when it becomes active
2022-03-04 15:50:08 +01:00
Clément Pasteau
f63a16161f Add Liluo Link on homepage (#3720) 2022-03-04 13:35:09 +01:00
Florian Rival
c215a8c6bf Fix spacing in the EmptyPlaceholder (#3722)
Don't show in changelog
2022-03-04 10:57:55 +00:00
Clément Pasteau
225884dfe4 Allow defining up to 4 genres for a game 2022-03-04 09:35:44 +01:00
Fannie Yan
5d42956669 Fix duplicated extension after rename (#3718)
Fix duplicated extension after rename
2022-03-03 17:23:27 +01:00
Clément Pasteau
977bbdde89 Fix a crash on Windows when the Documents folder is missing 2022-03-03 12:24:02 +01:00
D8H
73357c830d Add a "get started" button at the center of empty lists for variables, behaviors, effects and events (#3708) 2022-03-02 18:04:22 +01:00
AlexandreS
c619f94277 Make context menus in scene editor and in object list consistent 2022-03-02 12:08:10 +01:00
Clément Pasteau
c5dabf9b6e Optimise onboarding flow to show to all web users 2022-03-02 10:24:36 +01:00
Arthur Pacaud
cc4303e21e Fine tune search in instruction selector and stores to better handle short words in main field 2022-03-01 13:54:54 +01:00
Florian Rival
566aadafd9 Fix initial language setup when only a language match exists (#3713)
Don't show in changelog
2022-03-01 12:50:09 +00:00
Elairyx
9cd96461d7 Fix various typos (#3711) 2022-03-01 11:34:28 +00:00
AlexandreS
0395b73f4a Clarify separation between object and instance in instance properties panel 2022-03-01 12:10:05 +01:00
Clément Pasteau
bcc40d4728 Show warning when accessing builds for a game you don't own 2022-02-28 16:39:40 +01:00
186 changed files with 3950 additions and 2156 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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."),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -208,6 +208,10 @@ export const ExtensionOptionsEditor = ({
text: 'Ads',
value: 'Ads',
},
{
text: 'Audio',
value: 'Audio',
},
{
text: 'Advanced',
value: 'Advanced',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,7 +49,8 @@ export type ExportPipeline<
renderCustomStepsProgress?: (
build: ?Build,
errored: boolean,
exportStep: BuildStep
exportStep: BuildStep,
getGameThumbnailUrl: (buildId: string) => ?string
) => React.Node,
prepareExporter: (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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