Compare commits

...

25 Commits

Author SHA1 Message Date
Clément Pasteau
abce34f2b1 Bump to 5.3.197 (#6497)
Do not show in changelog
2024-04-03 10:08:23 +02:00
github-actions[bot]
07276d5e16 Update translations [skip ci] (#6496)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2024-04-03 10:07:26 +02:00
Arthur Pacaud (arthuro555)
1fdd8cc792 Add an action to allow disabling P2P direct connections (#6475) 2024-04-02 22:38:41 +02:00
AlexandreS
79e40605d5 Fix collapse/expand arrow on event sheet for light themes (#6495) 2024-04-02 22:36:00 +02:00
D8H
fcc91e3fea Fix issues with Sprite animation frame updates (#6493) 2024-04-02 21:14:48 +02:00
github-actions[bot]
5c66623631 Update translations [skip ci] (#6487)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-03-28 12:07:38 +01:00
AlexandreS
5637642e1b Compute max discount instead of using hardcoded value (#6488)
Don't show in changelog
2024-03-28 11:05:13 +01:00
Florian Rival
7e8b44af2e Fix expressions involving a variable (or property) of type number/string wrongly rejected when an operator like + was used after them (#6467)
* For example, "Your score is " + NumberVariable + " points" was rejected, because the second operator type was not properly inferred.
* If something does not work in your game anymore, double check if your expressions in the events sheets are not underlined in red. Sometimes, adding `ToString(` or `ToNumber(` around it might be required.
2024-03-28 09:42:41 +01:00
Clément Pasteau
0dd4650aae Bump to 5.3.196 (#6486)
Do not show in changelog
2024-03-28 09:35:22 +01:00
github-actions[bot]
c7cac31830 Update translations [skip ci] (#6457)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2024-03-28 08:57:13 +01:00
Clément Pasteau
56cb8581c4 Fix plugin-consent version for admob (#6485) 2024-03-27 19:09:42 +01:00
Florian Rival
1993040b70 Add community leaderboard to highlight members doing great feedbacks (#6484) 2024-03-27 18:47:14 +01:00
AlexandreS
883991081a Define app theme related CSS variables at HTML body level (#6483)
Only show in developer changelog
2024-03-27 17:48:19 +01:00
D8H
7d8afef1ad Fix collision mask of rotated Spine objects (#6482)
- Fixes rotation of Spine objects in the editor.
2024-03-27 16:35:55 +01:00
Florian Rival
8178595546 Use a CSS module for SimpleTextField, to avoid name clashes (#6481)
Only show in developer changelog
2024-03-27 12:06:53 +01:00
AlexandreS
a478068c64 Add possibility to subscribe to GDevelop on a yearly basis (#6462) 2024-03-27 11:26:44 +01:00
Tristan Rhodes
368da1b610 New action for Physics behavior: Set the linear velocity towards an angle (#5670) 2024-03-27 08:27:00 +01:00
D8H
4ee43202e9 Fix sprite with resource tests (#6479)
- Don't show in changelog
2024-03-26 15:34:31 +01:00
Florian Rival
602fdf4bfd Fix warning
Don't show in changelog
2024-03-26 11:40:16 +01:00
Florian Rival
6110acafcc Add buttons to rate the quality of feedbacks (#6478)
* When you receive a feedback on a game, go to the Game Dashboard (or check the email notification for the feedback) to rank the comment as great, good or not useful. If a comment is abusive, spammy or harmful, you can also report it as such.
* Users making the best comments will be showcased in a leaderboard on the community page and on gd.games
* As a game creator, you're also benefit from this: games with the most rated feedbacks will be displayed in a leaderboard and on gd.games. We encourage you to take the time to rate the feedbacks you're receiving so your game becomes more visible to the community!
2024-03-26 11:28:45 +01:00
D8H
a3696ca9d1 Allow custom objects to use animations (#6426) 2024-03-25 10:47:28 +01:00
Clément Pasteau
1bb473b0b0 Display questions when canceling a subscription (#6471) 2024-03-22 14:07:59 +01:00
Florian Rival
4376b4f36e Improve first screen layouts (#6468) 2024-03-22 10:33:42 +01:00
Florian Rival
6ecbae9c35 Display the coordinates of the center point of a Sprite even when set automatically
Fix #6472
2024-03-22 10:33:05 +01:00
D8H
93e9fc6aed Fix missing expressions for text object (#6470) 2024-03-21 10:30:25 +01:00
395 changed files with 9514 additions and 4202 deletions

View File

@@ -6,8 +6,6 @@
#include "GDCore/Extensions/Builtin/SpriteExtension/Animation.h"
#include <vector>
#include "GDCore/Extensions/Builtin/SpriteExtension/Direction.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/Sprite.h"
#include "GDCore/String.h"
namespace gd {

View File

@@ -4,13 +4,11 @@
* reserved. This project is released under the MIT License.
*/
#ifndef GDCORE_ANIMATION_H
#define GDCORE_ANIMATION_H
#pragma once
#include <vector>
#include "GDCore/String.h"
namespace gd {
class Direction;
}
#include "GDCore/Extensions/Builtin/SpriteExtension/Direction.h"
namespace gd {
@@ -93,4 +91,3 @@ class GD_CORE_API Animation {
};
} // namespace gd
#endif // GDCORE_ANIMATION_H

View File

@@ -3,12 +3,12 @@
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#ifndef GDCORE_DIRECTION_H
#define GDCORE_DIRECTION_H
#pragma once
#include <vector>
#include "GDCore/String.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/Sprite.h"
namespace gd {
class Sprite;
class SerializerElement;
}
@@ -142,4 +142,3 @@ class GD_CORE_API Direction {
};
} // namespace gd
#endif // GDCORE_DIRECTION_H

View File

@@ -0,0 +1,157 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteAnimationList.h"
#include <algorithm>
#include "GDCore/CommonTools.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/Animation.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/Direction.h"
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
#include "GDCore/Project/InitialInstance.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/PropertyDescriptor.h"
#include "GDCore/Serialization/SerializerElement.h"
#include "GDCore/Tools/Localization.h"
namespace gd {
Animation SpriteAnimationList::badAnimation;
SpriteAnimationList::SpriteAnimationList()
: adaptCollisionMaskAutomatically(true) {}
SpriteAnimationList::~SpriteAnimationList(){};
void SpriteAnimationList::UnserializeFrom(const gd::SerializerElement& element) {
adaptCollisionMaskAutomatically =
element.GetBoolAttribute("adaptCollisionMaskAutomatically", false);
RemoveAllAnimations();
const gd::SerializerElement& animationsElement =
element.GetChild("animations", 0, "Animations");
animationsElement.ConsiderAsArrayOf("animation", "Animation");
for (std::size_t i = 0; i < animationsElement.GetChildrenCount(); ++i) {
const gd::SerializerElement& animationElement =
animationsElement.GetChild(i);
Animation newAnimation;
newAnimation.useMultipleDirections = animationElement.GetBoolAttribute(
"useMultipleDirections", false, "typeNormal");
newAnimation.SetName(animationElement.GetStringAttribute("name", ""));
// Compatibility with GD <= 3.3
if (animationElement.HasChild("Direction")) {
for (std::size_t j = 0;
j < animationElement.GetChildrenCount("Direction");
++j) {
Direction direction;
direction.UnserializeFrom(animationElement.GetChild("Direction", j));
newAnimation.SetDirectionsCount(newAnimation.GetDirectionsCount() + 1);
newAnimation.SetDirection(direction,
newAnimation.GetDirectionsCount() - 1);
}
}
// End of compatibility code
else {
const gd::SerializerElement& directionsElement =
animationElement.GetChild("directions");
directionsElement.ConsiderAsArrayOf("direction");
for (std::size_t j = 0; j < directionsElement.GetChildrenCount(); ++j) {
Direction direction;
direction.UnserializeFrom(directionsElement.GetChild(j));
newAnimation.SetDirectionsCount(newAnimation.GetDirectionsCount() + 1);
newAnimation.SetDirection(direction,
newAnimation.GetDirectionsCount() - 1);
}
}
AddAnimation(newAnimation);
}
}
void SpriteAnimationList::SerializeTo(gd::SerializerElement& element) const {
element.SetAttribute("adaptCollisionMaskAutomatically",
adaptCollisionMaskAutomatically);
// Animations
gd::SerializerElement& animationsElement = element.AddChild("animations");
animationsElement.ConsiderAsArrayOf("animation");
for (std::size_t k = 0; k < GetAnimationsCount(); k++) {
gd::SerializerElement& animationElement =
animationsElement.AddChild("animation");
animationElement.SetAttribute("useMultipleDirections",
GetAnimation(k).useMultipleDirections);
animationElement.SetAttribute("name", GetAnimation(k).GetName());
gd::SerializerElement& directionsElement =
animationElement.AddChild("directions");
directionsElement.ConsiderAsArrayOf("direction");
for (std::size_t l = 0; l < GetAnimation(k).GetDirectionsCount(); l++) {
GetAnimation(k).GetDirection(l).SerializeTo(
directionsElement.AddChild("direction"));
}
}
}
void SpriteAnimationList::ExposeResources(gd::ArbitraryResourceWorker& worker) {
for (std::size_t j = 0; j < GetAnimationsCount(); j++) {
for (std::size_t k = 0; k < GetAnimation(j).GetDirectionsCount(); k++) {
for (std::size_t l = 0;
l < GetAnimation(j).GetDirection(k).GetSpritesCount();
l++) {
worker.ExposeImage(
GetAnimation(j).GetDirection(k).GetSprite(l).GetImageName());
}
}
}
}
const Animation& SpriteAnimationList::GetAnimation(std::size_t nb) const {
if (nb >= animations.size()) return badAnimation;
return animations[nb];
}
Animation& SpriteAnimationList::GetAnimation(std::size_t nb) {
if (nb >= animations.size()) return badAnimation;
return animations[nb];
}
void SpriteAnimationList::AddAnimation(const Animation& animation) {
animations.push_back(animation);
}
bool SpriteAnimationList::RemoveAnimation(std::size_t nb) {
if (nb >= GetAnimationsCount()) return false;
animations.erase(animations.begin() + nb);
return true;
}
void SpriteAnimationList::SwapAnimations(std::size_t firstIndex,
std::size_t secondIndex) {
if (firstIndex < animations.size() && secondIndex < animations.size() &&
firstIndex != secondIndex)
std::swap(animations[firstIndex], animations[secondIndex]);
}
void SpriteAnimationList::MoveAnimation(std::size_t oldIndex, std::size_t newIndex) {
if (oldIndex >= animations.size() || newIndex >= animations.size()) return;
auto animation = animations[oldIndex];
animations.erase(animations.begin() + oldIndex);
animations.insert(animations.begin() + newIndex, animation);
}
} // namespace gd

View File

@@ -0,0 +1,119 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#pragma once
#include "GDCore/Extensions/Builtin/SpriteExtension/Animation.h"
namespace gd {
class InitialInstance;
class SerializerElement;
class PropertyDescriptor;
class ArbitraryResourceWorker;
} // namespace gd
namespace gd {
/**
* \brief A list of animations, containing directions with images and collision mask.
*
* It's used in the configuration of object that implements image-based animations.
*
* \see Animation
* \see Direction
* \see Sprite
* \ingroup SpriteObjectExtension
*/
class GD_CORE_API SpriteAnimationList {
public:
SpriteAnimationList();
virtual ~SpriteAnimationList();
void ExposeResources(gd::ArbitraryResourceWorker& worker);
/**
* \brief Return the animation at the specified index.
* If the index is out of bound, a "bad animation" object is returned.
*/
const Animation& GetAnimation(std::size_t nb) const;
/**
* \brief Return the animation at the specified index.
* If the index is out of bound, a "bad animation" object is returned.
*/
Animation& GetAnimation(std::size_t nb);
/**
* \brief Return the number of animations this object has.
*/
std::size_t GetAnimationsCount() const { return animations.size(); };
/**
* \brief Add an animation at the end of the existing ones.
*/
void AddAnimation(const Animation& animation);
/**
* \brief Remove an animation.
*/
bool RemoveAnimation(std::size_t nb);
/**
* \brief Remove all animations.
*/
void RemoveAllAnimations() { animations.clear(); }
/**
* \brief Return true if the object hasn't any animation.
*/
bool HasNoAnimations() const { return animations.empty(); }
/**
* \brief Swap the position of two animations
*/
void SwapAnimations(std::size_t firstIndex, std::size_t secondIndex);
/**
* \brief Change the position of the specified animation
*/
void MoveAnimation(std::size_t oldIndex, std::size_t newIndex);
/**
* \brief Return a read-only reference to the vector containing all the
* animation of the object.
*/
const std::vector<Animation>& GetAllAnimations() const { return animations; }
/**
* @brief Check if the collision mask adapts automatically to the animation.
*/
bool AdaptCollisionMaskAutomatically() const {
return adaptCollisionMaskAutomatically;
}
/**
* @brief Set if the collision mask adapts automatically to the animation.
*/
void SetAdaptCollisionMaskAutomatically(bool enable) {
adaptCollisionMaskAutomatically = enable;
}
void UnserializeFrom(const gd::SerializerElement& element);
void SerializeTo(gd::SerializerElement& element) const;
private:
mutable std::vector<Animation> animations;
static Animation badAnimation; //< Bad animation when an out of bound
// animation is requested.
bool adaptCollisionMaskAutomatically; ///< If set to true, the collision
///< mask will be automatically
///< adapted to the animation of the
///< object.
};
} // namespace gd

View File

@@ -23,88 +23,20 @@
namespace gd {
Animation SpriteObject::badAnimation;
SpriteObject::SpriteObject()
: updateIfNotVisible(false), adaptCollisionMaskAutomatically(true) {}
: updateIfNotVisible(false) {}
SpriteObject::~SpriteObject(){};
void SpriteObject::DoUnserializeFrom(gd::Project& project,
const gd::SerializerElement& element) {
updateIfNotVisible = element.GetBoolAttribute("updateIfNotVisible", true);
adaptCollisionMaskAutomatically =
element.GetBoolAttribute("adaptCollisionMaskAutomatically", false);
RemoveAllAnimations();
const gd::SerializerElement& animationsElement =
element.GetChild("animations", 0, "Animations");
animationsElement.ConsiderAsArrayOf("animation", "Animation");
for (std::size_t i = 0; i < animationsElement.GetChildrenCount(); ++i) {
const gd::SerializerElement& animationElement =
animationsElement.GetChild(i);
Animation newAnimation;
newAnimation.useMultipleDirections = animationElement.GetBoolAttribute(
"useMultipleDirections", false, "typeNormal");
newAnimation.SetName(animationElement.GetStringAttribute("name", ""));
// Compatibility with GD <= 3.3
if (animationElement.HasChild("Direction")) {
for (std::size_t j = 0;
j < animationElement.GetChildrenCount("Direction");
++j) {
Direction direction;
direction.UnserializeFrom(animationElement.GetChild("Direction", j));
newAnimation.SetDirectionsCount(newAnimation.GetDirectionsCount() + 1);
newAnimation.SetDirection(direction,
newAnimation.GetDirectionsCount() - 1);
}
}
// End of compatibility code
else {
const gd::SerializerElement& directionsElement =
animationElement.GetChild("directions");
directionsElement.ConsiderAsArrayOf("direction");
for (std::size_t j = 0; j < directionsElement.GetChildrenCount(); ++j) {
Direction direction;
direction.UnserializeFrom(directionsElement.GetChild(j));
newAnimation.SetDirectionsCount(newAnimation.GetDirectionsCount() + 1);
newAnimation.SetDirection(direction,
newAnimation.GetDirectionsCount() - 1);
}
}
AddAnimation(newAnimation);
}
animations.UnserializeFrom(element);
}
void SpriteObject::DoSerializeTo(gd::SerializerElement& element) const {
element.SetAttribute("updateIfNotVisible", updateIfNotVisible);
element.SetAttribute("adaptCollisionMaskAutomatically",
adaptCollisionMaskAutomatically);
// Animations
gd::SerializerElement& animationsElement = element.AddChild("animations");
animationsElement.ConsiderAsArrayOf("animation");
for (std::size_t k = 0; k < GetAnimationsCount(); k++) {
gd::SerializerElement& animationElement =
animationsElement.AddChild("animation");
animationElement.SetAttribute("useMultipleDirections",
GetAnimation(k).useMultipleDirections);
animationElement.SetAttribute("name", GetAnimation(k).GetName());
gd::SerializerElement& directionsElement =
animationElement.AddChild("directions");
directionsElement.ConsiderAsArrayOf("direction");
for (std::size_t l = 0; l < GetAnimation(k).GetDirectionsCount(); l++) {
GetAnimation(k).GetDirection(l).SerializeTo(
directionsElement.AddChild("direction"));
}
}
animations.SerializeTo(element);
}
std::map<gd::String, gd::PropertyDescriptor> SpriteObject::GetProperties()
@@ -127,16 +59,7 @@ bool SpriteObject::UpdateProperty(const gd::String& name,
}
void SpriteObject::ExposeResources(gd::ArbitraryResourceWorker& worker) {
for (std::size_t j = 0; j < GetAnimationsCount(); j++) {
for (std::size_t k = 0; k < GetAnimation(j).GetDirectionsCount(); k++) {
for (std::size_t l = 0;
l < GetAnimation(j).GetDirection(k).GetSpritesCount();
l++) {
worker.ExposeImage(
GetAnimation(j).GetDirection(k).GetSprite(l).GetImageName());
}
}
}
animations.ExposeResources(worker);
}
std::map<gd::String, gd::PropertyDescriptor>
@@ -168,42 +91,12 @@ bool SpriteObject::UpdateInitialInstanceProperty(
return true;
}
const Animation& SpriteObject::GetAnimation(std::size_t nb) const {
if (nb >= animations.size()) return badAnimation;
return animations[nb];
const SpriteAnimationList& SpriteObject::GetAnimations() const {
return animations;
}
Animation& SpriteObject::GetAnimation(std::size_t nb) {
if (nb >= animations.size()) return badAnimation;
return animations[nb];
}
void SpriteObject::AddAnimation(const Animation& animation) {
animations.push_back(animation);
}
bool SpriteObject::RemoveAnimation(std::size_t nb) {
if (nb >= GetAnimationsCount()) return false;
animations.erase(animations.begin() + nb);
return true;
}
void SpriteObject::SwapAnimations(std::size_t firstIndex,
std::size_t secondIndex) {
if (firstIndex < animations.size() && secondIndex < animations.size() &&
firstIndex != secondIndex)
std::swap(animations[firstIndex], animations[secondIndex]);
}
void SpriteObject::MoveAnimation(std::size_t oldIndex, std::size_t newIndex) {
if (oldIndex >= animations.size() || newIndex >= animations.size()) return;
auto animation = animations[oldIndex];
animations.erase(animations.begin() + oldIndex);
animations.insert(animations.begin() + newIndex, animation);
SpriteAnimationList& SpriteObject::GetAnimations() {
return animations;
}
} // namespace gd

View File

@@ -4,18 +4,15 @@
* reserved. This project is released under the MIT License.
*/
#ifndef GDCORE_SPRITEOBJECT_H
#define GDCORE_SPRITEOBJECT_H
#include "GDCore/Extensions/Builtin/SpriteExtension/Animation.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/Direction.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/Sprite.h"
#pragma once
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteAnimationList.h"
#include "GDCore/Project/Object.h"
namespace gd {
class InitialInstance;
class Object;
class Layout;
class Sprite;
class Animation;
class SerializerElement;
class PropertyDescriptor;
} // namespace gd
@@ -59,76 +56,15 @@ class GD_CORE_API SpriteObject : public gd::ObjectConfiguration {
gd::Project& project,
gd::Layout& scene) override;
/** \name Animations
* Methods related to animations management
*/
///@{
/**
* \brief Return the animation at the specified index.
* If the index is out of bound, a "bad animation" object is returned.
* \brief Return the animation configuration.
*/
const Animation& GetAnimation(std::size_t nb) const;
const SpriteAnimationList& GetAnimations() const;
/**
* \brief Return the animation at the specified index.
* If the index is out of bound, a "bad animation" object is returned.
* @brief Return the animation configuration.
*/
Animation& GetAnimation(std::size_t nb);
/**
* \brief Return the number of animations this object has.
*/
std::size_t GetAnimationsCount() const { return animations.size(); };
/**
* \brief Add an animation at the end of the existing ones.
*/
void AddAnimation(const Animation& animation);
/**
* \brief Remove an animation.
*/
bool RemoveAnimation(std::size_t nb);
/**
* \brief Remove all animations.
*/
void RemoveAllAnimations() { animations.clear(); }
/**
* \brief Return true if the object hasn't any animation.
*/
bool HasNoAnimations() const { return animations.empty(); }
/**
* \brief Swap the position of two animations
*/
void SwapAnimations(std::size_t firstIndex, std::size_t secondIndex);
/**
* \brief Change the position of the specified animation
*/
void MoveAnimation(std::size_t oldIndex, std::size_t newIndex);
/**
* \brief Return a read-only reference to the vector containing all the
* animation of the object.
*/
const std::vector<Animation>& GetAllAnimations() const { return animations; }
/**
* @brief Check if the collision mask adapts automatically to the animation.
*/
bool AdaptCollisionMaskAutomatically() const {
return adaptCollisionMaskAutomatically;
}
/**
* @brief Set if the collision mask adapts automatically to the animation.
*/
void SetAdaptCollisionMaskAutomatically(bool enable) {
adaptCollisionMaskAutomatically = enable;
}
SpriteAnimationList& GetAnimations();
/**
* \brief Set if the object animation should be played even if the object is
@@ -143,25 +79,17 @@ class GD_CORE_API SpriteObject : public gd::ObjectConfiguration {
* is hidden or far from the camera (false by default).
*/
bool GetUpdateIfNotVisible() const { return updateIfNotVisible; }
///@}
private:
void DoUnserializeFrom(gd::Project& project,
const gd::SerializerElement& element) override;
void DoSerializeTo(gd::SerializerElement& element) const override;
mutable std::vector<Animation> animations;
SpriteAnimationList animations;
bool updateIfNotVisible; ///< If set to true, ask the game engine to play
///< object animation even if hidden or far from
///< the screen.
static Animation badAnimation; //< Bad animation when an out of bound
// animation is requested.
bool adaptCollisionMaskAutomatically; ///< If set to true, the collision
///< mask will be automatically
///< adapted to the animation of the
///< object.
};
} // namespace gd
#endif // GDCORE_SPRITEOBJECT_H

View File

@@ -86,8 +86,10 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
void OnVisitOperatorNode(OperatorNode& node) override {
ReportAnyError(node);
// The "required" type ("parentType") will be used when visiting the first operand.
// Note that it may be refined thanks to this first operand (see later).
node.leftHandSide->Visit(*this);
const Type leftType = childType;
const Type leftType = childType; // Store the type of the first operand.
if (leftType == Type::Number) {
if (node.op == ' ') {
@@ -120,15 +122,19 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
node.rightHandSide->location);
}
parentType = leftType;
// The "required" type ("parentType") of the second operator is decided by:
// - the parent type. Unless it can (`number|string`) or should (`unknown`) be refined, then:
// - the first operand.
parentType = ShouldTypeBeRefined(parentType) ? leftType : parentType;
node.rightHandSide->Visit(*this);
const Type rightType = childType;
// The type is decided by the first operand, unless it can (`number|string`)
// or should (`unknown`) be refined, in which case we go for the right
// operand (which got visited knowing the type of the first operand, so it's
// The type of the overall operator ("childType") is decided by:
// - the parent type. Unless it can (`number|string`) or should (`unknown`) be refined, then:
// - the first operand. Unless it can (`number|string`) or should (`unknown`) be refined, then:
// - the right operand (which got visited knowing the type of the first operand, so it's
// equal or strictly more precise than the left operand).
childType = (leftType == Type::Unknown || leftType == Type::NumberOrString) ? leftType : rightType;
childType = ShouldTypeBeRefined(parentType) ? (ShouldTypeBeRefined(leftType) ? leftType : rightType) : parentType;
}
void OnVisitUnaryOperatorNode(UnaryOperatorNode& node) override {
ReportAnyError(node);
@@ -395,6 +401,10 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
}
}
static bool ShouldTypeBeRefined(Type type) {
return (type == Type::Unknown || type == Type::NumberOrString);
}
static Type StringToType(const gd::String &type);
static const gd::String &TypeToString(Type type);
static const gd::String unknownTypeString;

View File

@@ -13,12 +13,14 @@
#include "GDCore/Serialization/SerializerElement.h"
#include "GDCore/Tools/Log.h"
#include "GDCore/Project/CustomConfigurationHelper.h"
#include "GDCore/Project/InitialInstance.h"
using namespace gd;
void CustomObjectConfiguration::Init(const gd::CustomObjectConfiguration& objectConfiguration) {
project = objectConfiguration.project;
objectContent = objectConfiguration.objectContent;
animations = objectConfiguration.animations;
// There is no default copy for a map of unique_ptr like childObjectConfigurations.
childObjectConfigurations.clear();
@@ -88,23 +90,38 @@ bool CustomObjectConfiguration::UpdateProperty(const gd::String& propertyName,
std::map<gd::String, gd::PropertyDescriptor>
CustomObjectConfiguration::GetInitialInstanceProperties(
const gd::InitialInstance& instance,
gd::Project& project,
gd::Layout& scene) {
return std::map<gd::String, gd::PropertyDescriptor>();
const gd::InitialInstance &initialInstance, gd::Project &project,
gd::Layout &scene) {
std::map<gd::String, gd::PropertyDescriptor> properties;
if (!animations.HasNoAnimations()) {
properties["animation"] =
gd::PropertyDescriptor(
gd::String::From(initialInstance.GetRawDoubleProperty("animation")))
.SetLabel(_("Animation"))
.SetType("number");
}
return properties;
}
bool CustomObjectConfiguration::UpdateInitialInstanceProperty(
gd::InitialInstance& instance,
const gd::String& name,
const gd::String& value,
gd::Project& project,
gd::Layout& scene) {
return false;
gd::InitialInstance &initialInstance, const gd::String &name,
const gd::String &value, gd::Project &project, gd::Layout &scene) {
if (name == "animation") {
initialInstance.SetRawDoubleProperty(
"animation", std::max(0, value.empty() ? 0 : value.To<int>()));
}
return true;
}
void CustomObjectConfiguration::DoSerializeTo(SerializerElement& element) const {
element.AddChild("content") = objectContent;
if (!animations.HasNoAnimations()) {
auto &animatableElement = element.AddChild("animatable");
animations.SerializeTo(animatableElement);
}
auto &childrenContentElement = element.AddChild("childrenContent");
for (auto &pair : childObjectConfigurations) {
auto &childName = pair.first;
@@ -116,6 +133,12 @@ void CustomObjectConfiguration::DoSerializeTo(SerializerElement& element) const
void CustomObjectConfiguration::DoUnserializeFrom(Project& project,
const SerializerElement& element) {
objectContent = element.GetChild("content");
if (element.HasChild("animatable")) {
auto &animatableElement = element.GetChild("animatable");
animations.UnserializeFrom(animatableElement);
}
auto &childrenContentElement = element.GetChild("childrenContent");
for (auto &pair : childrenContentElement.GetAllChildren()) {
auto &childName = pair.first;
@@ -126,6 +149,8 @@ void CustomObjectConfiguration::DoUnserializeFrom(Project& project,
}
void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& worker) {
animations.ExposeResources(worker);
std::map<gd::String, gd::PropertyDescriptor> properties = GetProperties();
for (auto& property : properties) {
@@ -178,3 +203,11 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor
configuration.ExposeResources(worker);
}
}
const SpriteAnimationList& CustomObjectConfiguration::GetAnimations() const {
return animations;
}
SpriteAnimationList& CustomObjectConfiguration::GetAnimations() {
return animations;
}

View File

@@ -3,8 +3,7 @@
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#ifndef GDCORE_CUSTOMOBJECTCONFIGURATION_H
#define GDCORE_CUSTOMOBJECTCONFIGURATION_H
#pragma once
#include "GDCore/Project/ObjectConfiguration.h"
@@ -16,7 +15,7 @@
#include "GDCore/Project/PropertyDescriptor.h"
#include "GDCore/Serialization/Serializer.h"
#include "GDCore/Serialization/SerializerElement.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteAnimationList.h"
using namespace gd;
@@ -72,7 +71,17 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
gd::ObjectConfiguration &GetChildObjectConfiguration(const gd::String& objectName);
protected:
/**
* \brief Return the animation configuration for Animatable custom objects.
*/
const SpriteAnimationList& GetAnimations() const;
/**
* @brief Return the animation configuration for Animatable custom objects.
*/
SpriteAnimationList& GetAnimations();
protected:
void DoSerializeTo(SerializerElement& element) const override;
void DoUnserializeFrom(Project& project, const SerializerElement& element) override;
@@ -84,6 +93,8 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
static gd::ObjectConfiguration badObjectConfiguration;
SpriteAnimationList animations;
/**
* Initialize configuration using another configuration. Used by copy-ctor
* and assign-op.
@@ -95,6 +106,5 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
*/
void Init(const gd::CustomObjectConfiguration& object);
};
} // namespace gd
#endif // GDCORE_CUSTOMOBJECTCONFIGURATION_H
} // namespace gd

View File

@@ -15,6 +15,7 @@ EventsBasedObject::EventsBasedObject()
gd::EventsFunctionsContainer::FunctionOwner::Object),
ObjectsContainer(),
isRenderedIn3D(false),
isAnimatable(false),
isTextContainer(false) {
}
@@ -32,6 +33,9 @@ void EventsBasedObject::SerializeTo(SerializerElement& element) const {
if (isRenderedIn3D) {
element.SetBoolAttribute("is3D", true);
}
if (isAnimatable) {
element.SetBoolAttribute("isAnimatable", true);
}
if (isTextContainer) {
element.SetBoolAttribute("isTextContainer", true);
}
@@ -45,6 +49,7 @@ void EventsBasedObject::UnserializeFrom(gd::Project& project,
const SerializerElement& element) {
defaultName = element.GetStringAttribute("defaultName");
isRenderedIn3D = element.GetBoolAttribute("is3D", false);
isAnimatable = element.GetBoolAttribute("isAnimatable", false);
isTextContainer = element.GetBoolAttribute("isTextContainer", false);
AbstractEventsBasedEntity::UnserializeFrom(project, element);

View File

@@ -85,6 +85,19 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity, public Ob
*/
bool IsRenderedIn3D() const { return isRenderedIn3D; }
/**
* \brief Declare an Animatable capability.
*/
EventsBasedObject& MarkAsAnimatable(bool isAnimatable_) {
isAnimatable = isAnimatable_;
return *this;
}
/**
* \brief Return true if the object needs an Animatable capability.
*/
bool IsAnimatable() const { return isAnimatable; }
/**
* \brief Declare a TextContainer capability.
*/
@@ -106,6 +119,7 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity, public Ob
private:
gd::String defaultName;
bool isRenderedIn3D;
bool isAnimatable;
bool isTextContainer;
};

View File

@@ -91,7 +91,7 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
sprite.SetImageName("res1");
anim.SetDirectionsCount(1);
anim.GetDirection(0).AddSprite(sprite);
spriteConfiguration.AddAnimation(anim);
spriteConfiguration.GetAnimations().AddAnimation(anim);
gd::Object obj("myObject", "", spriteConfiguration.Clone());
project.InsertObject(obj, 0);
@@ -138,7 +138,7 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
sprite.SetImageName("res1");
anim.SetDirectionsCount(1);
anim.GetDirection(0).AddSprite(sprite);
spriteConfiguration.AddAnimation(anim);
spriteConfiguration.GetAnimations().AddAnimation(anim);
gd::Object obj("myObject", "", spriteConfiguration.Clone());
layout.InsertObject(obj, 0);
@@ -437,7 +437,7 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
sprite.SetImageName("res1");
anim.SetDirectionsCount(1);
anim.GetDirection(0).AddSprite(sprite);
spriteConfiguration.AddAnimation(anim);
spriteConfiguration.GetAnimations().AddAnimation(anim);
gd::Object obj("myObject", "", spriteConfiguration.Clone());
layout.InsertObject(obj, 0);

View File

@@ -30,6 +30,21 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") {
layout1.GetVariables().InsertNew("MySceneBooleanVariable").SetBool(true);
layout1.GetVariables().InsertNew("MySceneStructureVariable").GetChild("MyChild");
layout1.GetVariables().InsertNew("MySceneStructureVariable2").GetChild("MyChild");
layout1.GetVariables().InsertNew("MySceneEmptyArrayVariable").CastTo(gd::Variable::Type::Array);
{
auto& variable = layout1.GetVariables().InsertNew("MySceneNumberArrayVariable");
variable.CastTo(gd::Variable::Type::Array);
variable.PushNew().SetValue(1);
variable.PushNew().SetValue(2);
variable.PushNew().SetValue(3);
}
{
auto& variable = layout1.GetVariables().InsertNew("MySceneStringArrayVariable");
variable.CastTo(gd::Variable::Type::Array);
variable.PushNew().SetString("1");
variable.PushNew().SetString("2");
variable.PushNew().SetString("3");
}
auto &mySpriteObject = layout1.InsertNewObject(project, "MyExtension::Sprite", "MySpriteObject", 0);
mySpriteObject.GetVariables().InsertNew("MyNumberVariable").SetValue(123);
@@ -1295,6 +1310,221 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") {
REQUIRE(output == "getVariableForObject(MySpriteObject, MyOtherSpriteObject).getChild(\"Child\").getChild(\"Grandchild\")");
}
}
SECTION("Type conversions (valid operators with variables having different types than the expression)") {
SECTION("Expression/parent type is 'string'") {
{
auto node =
parser.ParseExpression("\"You have \" + MySceneVariable + \" points\"");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "\"You have \" + getLayoutVariable(MySceneVariable).getAsString() + \" points\"");
}
{
auto node =
parser.ParseExpression("MySceneVariable + MySceneStringVariable");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneVariable).getAsString() + getLayoutVariable(MySceneStringVariable).getAsString()");
}
}
SECTION("Expression/parent type is 'string' (with an unknown variable)") {
{
auto node =
parser.ParseExpression("\"You have \" + MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric + \" points\"");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "\"You have \" + getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getChild(\"CantKnownTheTypeSoStayGeneric\").getAsString() + \" points\"");
}
}
SECTION("Expression/parent type is 'string' (2 number variables)") {
{
auto node =
parser.ParseExpression("MySceneVariable + MySceneVariable2 + \"world\"");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneVariable).getAsString() + getLayoutVariable(MySceneVariable2).getAsString() + \"world\"");
}
{
auto node =
parser.ParseExpression("MySceneVariable + MySceneVariable2 + MySceneStringVariable");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneVariable).getAsString() + getLayoutVariable(MySceneVariable2).getAsString() + getLayoutVariable(MySceneStringVariable).getAsString()");
}
}
SECTION("Expression/parent type is 'string' (array variable)") {
{
auto node =
parser.ParseExpression("\"hello\" + MySceneNumberArrayVariable[2] + \"world\"");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "\"hello\" + getLayoutVariable(MySceneNumberArrayVariable).getChild(2).getAsString() + \"world\"");
}
{
auto node =
parser.ParseExpression("\"hello\" + MySceneEmptyArrayVariable[2] + \"world\"");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "\"hello\" + getLayoutVariable(MySceneEmptyArrayVariable).getChild(2).getAsString() + \"world\"");
}
}
SECTION("Expression/parent type is 'number'") {
{
auto node =
parser.ParseExpression("123 + MySceneVariable + 456");
gd::ExpressionCodeGenerator expressionCodeGenerator("number",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "123 + getLayoutVariable(MySceneVariable).getAsNumber() + 456");
}
{
auto node =
parser.ParseExpression("MySceneStringVariable + MySceneVariable");
gd::ExpressionCodeGenerator expressionCodeGenerator("number",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStringVariable).getAsNumber() + getLayoutVariable(MySceneVariable).getAsNumber()");
}
}
SECTION("Expression/parent type is 'string' (with an unknown variable)") {
{
auto node =
parser.ParseExpression("123 + MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric + 456");
gd::ExpressionCodeGenerator expressionCodeGenerator("number",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "123 + getLayoutVariable(MySceneStructureVariable).getChild(\"MyChild\").getChild(\"CantKnownTheTypeSoStayGeneric\").getAsNumber() + 456");
}
}
SECTION("Expression/parent type is 'number' (2 string variables)") {
{
auto node =
parser.ParseExpression("MySceneStringVariable + MySceneStringVariable + 456");
gd::ExpressionCodeGenerator expressionCodeGenerator("number",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStringVariable).getAsNumber() + getLayoutVariable(MySceneStringVariable).getAsNumber() + 456");
}
{
auto node =
parser.ParseExpression("MySceneStringVariable + MySceneStringVariable + MySceneVariable");
gd::ExpressionCodeGenerator expressionCodeGenerator("number",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "getLayoutVariable(MySceneStringVariable).getAsNumber() + getLayoutVariable(MySceneStringVariable).getAsNumber() + getLayoutVariable(MySceneVariable).getAsNumber()");
}
}
SECTION("Expression/parent type is 'number' (array variable)") {
{
auto node =
parser.ParseExpression("123 + MySceneNumberArrayVariable[2] + 456");
gd::ExpressionCodeGenerator expressionCodeGenerator("number",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "123 + getLayoutVariable(MySceneNumberArrayVariable).getChild(2).getAsNumber() + 456");
}
{
auto node =
parser.ParseExpression("123 + MySceneEmptyArrayVariable[2] + 456");
gd::ExpressionCodeGenerator expressionCodeGenerator("number",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "123 + getLayoutVariable(MySceneEmptyArrayVariable).getChild(2).getAsNumber() + 456");
}
}
SECTION("Multiple type conversions in sub expressions or same expression") {
{
auto node =
parser.ParseExpression("\"hello\" + MySceneNumberArrayVariable[2 + MySceneStringVariable] + \"world\" + MySceneVariable + \"world 2\"");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "\"hello\" + getLayoutVariable(MySceneNumberArrayVariable).getChild(2 + getLayoutVariable(MySceneStringVariable).getAsNumber()).getAsString() + \"world\" + getLayoutVariable(MySceneVariable).getAsString() + \"world 2\"");
}
{
auto node =
parser.ParseExpression("\"hello\" + MySceneNumberArrayVariable[\"foo\" + MySceneVariable + \"bar\"] + \"world\" + MySceneVariable + \"world 2\"");
gd::ExpressionCodeGenerator expressionCodeGenerator("string",
"",
codeGenerator,
context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() == "\"hello\" + getLayoutVariable(MySceneNumberArrayVariable).getChild(\"foo\" + getLayoutVariable(MySceneVariable).getAsString() + \"bar\").getAsString() + \"world\" + getLayoutVariable(MySceneVariable).getAsString() + \"world 2\"");
}
}
}
SECTION("Mixed test (1)") {
{
auto node = parser.ParseExpression("-+-MyExtension::MouseX(,)");

View File

@@ -2954,6 +2954,238 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
}
}
SECTION("Valid operators with variables having different types than the expression") {
SECTION("Expression/parent type is 'string'") {
// A trivial test (everything is a string).
{
auto node = parser.ParseExpression("\"You have \" + MySceneStringVariable + \" points\"");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A string concatenated with a number variable (will have to be casted to a string in code generation)
{
auto node = parser.ParseExpression("\"You have \" + MySceneNumberVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A string concatenated with a number variable (will have to be casted to a string in code generation)
// and then with a string again.
{
auto node = parser.ParseExpression("\"You have \" + MySceneNumberVariable + \" points\"");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A string concatenated with an unknown variable (will have to be casted to a string in code generation)
// and then with a string again.
{
auto node = parser.ParseExpression("\"You have \" + MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric + \" points\"");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
}
SECTION("Expression/parent type is 'number'") {
// A trivial test (everything is a string).
{
auto node = parser.ParseExpression("123 + MySceneNumberVariable + 456");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A number concatenated with a string variable (will have to be casted to a number in code generation)
{
auto node = parser.ParseExpression("123 + MySceneStringVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A number concatenated with a string variable (will have to be casted to a number in code generation)
// and then with a number again.
{
auto node = parser.ParseExpression("123 + MySceneStringVariable + 456");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A number concatenated with an unknown variable (will have to be casted to a number in code generation)
// and then with a number again.
{
auto node = parser.ParseExpression("123 + MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric + 456");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
}
SECTION("Expression/parent type is 'number|string'") {
SECTION("Expression/parent inferred type is 'string'") {
// A trivial test (everything is a string).
{
auto node = parser.ParseExpression("\"You have \" + MySceneStringVariable + \" points\"");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A string concatenated with a number variable (will have to be casted to a string in code generation)
{
auto node = parser.ParseExpression("\"You have \" + MySceneNumberVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A string concatenated with a number variable (will have to be casted to a string in code generation)
// and then with a string again.
{
auto node = parser.ParseExpression("\"You have \" + MySceneNumberVariable + \" points\"");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A string concatenated with an unknown variable (will have to be casted to a string in code generation)
// and then with a string again.
{
auto node = parser.ParseExpression("\"You have \" + MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric + \" points\"");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
}
SECTION("Expression/parent inferred type is 'number'") {
// A trivial test (everything is a string).
{
auto node = parser.ParseExpression("123 + MySceneNumberVariable + 456");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A number concatenated with a string variable (will have to be casted to a number in code generation)
{
auto node = parser.ParseExpression("123 + MySceneStringVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A number concatenated with a string variable (will have to be casted to a number in code generation)
// and then with a number again.
{
auto node = parser.ParseExpression("123 + MySceneStringVariable + 456");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
// A number concatenated with an unknown variable (will have to be casted to a number in code generation)
// and then with a number again.
{
auto node = parser.ParseExpression("123 + MySceneStructureVariable.MyChild.CantKnownTheTypeSoStayGeneric + 456");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
}
}
}
SECTION("Invalid operators with variables having different types than the expression") {
// Try to do a sum between numbers in a string expression
{
auto node = parser.ParseExpression("\"You have \" + MySceneNumberVariable + 2 + \" points\"");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == "You entered a number, but a text was expected (in quotes).");
REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 38);
REQUIRE(validator.GetFatalErrors()[0]->GetEndPosition() == 39);
}
// Try to do a sum between numbers in a number|string expression (that is inferred as a string with the first operand)
{
auto node = parser.ParseExpression("\"You have \" + MySceneNumberVariable + 2 + \" points\"");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == "You entered a number, but a text was expected (in quotes).");
REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 38);
REQUIRE(validator.GetFatalErrors()[0]->GetEndPosition() == 39);
}
// Try to do a string concatenation in a number expression
{
auto node = parser.ParseExpression("123 + MySceneStringVariable + \"hello\" + 456");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == "You entered a text, but a number was expected.");
REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 30);
REQUIRE(validator.GetFatalErrors()[0]->GetEndPosition() == 37);
}
// Try to do a string concatenation in a number|string expression (that is inferred as a number with the first operand)
{
auto node = parser.ParseExpression("123 + MySceneStringVariable + \"hello\" + 456");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == "You entered a text, but a number was expected.");
REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 30);
REQUIRE(validator.GetFatalErrors()[0]->GetEndPosition() == 37);
}
}
SECTION("Valid function call with object variable") {
{
// Note that in this test we need to use an expression with "objectvar",

View File

@@ -71,7 +71,7 @@ TEST_CASE("ObjectAssetSerializer", "[common]") {
frame.SetImageName("assets/Idle.png");
direction.AddSprite(frame);
spriteConfiguration->AddAnimation(animation);
spriteConfiguration->GetAnimations().AddAnimation(animation);
}
SerializerElement assetElement;

View File

@@ -35,7 +35,7 @@ void SetupSpriteConfiguration(gd::ObjectConfiguration &configuration) {
REQUIRE(spriteConfiguration != nullptr);
gd::Animation animation;
animation.SetName("Idle");
spriteConfiguration->AddAnimation(animation);
spriteConfiguration->GetAnimations().AddAnimation(animation);
};
gd::Object &SetupProjectWithSprite(gd::Project &project,
@@ -83,9 +83,9 @@ void CheckSpriteConfigurationInProjectElement(
void CheckSpriteConfiguration(gd::ObjectConfiguration &configuration) {
auto *spriteConfiguration = dynamic_cast<gd::SpriteObject *>(&configuration);
REQUIRE(spriteConfiguration);
REQUIRE(spriteConfiguration->GetAnimationsCount() == 1);
REQUIRE(spriteConfiguration->GetAnimations().GetAnimationsCount() == 1);
auto &animation = spriteConfiguration->GetAnimation(0);
auto &animation = spriteConfiguration->GetAnimations().GetAnimation(0);
REQUIRE(animation.GetName() == "Idle");
};

View File

@@ -1,4 +1,7 @@
namespace gdjs {
export interface PixiImageManager {
_threeAnimationFrameTextureManager: ThreeAnimationFrameTextureManager;
}
/**
* The renderer for a {@link gdjs.CustomRuntimeObject3D} using Three.js.
*/
@@ -131,5 +134,46 @@ namespace gdjs {
setLayerIndex(layer: gdjs.RuntimeLayer, index: float): void {
// Layers are not handled for 3D custom objects.
}
static getAnimationFrameTextureManager(
imageManager: gdjs.PixiImageManager
): ThreeAnimationFrameTextureManager {
if (!imageManager._threeAnimationFrameTextureManager) {
imageManager._threeAnimationFrameTextureManager = new ThreeAnimationFrameTextureManager(
imageManager
);
}
return imageManager._threeAnimationFrameTextureManager;
}
}
class ThreeAnimationFrameTextureManager
implements gdjs.AnimationFrameTextureManager<THREE.Material> {
private _imageManager: gdjs.PixiImageManager;
constructor(imageManager: gdjs.PixiImageManager) {
this._imageManager = imageManager;
}
getAnimationFrameTexture(imageName: string) {
return this._imageManager.getThreeMaterial(imageName, {
useTransparentTexture: true,
forceBasicMaterial: true,
});
}
getAnimationFrameWidth(material: THREE.Material) {
const map = (material as
| THREE.MeshBasicMaterial
| THREE.MeshStandardMaterial).map;
return map ? map.image.width : 0;
}
getAnimationFrameHeight(material: THREE.Material) {
const map = (material as
| THREE.MeshBasicMaterial
| THREE.MeshStandardMaterial).map;
return map ? map.image.height : 0;
}
}
}

View File

@@ -33,6 +33,14 @@ module.exports = {
.addInstructionOrExpressionGroupMetadata(_('AdMob'))
.setIcon('JsPlatform/Extensions/admobicon24.png');
extension
.addDependency()
.setName('Consent Cordova plugin')
.setDependencyType('cordova')
.setExportName('cordova-plugin-consent')
.setVersion('2.4.0')
.onlyIfOtherDependencyIsExported('AdMob Cordova plugin');
extension
.registerProperty('AdMobAppIdAndroid')
.setLabel(_('AdMob Android App ID'))
@@ -63,13 +71,6 @@ module.exports = {
)
.onlyIfSomeExtraSettingsNonEmpty();
extension
.addDependency()
.setName('Consent Cordova plugin')
.setDependencyType('cordova')
.setExportName('cordova-plugin-consent')
.onlyIfOtherDependencyIsExported('AdMob Cordova plugin');
extension
.addAction(
'SetTestMode',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -422,6 +422,17 @@ namespace gdjs {
});
};
/**
* Forces the usage of a relay (TURN) server, to avoid sharing IP addresses with the other peers.
* @param shouldUseRelayServer Whether relay-only should be enabled or disabled.
*/
export const forceUseRelayServer = (shouldUseRelayServer: boolean) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceTransportPolicy = shouldUseRelayServer
? 'relay'
: 'all';
};
/**
* Overrides the default peer ID. Must be called before connecting to a broker.
* Overriding the ID may have unwanted consequences. Do not use this feature

View File

@@ -174,6 +174,33 @@ module.exports = {
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setFunctionName('gdjs.evtTools.p2p.useCustomICECandidate');
extension
.addAction(
'ForceRelayServer',
_('Disable IP address sharing'),
_(
'Disables the sharing of IP addresses with the other peers. ' +
'This action needs to be called BEFORE connecting to the broker server.'
),
_('Disable IP sharing: _PARAM0_'),
'',
'JsPlatform/Extensions/p2picon.svg',
'JsPlatform/Extensions/p2picon.svg'
)
.addParameter(
'yesorno',
_('Disable sharing of IP addresses'),
'Generally, it is recommended to keep sharing of IP addressed enabled ' +
'to make connections faster and more often possible. ' +
'Disabling IP address sharing will force all connections to pass messages through a ' +
'TURN relay server, you can make P2P use one by adding one as an ICE candidate.',
false
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setFunctionName('gdjs.evtTools.p2p.forceUseRelayServer');
extension
.addAction(
'UseDefaultBroker',

View File

@@ -1415,6 +1415,26 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('getLinearVelocityAngle');
aut
.addAction(
'LinearVelocityAngle',
_('Linear velocity towards an angle'),
_('Set the linear velocity towards an angle.'),
_(
'Set the linear velocity of _PARAM0_ towards angle: _PARAM2_ degrees, speed: _PARAM3_ pixels per second'
),
_('Velocity'),
'res/physics32.png',
'res/physics32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Angle'))
.addParameter('expression', _('Speed (in pixels per second)'))
.getCodeExtraInformation()
.setFunctionName('setLinearVelocityAngle')
.setGetter('getLinearVelocityAngle');
aut
.addExpression(
'LinearVelocityAngle',

View File

@@ -1429,6 +1429,23 @@ namespace gdjs {
);
}
setLinearVelocityAngle(angle: float, linearVelocity: float): void {
// If there is no body, set a new one
if (this._body === null) {
if (!this.createBody()) return;
}
const body = this._body!;
// Set the linear velocity toward an angle
angle = gdjs.toRad(angle);
body.SetLinearVelocity(
this.b2Vec2(
linearVelocity * Math.cos(angle) * this._sharedData.invScaleX,
linearVelocity * Math.sin(angle) * this._sharedData.invScaleY
)
);
}
getAngularVelocity(): float {
// If there is no body, set a new one
if (this._body === null) {

View File

@@ -148,6 +148,9 @@ module.exports = {
this._instance.getX(),
this._instance.getY()
);
this._pixiObject.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
this.setAnimation(this._instance.getRawDoubleProperty('animation'));
@@ -188,6 +191,14 @@ module.exports = {
return -this._spineOriginOffsetY;
}
getCenterX() {
return this.getOriginX();
}
getCenterY() {
return this.getOriginY();
}
/**
* @param {number} index - animation index
*/

View File

@@ -124,6 +124,16 @@ namespace gdjs {
return this.getY() + originOffset.y;
}
getCenterX(): float {
const originOffset = this._renderer.getOriginOffset();
return -originOffset.x;
}
getCenterY(): float {
const originOffset = this._renderer.getOriginOffset();
return -originOffset.y;
}
onDestroyed(): void {
super.onDestroyed();
this._renderer.onDestroy();

View File

@@ -40,38 +40,6 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
.AddDefaultBehavior("ScalableCapability::ScalableBehavior")
.AddDefaultBehavior("OpacityCapability::OpacityBehavior");
// Deprecated
obj.AddAction("String",
_("Modify the text"),
_("Modify the text of a Text object."),
_("the text"),
"",
"res/actions/text24_black.png",
"res/actions/text_black.png")
.SetHidden()
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"string",
gd::ParameterOptions::MakeNewOptions().SetDescription(_("Text")))
.SetFunctionName("SetString")
.SetGetter("GetString");
// Deprecated
obj.AddCondition("String",
_("Compare the text"),
_("Compare the text of a Text object."),
_("the text"),
"",
"res/conditions/text24_black.png",
"res/conditions/text_black.png")
.SetHidden()
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"string",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Text to compare to")))
.SetFunctionName("GetString");
obj.AddAction("Font",
_("Font"),
_("Change the font of the text."),
@@ -84,94 +52,6 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
.AddParameter("police", _("Font"))
.SetFunctionName("ChangeFont");
// Deprecated
obj.AddCondition("ScaleX",
_("Scale on X axis"),
_("Compare the scale of the text on the X axis"),
_("the scale on the X axis"),
"Scale",
"res/conditions/scaleWidth24_black.png",
"res/conditions/scaleWidth_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale to compare to (1 by default)")))
.SetHidden()
.SetFunctionName("GetScaleX");
// Deprecated
obj.AddAction(
"ScaleX",
_("Scale on X axis"),
_("Modify the scale of the text on the X axis (default scale is 1)"),
_("the scale on the X axis"),
_("Scale"),
"res/actions/scaleWidth24_black.png",
"res/actions/scaleWidth_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale (1 by default)")))
.SetHidden()
.SetFunctionName("SetScaleX");
// Deprecated
obj.AddCondition("ScaleY",
_("Scale on Y axis"),
_("Compare the scale of the text on the Y axis"),
_("the scale on the Y axis"),
"Scale",
"res/conditions/scaleHeight24_black.png",
"res/conditions/scaleHeight_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale to compare to (1 by default)")))
.SetHidden()
.SetFunctionName("GetScaleY");
// Deprecated
obj.AddAction(
"ScaleY",
_("Scale on Y axis"),
_("Modify the scale of the text on the Y axis (default scale is 1)"),
_("the scale on the Y axis"),
_("Scale"),
"res/actions/scaleHeight24_black.png",
"res/actions/scaleHeight_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale (1 by default)")))
.SetHidden()
.SetFunctionName("SetScaleY");
// Deprecated
obj.AddAction(
"Scale",
_("Scale"),
_("Modify the scale of the specified object (default scale is 1)"),
_("the scale"),
_("Scale"),
"res/actions/scale24_black.png",
"res/actions/scale_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale (1 by default)")))
.SetHidden()
.SetFunctionName("SetScale");
obj.AddAction(
"ChangeColor",
_("Color"),
@@ -355,43 +235,6 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Blur radius")));
// Deprecated
obj.AddAction("Opacity",
_("Text opacity"),
_("Change the opacity of a Text. 0 is fully transparent, 255 "
"is opaque (default)."),
_("the opacity"),
"",
"res/actions/opacity24.png",
"res/actions/opacity.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Opacity (0-255)")))
.SetFunctionName("SetOpacity")
.SetGetter("GetOpacity")
.SetHidden();
// Deprecated
obj.AddCondition("Opacity",
_("Opacity"),
_("Compare the opacity of a Text object, between 0 (fully "
"transparent) to 255 (opaque)."),
_("the opacity"),
"",
"res/conditions/opacity24.png",
"res/conditions/opacity.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Opacity to compare to (0-255)")))
.SetFunctionName("GetOpacity")
.SetHidden();
obj.AddAction("SetSmooth",
_("Smoothing"),
_("Activate or deactivate text smoothing."),
@@ -484,37 +327,6 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
.AddParameter("object", _("Object"), "Text")
.SetFunctionName("IsUnderlined");
obj.AddAction("Angle",
_("Angle"),
_("Modify the angle of a Text object."),
_("the angle"),
_("Rotation"),
"res/actions/rotate24_black.png",
"res/actions/rotate_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Angle (in degrees)")))
.SetFunctionName("SetAngle")
.SetGetter("GetAngle");
obj.AddCondition("Angle",
_("Angle"),
_("Compare the value of the angle of a Text object."),
_("the angle"),
_("Rotation"),
"res/conditions/rotate24_black.png",
"res/conditions/rotate_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Angle to compare to (in degrees)")))
.SetFunctionName("GetAngle");
obj.AddCondition("Padding",
_("Padding"),
_("Compare the number of pixels around a text object. If "
@@ -628,6 +440,143 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
"res/actions/textPadding_black.png")
.AddParameter("object", _("Object"), "Text");
obj.AddExpressionAndConditionAndAction("number",
"FontSize",
_("Font size"),
_("the font size of a text object"),
_("the font size"),
"",
"res/conditions/characterSize24.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions());
// Support for deprecated "Size" actions/conditions:
obj.AddDuplicatedAction("Size", "Text::SetFontSize").SetHidden();
obj.AddDuplicatedCondition("Size", "Text::FontSize").SetHidden();
// Deprecated
obj.AddAction("Angle",
_("Angle"),
_("Modify the angle of a Text object."),
_("the angle"),
_("Rotation"),
"res/actions/rotate24_black.png",
"res/actions/rotate_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Angle (in degrees)")))
.SetHidden()
.SetFunctionName("SetAngle")
.SetGetter("GetAngle");
// Deprecated
obj.AddCondition("Angle",
_("Angle"),
_("Compare the value of the angle of a Text object."),
_("the angle"),
_("Rotation"),
"res/conditions/rotate24_black.png",
"res/conditions/rotate_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Angle to compare to (in degrees)")))
.SetHidden()
.SetFunctionName("GetAngle");
// Deprecated
obj.AddCondition("ScaleX",
_("Scale on X axis"),
_("Compare the scale of the text on the X axis"),
_("the scale on the X axis"),
"Scale",
"res/conditions/scaleWidth24_black.png",
"res/conditions/scaleWidth_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale to compare to (1 by default)")))
.SetHidden()
.SetFunctionName("GetScaleX");
// Deprecated
obj.AddAction(
"ScaleX",
_("Scale on X axis"),
_("Modify the scale of the text on the X axis (default scale is 1)"),
_("the scale on the X axis"),
_("Scale"),
"res/actions/scaleWidth24_black.png",
"res/actions/scaleWidth_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale (1 by default)")))
.SetHidden()
.SetFunctionName("SetScaleX");
// Deprecated
obj.AddCondition("ScaleY",
_("Scale on Y axis"),
_("Compare the scale of the text on the Y axis"),
_("the scale on the Y axis"),
"Scale",
"res/conditions/scaleHeight24_black.png",
"res/conditions/scaleHeight_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale to compare to (1 by default)")))
.SetHidden()
.SetFunctionName("GetScaleY");
// Deprecated
obj.AddAction(
"ScaleY",
_("Scale on Y axis"),
_("Modify the scale of the text on the Y axis (default scale is 1)"),
_("the scale on the Y axis"),
_("Scale"),
"res/actions/scaleHeight24_black.png",
"res/actions/scaleHeight_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale (1 by default)")))
.SetHidden()
.SetFunctionName("SetScaleY");
// Deprecated
obj.AddAction(
"Scale",
_("Scale"),
_("Modify the scale of the specified object (default scale is 1)"),
_("the scale"),
_("Scale"),
"res/actions/scale24_black.png",
"res/actions/scale_black.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Scale (1 by default)")))
.SetHidden()
.SetFunctionName("SetScale");
// Deprecated
obj.AddExpression("ScaleX",
_("X Scale of a Text object"),
@@ -648,6 +597,43 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
.SetHidden()
.SetFunctionName("GetScaleY");
// Deprecated
obj.AddAction("Opacity",
_("Text opacity"),
_("Change the opacity of a Text. 0 is fully transparent, 255 "
"is opaque (default)."),
_("the opacity"),
"",
"res/actions/opacity24.png",
"res/actions/opacity.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Opacity (0-255)")))
.SetFunctionName("SetOpacity")
.SetGetter("GetOpacity")
.SetHidden();
// Deprecated
obj.AddCondition("Opacity",
_("Opacity"),
_("Compare the opacity of a Text object, between 0 (fully "
"transparent) to 255 (opaque)."),
_("the opacity"),
"",
"res/conditions/opacity24.png",
"res/conditions/opacity.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Opacity to compare to (0-255)")))
.SetFunctionName("GetOpacity")
.SetHidden();
// Deprecated
obj.AddExpression("Opacity",
_("Opacity of a Text object"),
@@ -658,30 +644,52 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
.SetFunctionName("GetOpacity")
.SetHidden();
// Deprecated
obj.AddExpression("Angle",
_("Angle"),
_("Angle"),
_("Rotation"),
"res/actions/rotate_black.png")
.AddParameter("object", _("Object"), "Text")
.SetHidden()
.SetFunctionName("GetAngle");
obj.AddExpressionAndConditionAndAction("number",
"FontSize",
_("Font size"),
_("the font size of a text object"),
_("the font size"),
"",
"res/conditions/characterSize24.png")
// Deprecated
obj.AddAction("String",
_("Modify the text"),
_("Modify the text of a Text object."),
_("the text"),
"",
"res/actions/text24_black.png",
"res/actions/text_black.png")
.SetHidden()
.AddParameter("object", _("Object"), "Text")
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions());
.UseStandardOperatorParameters(
"string",
gd::ParameterOptions::MakeNewOptions().SetDescription(_("Text")))
.SetFunctionName("SetString")
.SetGetter("GetString");
// Support for deprecated "Size" actions/conditions:
obj.AddDuplicatedAction("Size", "Text::SetFontSize").SetHidden();
obj.AddDuplicatedCondition("Size", "Text::FontSize").SetHidden();
// Deprecated
obj.AddCondition("String",
_("Compare the text"),
_("Compare the text of a Text object."),
_("the text"),
"",
"res/conditions/text24_black.png",
"res/conditions/text_black.png")
.SetHidden()
.AddParameter("object", _("Object"), "Text")
.UseStandardRelationalOperatorParameters(
"string",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Text to compare to")))
.SetFunctionName("GetString");
// Deprecated
obj.AddStrExpression(
"String", _("Text"), _("Text"), _("Text"), "res/texteicon.png")
.AddParameter("object", _("Object"), "Text")
.SetHidden()
.SetFunctionName("GetString");
}

View File

@@ -29,24 +29,6 @@ class TextObjectJsExtension : public gd::PlatformExtension {
.SetIncludeFile("Extensions/TextObject/textruntimeobject.js")
.AddIncludeFile(
"Extensions/TextObject/textruntimeobject-pixi-renderer.js");
GetAllActionsForObject("TextObject::Text")["TextObject::Scale"]
.SetFunctionName("setScale")
.SetGetter("getScaleMean");
GetAllActionsForObject("TextObject::Text")["TextObject::ScaleX"]
.SetFunctionName("setScaleX")
.SetGetter("getScaleX");
GetAllConditionsForObject("TextObject::Text")["TextObject::ScaleX"]
.SetFunctionName("getScaleX");
GetAllActionsForObject("TextObject::Text")["TextObject::ScaleY"]
.SetFunctionName("setScaleY")
.SetGetter("getScaleY");
GetAllConditionsForObject("TextObject::Text")["TextObject::ScaleY"]
.SetFunctionName("getScaleY");
GetAllActionsForObject("TextObject::Text")["TextObject::String"]
.SetFunctionName("setString")
.SetGetter("getString");
GetAllConditionsForObject("TextObject::Text")["TextObject::String"]
.SetFunctionName("getString");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetFontSize"]
.SetFunctionName("setCharacterSize")
@@ -56,24 +38,6 @@ class TextObjectJsExtension : public gd::PlatformExtension {
GetAllExpressionsForObject("TextObject::Text")["FontSize"]
.SetFunctionName("getCharacterSize");
// Deprecated actions/conditions (use "FontSize"/"SetFontSize" instead):
GetAllActionsForObject("TextObject::Text")["TextObject::Size"]
.SetFunctionName("setCharacterSize")
.SetGetter("getCharacterSize");
GetAllConditionsForObject("TextObject::Text")["TextObject::Size"]
.SetFunctionName("getCharacterSize");
GetAllActionsForObject("TextObject::Text")["TextObject::Angle"]
.SetFunctionName("setAngle")
.SetGetter("getAngle");
GetAllConditionsForObject("TextObject::Text")["TextObject::Angle"]
.SetFunctionName("getAngle");
GetAllActionsForObject("TextObject::Text")["TextObject::Opacity"]
.SetFunctionName("setOpacity")
.SetGetter("getOpacity");
GetAllConditionsForObject("TextObject::Text")["TextObject::Opacity"]
.SetFunctionName("getOpacity");
GetAllActionsForObject("TextObject::Text")["TextObject::SetBold"]
.SetFunctionName("setBold");
GetAllConditionsForObject("TextObject::Text")["TextObject::IsBold"]
@@ -108,16 +72,6 @@ class TextObjectJsExtension : public gd::PlatformExtension {
GetAllExpressionsForObject("TextObject::Text")["Padding"]
.SetFunctionName("getPadding");
GetAllExpressionsForObject("TextObject::Text")["ScaleX"]
.SetFunctionName("getScaleX");
GetAllExpressionsForObject("TextObject::Text")["ScaleY"]
.SetFunctionName("getScaleY");
GetAllExpressionsForObject("TextObject::Text")["Opacity"]
.SetFunctionName("getOpacity");
GetAllExpressionsForObject("TextObject::Text")["Angle"]
.SetFunctionName("getAngle");
GetAllStrExpressionsForObject("TextObject::Text")["String"]
.SetFunctionName("getString");
GetAllActionsForObject("TextObject::Text")["TextObject::ChangeColor"]
.SetFunctionName("setColor");
@@ -125,15 +79,13 @@ class TextObjectJsExtension : public gd::PlatformExtension {
GetAllActionsForObject("TextObject::Text")["TextObject::SetGradient"]
.SetFunctionName("setGradient");
GetAllActionsForObject("TextObject::Text")["TextObject::SetOutline"]
.SetFunctionName("setOutline");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetOutlineEnabled"]
.SetFunctionName("setOutlineEnabled");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::IsOutlineEnabled"]
.SetFunctionName("isOutlineEnabled");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetOutlineColor"]
.SetFunctionName("setOutlineColor");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::OutlineThickness"]
GetAllExpressionsForObject("TextObject::Text")["OutlineThickness"]
.SetFunctionName("getOutlineThickness");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::OutlineThickness"]
.SetFunctionName("getOutlineThickness");
@@ -141,8 +93,6 @@ class TextObjectJsExtension : public gd::PlatformExtension {
.SetFunctionName("setOutlineThickness")
.SetGetter("getOutlineThickness");
GetAllActionsForObject("TextObject::Text")["TextObject::SetShadow"]
.SetFunctionName("setShadow");
GetAllActionsForObject("TextObject::Text")["TextObject::ShowShadow"]
.SetFunctionName("showShadow");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::IsShadowEnabled"]
@@ -150,7 +100,7 @@ class TextObjectJsExtension : public gd::PlatformExtension {
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetShadowColor"]
.SetFunctionName("setShadowColor");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::ShadowOpacity"]
GetAllExpressionsForObject("TextObject::Text")["ShadowOpacity"]
.SetFunctionName("getShadowOpacity");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::ShadowOpacity"]
.SetFunctionName("getShadowOpacity");
@@ -158,7 +108,7 @@ class TextObjectJsExtension : public gd::PlatformExtension {
.SetFunctionName("setShadowOpacity")
.SetGetter("getShadowOpacity");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::ShadowDistance"]
GetAllExpressionsForObject("TextObject::Text")["ShadowDistance"]
.SetFunctionName("getShadowDistance");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::ShadowDistance"]
.SetFunctionName("getShadowDistance");
@@ -166,7 +116,7 @@ class TextObjectJsExtension : public gd::PlatformExtension {
.SetFunctionName("setShadowDistance")
.SetGetter("getShadowDistance");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::ShadowAngle"]
GetAllExpressionsForObject("TextObject::Text")["ShadowAngle"]
.SetFunctionName("getShadowAngle");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::ShadowAngle"]
.SetFunctionName("getShadowAngle");
@@ -174,7 +124,7 @@ class TextObjectJsExtension : public gd::PlatformExtension {
.SetFunctionName("setShadowAngle")
.SetGetter("getShadowAngle");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::ShadowBlurRadius"]
GetAllExpressionsForObject("TextObject::Text")["ShadowBlurRadius"]
.SetFunctionName("getShadowBlurRadius");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::ShadowBlurRadius"]
.SetFunctionName("getShadowBlurRadius");
@@ -182,6 +132,61 @@ class TextObjectJsExtension : public gd::PlatformExtension {
.SetFunctionName("setShadowBlurRadius")
.SetGetter("getShadowBlurRadius");
// Deprecated actions/conditions (use "FontSize"/"SetFontSize" instead):
GetAllActionsForObject("TextObject::Text")["TextObject::Size"]
.SetFunctionName("setCharacterSize")
.SetGetter("getCharacterSize");
GetAllConditionsForObject("TextObject::Text")["TextObject::Size"]
.SetFunctionName("getCharacterSize");
// Deprecated: now available for all objects.
GetAllActionsForObject("TextObject::Text")["TextObject::Angle"]
.SetFunctionName("setAngle")
.SetGetter("getAngle");
GetAllConditionsForObject("TextObject::Text")["TextObject::Angle"]
.SetFunctionName("getAngle");
GetAllExpressionsForObject("TextObject::Text")["Angle"]
.SetFunctionName("getAngle");
// Deprecated: available through capabilities.
GetAllActionsForObject("TextObject::Text")["TextObject::Scale"]
.SetFunctionName("setScale")
.SetGetter("getScaleMean");
GetAllActionsForObject("TextObject::Text")["TextObject::ScaleX"]
.SetFunctionName("setScaleX")
.SetGetter("getScaleX");
GetAllConditionsForObject("TextObject::Text")["TextObject::ScaleX"]
.SetFunctionName("getScaleX");
GetAllActionsForObject("TextObject::Text")["TextObject::ScaleY"]
.SetFunctionName("setScaleY")
.SetGetter("getScaleY");
GetAllConditionsForObject("TextObject::Text")["TextObject::ScaleY"]
.SetFunctionName("getScaleY");
GetAllExpressionsForObject("TextObject::Text")["ScaleX"]
.SetFunctionName("getScaleX");
GetAllExpressionsForObject("TextObject::Text")["ScaleY"]
.SetFunctionName("getScaleY");
GetAllActionsForObject("TextObject::Text")["TextObject::String"]
.SetFunctionName("setString")
.SetGetter("getString");
GetAllStrExpressionsForObject("TextObject::Text")["String"]
.SetFunctionName("getString");
GetAllConditionsForObject("TextObject::Text")["TextObject::String"]
.SetFunctionName("getString");
GetAllExpressionsForObject("TextObject::Text")["Opacity"]
.SetFunctionName("getOpacity");
GetAllActionsForObject("TextObject::Text")["TextObject::Opacity"]
.SetFunctionName("setOpacity")
.SetGetter("getOpacity");
GetAllConditionsForObject("TextObject::Text")["TextObject::Opacity"]
.SetFunctionName("getOpacity");
// Deprecated: split into several instructions.
GetAllActionsForObject("TextObject::Text")["TextObject::SetOutline"]
.SetFunctionName("setOutline");
GetAllActionsForObject("TextObject::Text")["TextObject::SetShadow"]
.SetFunctionName("setShadow");
// Unimplemented actions and conditions:
GetAllActionsForObject("TextObject::Text")["TextObject::Font"]
.SetFunctionName("");

View File

@@ -141,6 +141,10 @@ gd::ObjectMetadata &MetadataDeclarationHelper::DeclareObjectMetadata(
objectMetadata.AddDefaultBehavior("EffectCapability::EffectBehavior");
objectMetadata.AddDefaultBehavior("OpacityCapability::OpacityBehavior");
}
if (eventsBasedObject.IsAnimatable()) {
objectMetadata
.AddDefaultBehavior("AnimatableCapability::AnimatableBehavior");
}
if (eventsBasedObject.IsTextContainer()) {
objectMetadata
.AddDefaultBehavior("TextContainerCapability::TextContainerBehavior");

View File

@@ -78,7 +78,7 @@ gd::String ObjectCodeGenerator::GenerateRuntimeObjectCompleteCode(
methodFullyQualifiedName,
"that._onceTriggers",
functionName == doStepPreEventsFunctionName
? GenerateDoStepPreEventsPreludeCode()
? GenerateDoStepPreEventsPreludeCode(eventsBasedObject)
: "",
functionName == onCreatedFunctionName
? "gdjs.CustomRuntimeObject.prototype.onCreated.call(this);\n"
@@ -110,6 +110,64 @@ gd::String ObjectCodeGenerator::GenerateRuntimeObjectCompleteCode(
return updateFromObjectCode;
},
// generateInitializeAnimatableCode
[&]() {
return gd::String(R"jscode_template(
this._animator = new gdjs.SpriteAnimator(
objectData.animatable.animations,
gdjs.RENDERER_CLASS_NAME.getAnimationFrameTextureManager(
parentInstanceContainer.getGame().getImageManager()));
)jscode_template")
.FindAndReplace("RENDERER_CLASS_NAME", eventsBasedObject.IsRenderedIn3D() ? "CustomRuntimeObject3DRenderer" : "CustomRuntimeObject2DRenderer");
},
// generateAnimatableCode
[&]() {
return gd::String(R"jscode_template(
// gdjs.Animatable interface implementation
getAnimator() {
return this._animator;
}
getAnimationIndex() {
return this._animator.getAnimationIndex();
}
setAnimationIndex(animationIndex) {
this._animator.setAnimationIndex(animationIndex);
}
getAnimationName() {
return this._animator.getAnimationName();
}
setAnimationName(animationName) {
this._animator.setAnimationName(animationName);
}
hasAnimationEnded() {
return this._animator.hasAnimationEnded();
}
isAnimationPaused() {
return this._animator.isAnimationPaused();
}
pauseAnimation() {
this._animator.pauseAnimation();
}
resumeAnimation() {
this._animator.resumeAnimation();
}
getAnimationSpeedScale() {
return this._animator.getAnimationSpeedScale();
}
setAnimationSpeedScale(ratio) {
this._animator.setAnimationSpeedScale(ratio);
}
getAnimationElapsedTime() {
return this._animator.getAnimationElapsedTime();
}
setAnimationElapsedTime(time) {
this._animator.setAnimationElapsedTime(time);
}
getAnimationDuration() {
return this._animator.getAnimationDuration();
}
)jscode_template");
},
// generateTextContainerCode
[&]() {
return gd::String(R"jscode_template(
@@ -133,6 +191,8 @@ gd::String ObjectCodeGenerator::GenerateRuntimeObjectTemplateCode(
std::function<gd::String()> generatePropertiesCode,
std::function<gd::String()> generateMethodsCode,
std::function<gd::String()> generateUpdateFromObjectDataCode,
std::function<gd::String()> generateInitializeAnimatableCode,
std::function<gd::String()> generateAnimatableCode,
std::function<gd::String()> generateTextContainerCode) {
return gd::String(R"jscode_template(
CODE_NAMESPACE = CODE_NAMESPACE || {};
@@ -148,6 +208,7 @@ CODE_NAMESPACE.RUNTIME_OBJECT_CLASSNAME = class RUNTIME_OBJECT_CLASSNAME extends
this._onceTriggers = new gdjs.OnceTriggers();
this._objectData = {};
INITIALIZE_PROPERTIES_CODE
INITIALIZE_ANIMATABLE_CODE
// It calls the onCreated super implementation at the end.
this.onCreated();
@@ -164,6 +225,8 @@ CODE_NAMESPACE.RUNTIME_OBJECT_CLASSNAME = class RUNTIME_OBJECT_CLASSNAME extends
// Properties:
PROPERTIES_CODE
ANIMATABLE_CODE
TEXT_CONTAINER_CODE
}
@@ -182,8 +245,13 @@ gdjs.registerObject("EXTENSION_NAME::OBJECT_NAME", CODE_NAMESPACE.RUNTIME_OBJECT
.FindAndReplace("CODE_NAMESPACE", codeNamespace)
.FindAndReplace("INITIALIZE_PROPERTIES_CODE",
generateInitializePropertiesCode())
.FindAndReplace("INITIALIZE_ANIMATABLE_CODE",
eventsBasedObject.IsAnimatable()
? generateInitializeAnimatableCode()
: "")
.FindAndReplace("UPDATE_FROM_OBJECT_DATA_CODE", generateUpdateFromObjectDataCode())
.FindAndReplace("PROPERTIES_CODE", generatePropertiesCode())
.FindAndReplace("ANIMATABLE_CODE", eventsBasedObject.IsAnimatable() ? generateAnimatableCode() : "")
.FindAndReplace("TEXT_CONTAINER_CODE", eventsBasedObject.IsTextContainer() ? generateTextContainerCode() : "")
.FindAndReplace("METHODS_CODE", generateMethodsCode());
;
@@ -282,11 +350,18 @@ CODE_NAMESPACE.RUNTIME_OBJECT_CLASSNAME.prototype.doStepPreEvents = function() {
.FindAndReplace("RUNTIME_OBJECT_CLASSNAME",
eventsBasedObject.GetName())
.FindAndReplace("CODE_NAMESPACE", codeNamespace)
.FindAndReplace("PRELUDE_CODE", GenerateDoStepPreEventsPreludeCode());
}
.FindAndReplace("PRELUDE_CODE",
GenerateDoStepPreEventsPreludeCode(eventsBasedObject));}
gd::String ObjectCodeGenerator::GenerateDoStepPreEventsPreludeCode() {
return "this._onceTriggers.startNewFrame();";
gd::String ObjectCodeGenerator::GenerateDoStepPreEventsPreludeCode(
const gd::EventsBasedObject& eventsBasedObject) {
gd::String doStepPreEventsPreludeCode;
doStepPreEventsPreludeCode += "this._onceTriggers.startNewFrame();";
if (eventsBasedObject.IsAnimatable()) {
doStepPreEventsPreludeCode +=
"\nthis._animator.step(this.getElapsedTime() / 1000);";
}
return doStepPreEventsPreludeCode;
}
} // namespace gdjs

View File

@@ -75,6 +75,8 @@ class ObjectCodeGenerator {
std::function<gd::String()> generateMethodsCode,
std::function<gd::String()> generatePropertiesCode,
std::function<gd::String()> generateUpdateFromObjectDataCode,
std::function<gd::String()> generateInitializeAnimatableCode,
std::function<gd::String()> generateAnimatableCode,
std::function<gd::String()> generateTextContainerCode);
gd::String GenerateRuntimeObjectPropertyTemplateCode(
@@ -105,7 +107,8 @@ class ObjectCodeGenerator {
const gd::EventsBasedObject& eventsBasedObject,
const gd::String& codeNamespace);
gd::String GenerateDoStepPreEventsPreludeCode();
gd::String GenerateDoStepPreEventsPreludeCode(
const gd::EventsBasedObject& eventsBasedObject);
gd::Project& project;

View File

@@ -745,6 +745,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "variablescontainer.js");
InsertUnique(includesFiles, "oncetriggers.js");
InsertUnique(includesFiles, "runtimebehavior.js");
InsertUnique(includesFiles, "SpriteAnimator.js");
InsertUnique(includesFiles, "spriteruntimeobject.js");
InsertUnique(includesFiles, "affinetransformation.js");
InsertUnique(includesFiles, "CustomRuntimeObjectInstanceContainer.js");

View File

@@ -9,6 +9,7 @@ namespace gdjs {
};
export type CustomObjectConfiguration = ObjectConfiguration & {
animatable?: SpriteAnimationData[];
childrenContent: { [objectName: string]: ObjectConfiguration & any };
};
@@ -86,10 +87,30 @@ namespace gdjs {
oldObjectData: ObjectData & CustomObjectConfiguration,
newObjectData: ObjectData & CustomObjectConfiguration
): boolean {
const animator = this.getAnimator();
if (animator) {
animator.updateFromObjectData(
oldObjectData.animatable || [],
newObjectData.animatable || []
);
}
return this._instanceContainer.updateFrom(oldObjectData, newObjectData);
}
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
const animator = this.getAnimator();
if (initialInstanceData.numberProperties) {
for (
let i = 0, len = initialInstanceData.numberProperties.length;
i < len;
++i
) {
const extraData = initialInstanceData.numberProperties[i];
if (animator && extraData.name === 'animation') {
animator.setAnimationIndex(extraData.value);
}
}
}
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
@@ -658,6 +679,15 @@ namespace gdjs {
isFlippedY(): boolean {
return this._flippedY;
}
/**
* Return the sprite animator.
*
* It returns `null` when custom objects don't have the Animatable capability.
*/
getAnimator(): gdjs.SpriteAnimator<any> | null {
return null;
}
}
// Others initialization and internal state management :

View File

@@ -0,0 +1,716 @@
namespace gdjs {
/** Represents a point in a coordinate system. */
export type SpritePoint = {
/** X position of the point. */
x: number;
/** Y position of the point. */
y: number;
};
/** Represents a custom point in a frame. */
export type SpriteCustomPointData = {
/** Name of the point. */
name: string;
/** X position of the point. */
x: number;
/** Y position of the point. */
y: number;
};
/** Represents the center point in a frame. */
export type SpriteCenterPointData = {
/** Name of the point. */
name: string;
/** Is the center automatically computed? */
automatic: boolean;
/** X position of the point. */
x: number;
/** Y position of the point. */
y: number;
};
/** Represents a {@link gdjs.SpriteAnimationFrame}. */
export type SpriteFrameData = {
/** The resource name of the image used in this frame. */
image: string;
/** The points of the frame. */
points: Array<SpriteCustomPointData>;
/** The origin point. */
originPoint: SpriteCustomPointData;
/** The center of the frame. */
centerPoint: SpriteCenterPointData;
/** Is The collision mask custom? */
hasCustomCollisionMask: boolean;
/** The collision mask if it is custom. */
customCollisionMask: Array<Array<SpritePoint>>;
};
/** Represents the data of a {@link gdjs.SpriteAnimationDirection}. */
export type SpriteDirectionData = {
/** Time between each frame, in seconds. */
timeBetweenFrames: number;
/** Is the animation looping? */
looping: boolean;
/** The list of frames. */
sprites: Array<SpriteFrameData>;
};
/** Represents the data of a {@link gdjs.SpriteAnimation}. */
export type SpriteAnimationData = {
/** The name of the animation. */
name: string;
/** Does the animation use multiple {@link gdjs.SpriteAnimationDirection}? */
useMultipleDirections: boolean;
/** The list of {@link SpriteDirectionData} representing {@link gdjs.SpriteAnimationDirection} instances. */
directions: Array<SpriteDirectionData>;
};
/**
* Abstraction from graphic libraries texture classes.
*/
export interface AnimationFrameTextureManager<T> {
getAnimationFrameTexture(imageName: string): T;
getAnimationFrameWidth(pixiTexture: T);
getAnimationFrameHeight(pixiTexture: T);
}
/**
* A frame used by a SpriteAnimation in a {@link gdjs.SpriteRuntimeObject}.
*
* It contains the texture displayed as well as information like the points position
* or the collision mask.
*/
export class SpriteAnimationFrame<T> {
image: string;
//TODO: Rename in imageName, and do not store it in the object?
texture: T;
center: SpritePoint = { x: 0, y: 0 };
origin: SpritePoint = { x: 0, y: 0 };
hasCustomHitBoxes: boolean = false;
customHitBoxes: gdjs.Polygon[] = [];
points: Hashtable<SpritePoint>;
/**
* @param imageManager The game image manager
* @param frameData The frame data used to initialize the frame
*/
constructor(
frameData: SpriteFrameData,
textureManager: gdjs.AnimationFrameTextureManager<T>
) {
this.image = frameData ? frameData.image : '';
this.texture = textureManager.getAnimationFrameTexture(this.image);
this.points = new Hashtable();
this.reinitialize(frameData, textureManager);
}
/**
* @param frameData The frame data used to initialize the frame
* @param textureManager The game image manager
*/
reinitialize(
frameData: SpriteFrameData,
textureManager: gdjs.AnimationFrameTextureManager<T>
) {
this.points.clear();
for (let i = 0, len = frameData.points.length; i < len; ++i) {
const ptData = frameData.points[i];
const point = { x: ptData.x, y: ptData.y };
this.points.put(ptData.name, point);
}
const origin = frameData.originPoint;
this.origin.x = origin.x;
this.origin.y = origin.y;
const center = frameData.centerPoint;
if (center.automatic !== true) {
this.center.x = center.x;
this.center.y = center.y;
} else {
this.center.x = textureManager.getAnimationFrameWidth(this.texture) / 2;
this.center.y =
textureManager.getAnimationFrameHeight(this.texture) / 2;
}
//Load the custom collision mask, if any:
if (frameData.hasCustomCollisionMask) {
this.hasCustomHitBoxes = true;
let i = 0;
for (let len = frameData.customCollisionMask.length; i < len; ++i) {
const polygonData: SpritePoint[] = frameData.customCollisionMask[i];
//Add a polygon, if necessary (Avoid recreating a polygon if it already exists).
if (i >= this.customHitBoxes.length) {
this.customHitBoxes.push(new gdjs.Polygon());
}
let j = 0;
for (const len2 = polygonData.length; j < len2; ++j) {
const pointData: SpritePoint = polygonData[j];
//Add a point, if necessary (Avoid recreating a point if it already exists).
if (j >= this.customHitBoxes[i].vertices.length) {
this.customHitBoxes[i].vertices.push([0, 0]);
}
this.customHitBoxes[i].vertices[j][0] = pointData.x;
this.customHitBoxes[i].vertices[j][1] = pointData.y;
}
this.customHitBoxes[i].vertices.length = j;
}
this.customHitBoxes.length = i;
} else {
this.customHitBoxes.length = 0;
}
}
/**
* Get a point of the frame.<br>
* If the point does not exist, the origin is returned.
* @param name The point's name
* @return The requested point. If it doesn't exists returns the origin point.
*/
getPoint(name: string): SpritePoint {
if (name === 'Centre' || name === 'Center') {
return this.center;
} else {
if (name === 'Origin') {
return this.origin;
}
}
return this.points.containsKey(name)
? this.points.get(name)
: this.origin;
}
}
/**
* Represents a direction of an animation of a {@link gdjs.SpriteRuntimeObject}.
*/
export class SpriteAnimationDirection<T> {
timeBetweenFrames: number;
loop: boolean;
frames: SpriteAnimationFrame<T>[] = [];
/**
* @param imageManager The game image manager
* @param directionData The direction data used to initialize the direction
*/
constructor(
directionData: SpriteDirectionData,
textureManager: gdjs.AnimationFrameTextureManager<T>
) {
this.timeBetweenFrames = directionData
? directionData.timeBetweenFrames
: 1.0;
this.loop = !!directionData.looping;
this.reinitialize(directionData, textureManager);
}
/**
* @param directionData The direction data used to initialize the direction
* @param textureManager The game image manager
*/
reinitialize(
directionData: SpriteDirectionData,
textureManager: gdjs.AnimationFrameTextureManager<T>
) {
this.timeBetweenFrames = directionData
? directionData.timeBetweenFrames
: 1.0;
this.loop = !!directionData.looping;
let i = 0;
for (const len = directionData.sprites.length; i < len; ++i) {
const frameData = directionData.sprites[i];
if (i < this.frames.length) {
this.frames[i].reinitialize(frameData, textureManager);
} else {
this.frames.push(
new gdjs.SpriteAnimationFrame<T>(frameData, textureManager)
);
}
}
this.frames.length = i;
}
}
/**
* Represents an animation of a {@link SpriteRuntimeObject}.
*/
export class SpriteAnimation<T> {
hasMultipleDirections: boolean;
name: string;
directions: gdjs.SpriteAnimationDirection<T>[] = [];
/**
* @param animData The animation data used to initialize the animation
* @param textureManager The game image manager
*/
constructor(
animData: SpriteAnimationData,
textureManager: gdjs.AnimationFrameTextureManager<T>
) {
this.hasMultipleDirections = !!animData.useMultipleDirections;
this.name = animData.name || '';
this.reinitialize(animData, textureManager);
}
/**
* @param animData The animation data used to initialize the animation
* @param textureManager The game image manager
*/
reinitialize(
animData: SpriteAnimationData,
textureManager: gdjs.AnimationFrameTextureManager<T>
) {
this.hasMultipleDirections = !!animData.useMultipleDirections;
this.name = animData.name || '';
let i = 0;
for (const len = animData.directions.length; i < len; ++i) {
const directionData = animData.directions[i];
if (i < this.directions.length) {
this.directions[i].reinitialize(directionData, textureManager);
} else {
this.directions.push(
new gdjs.SpriteAnimationDirection(directionData, textureManager)
);
}
}
// Make sure to delete already existing directions which are not used anymore.
this.directions.length = i;
}
}
/**
* Image-base animation model.
*/
export class SpriteAnimator<T> implements gdjs.Animatable {
_animations: gdjs.SpriteAnimation<T>[] = [];
_textureManager: gdjs.AnimationFrameTextureManager<T>;
/**
* Reference to the current SpriteAnimationFrame that is displayed.
* Can be null, so ensure that this case is handled properly.
*/
private _animationFrame: gdjs.SpriteAnimationFrame<T> | null = null;
private _animationFrameDirty: boolean = true;
private _currentAnimation: integer = 0;
private _currentDirection: integer = 0;
private _currentFrameIndex: integer = 0;
/** In seconds */
private _animationElapsedTime: float = 0;
private _animationSpeedScale: float = 1;
private _animationPaused: boolean = false;
private _onFrameChange: (() => void) | null = null;
/**
* @param frameData The frame data used to initialize the frame
* @param textureManager The game image manager
*/
constructor(
animations: Array<SpriteAnimationData>,
textureManager: gdjs.AnimationFrameTextureManager<T>
) {
this._textureManager = textureManager;
for (let i = 0, len = animations.length; i < len; ++i) {
this._animations.push(
new gdjs.SpriteAnimation(animations[i], textureManager)
);
}
}
invalidateFrame() {
this._animationFrameDirty = true;
if (this._onFrameChange) {
this._onFrameChange();
}
}
reinitialize(animations: Array<SpriteAnimationData>) {
this._currentAnimation = 0;
this._currentDirection = 0;
this._currentFrameIndex = 0;
this._animationElapsedTime = 0;
this._animationSpeedScale = 1;
this._animationPaused = false;
let i = 0;
for (const len = animations.length; i < len; ++i) {
const animData = animations[i];
if (i < this._animations.length) {
this._animations[i].reinitialize(animData, this._textureManager);
} else {
this._animations.push(
new gdjs.SpriteAnimation(animData, this._textureManager)
);
}
}
this._animations.length = i;
// Make sure to delete already existing animations which are not used anymore.
this._animationFrame = null;
this.invalidateFrame();
}
updateFromObjectData(
oldAnimations: Array<SpriteAnimationData>,
newAnimations: Array<SpriteAnimationData>
): boolean {
let i = 0;
for (const len = newAnimations.length; i < len; ++i) {
const animData = newAnimations[i];
if (i < this._animations.length) {
this._animations[i].reinitialize(animData, this._textureManager);
} else {
this._animations.push(
new gdjs.SpriteAnimation(animData, this._textureManager)
);
}
}
this._animations.length = i;
// Make sure to delete already existing animations which are not used anymore.
this.invalidateFrame();
const animationFrame = this.getCurrentFrame();
if (!animationFrame) {
this.setAnimationIndex(0);
}
return true;
}
/**
* @returns Returns the current frame or null if the current animation doesn't have any frame.
*/
getCurrentFrame(): gdjs.SpriteAnimationFrame<T> | null {
if (!this._animationFrameDirty) {
return this._animationFrame;
}
this._animationFrameDirty = false;
if (
this._currentAnimation < this._animations.length &&
this._currentDirection <
this._animations[this._currentAnimation].directions.length
) {
const direction = this._animations[this._currentAnimation].directions[
this._currentDirection
];
if (this._currentFrameIndex < direction.frames.length) {
this._animationFrame = direction.frames[this._currentFrameIndex];
return this._animationFrame;
}
}
//Invalid animation/direction/frame:
this._animationFrame = null;
return this._animationFrame;
}
/**
* Update the current frame of the object according to the elapsed time on the scene.
* @param timeDelta in seconds
*/
step(timeDelta: float): boolean {
if (
this._currentAnimation >= this._animations.length ||
this._currentDirection >=
this._animations[this._currentAnimation].directions.length
) {
return false;
}
const direction = this._animations[this._currentAnimation].directions[
this._currentDirection
];
const animationDuration = this.getAnimationDuration();
if (
!this._animationPaused &&
(direction.loop || this._animationElapsedTime !== animationDuration) &&
direction.timeBetweenFrames
) {
const animationElapsedTime =
this._animationElapsedTime + timeDelta * this._animationSpeedScale;
return this.setAnimationElapsedTime(
direction.loop
? gdjs.evtTools.common.mod(animationElapsedTime, animationDuration)
: gdjs.evtTools.common.clamp(
animationElapsedTime,
0,
animationDuration
)
);
}
return false;
}
/**
* Register a listener to frame changes.
*
* It's useful for custom objects as they don't drive this class themselves.
*
* @param callback Called each time {@link getCurrentFrame} changes.
*/
setOnFrameChangeCallback(callback: () => void): void {
this._onFrameChange = callback;
}
getAnimationIndex(): number {
return this._currentAnimation;
}
setAnimationIndex(newAnimation: number): boolean {
newAnimation = newAnimation || 0;
if (
newAnimation < this._animations.length &&
this._currentAnimation !== newAnimation &&
newAnimation >= 0
) {
this._currentAnimation = newAnimation;
this._currentFrameIndex = 0;
this._animationElapsedTime = 0;
this.invalidateFrame();
return true;
}
return false;
}
getAnimationName(): string {
if (this._currentAnimation >= this._animations.length) {
return '';
}
return this._animations[this._currentAnimation].name;
}
setAnimationName(newAnimationName: string): boolean {
if (!newAnimationName) {
return false;
}
for (let i = 0; i < this._animations.length; ++i) {
if (this._animations[i].name === newAnimationName) {
this.setAnimationIndex(i);
return true;
}
}
return false;
}
hasAnimationEnded(): boolean {
if (
this._currentAnimation >= this._animations.length ||
this._currentDirection >=
this._animations[this._currentAnimation].directions.length
) {
return true;
}
const direction = this._animations[this._currentAnimation].directions[
this._currentDirection
];
if (direction.loop) {
return false;
}
return (
this._currentFrameIndex === direction.frames.length - 1 &&
this._animationElapsedTime ===
direction.frames.length * direction.timeBetweenFrames
);
}
isAnimationPaused() {
return this._animationPaused;
}
pauseAnimation() {
this._animationPaused = true;
}
resumeAnimation() {
this._animationPaused = false;
}
getAnimationSpeedScale() {
return this._animationSpeedScale;
}
setAnimationSpeedScale(ratio: float): void {
this._animationSpeedScale = ratio;
}
/**
* Change the current frame displayed by the animation
* @param newFrameIndex The index of the frame to be displayed
*/
setAnimationFrameIndex(newFrameIndex: number): boolean {
if (
this._currentAnimation >= this._animations.length ||
this._currentDirection >=
this._animations[this._currentAnimation].directions.length
) {
return false;
}
const direction = this._animations[this._currentAnimation].directions[
this._currentDirection
];
if (
newFrameIndex >= 0 &&
newFrameIndex < direction.frames.length &&
newFrameIndex !== this._currentFrameIndex
) {
this._currentFrameIndex = newFrameIndex;
this._animationElapsedTime =
newFrameIndex * direction.timeBetweenFrames;
this.invalidateFrame();
return true;
}
return false;
}
/**
* Get the index of the current frame displayed by the animation
* @return newFrame The index of the frame being displayed
*/
getAnimationFrameIndex(): number {
return this._currentFrameIndex;
}
getAnimationElapsedTime(): float {
return this._animationElapsedTime;
}
setAnimationElapsedTime(time: float): boolean {
const direction = this._animations[this._currentAnimation].directions[
this._currentDirection
];
this._animationElapsedTime = gdjs.evtTools.common.clamp(
time,
0,
this.getAnimationDuration()
);
const oldFrame = this._currentFrameIndex;
this._currentFrameIndex = Math.min(
Math.floor(this._animationElapsedTime / direction.timeBetweenFrames),
direction.frames.length - 1
);
if (oldFrame !== this._currentFrameIndex) {
this.invalidateFrame();
return true;
}
return false;
}
getAnimationDuration(): float {
const direction = this._animations[this._currentAnimation].directions[
this._currentDirection
];
return direction.frames.length * direction.timeBetweenFrames;
}
getAnimationFrameCount(): number {
if (this._currentAnimation >= this._animations.length) {
return 0;
}
const currentAnimation = this._animations[this._currentAnimation];
if (this._currentDirection >= currentAnimation.directions.length) {
return 0;
}
return currentAnimation.directions[this._currentDirection].frames.length;
}
/**
* Change the angle (or direction index) of the object
* @param The new angle (or direction index) to be applied
* @deprecated
*/
setDirectionOrAngle(oldValue: float, newValue: float): number | null {
if (this._currentAnimation >= this._animations.length) {
return null;
}
const anim = this._animations[this._currentAnimation];
if (!anim.hasMultipleDirections) {
return oldValue === newValue ? null : newValue;
} else {
newValue = newValue | 0;
if (
newValue === this._currentDirection ||
newValue >= anim.directions.length ||
anim.directions[newValue].frames.length === 0
) {
return null;
}
this._currentDirection = newValue;
this._currentFrameIndex = 0;
this._animationElapsedTime = 0;
this.invalidateFrame();
return 0;
}
}
/**
* @deprecated
*/
getDirectionOrAngle(angle: float): float {
if (this._currentAnimation >= this._animations.length) {
return 0;
}
if (!this._animations[this._currentAnimation].hasMultipleDirections) {
return angle;
} else {
return this._currentDirection;
}
}
/**
* @deprecated
*/
getAngle(angle: float): float {
if (this._currentAnimation >= this._animations.length) {
return 0;
}
if (!this._animations[this._currentAnimation].hasMultipleDirections) {
return angle;
} else {
return this._currentDirection * 45;
}
}
/**
* @deprecated
*/
setAngle(oldAngle: float, angle: float): float | null {
if (this._currentAnimation >= this._animations.length) {
return null;
}
if (!this._animations[this._currentAnimation].hasMultipleDirections) {
if (oldAngle === angle) {
return null;
}
return angle;
} else {
angle = angle % 360;
if (angle < 0) {
angle += 360;
}
return this.setDirectionOrAngle(oldAngle, Math.round(angle / 45) % 8);
}
}
/**
* @deprecated
* Return true if animation has ended.
* Prefer using {@link hasAnimationEnded}. This method returns true as soon as
* the animation enters the last frame, not at the end of the last frame.
*/
hasAnimationEndedLegacy(): boolean {
if (
this._currentAnimation >= this._animations.length ||
this._currentDirection >=
this._animations[this._currentAnimation].directions.length
) {
return true;
}
const direction = this._animations[this._currentAnimation].directions[
this._currentDirection
];
if (direction.loop) {
return false;
}
return this._currentFrameIndex === direction.frames.length - 1;
}
}
}

View File

@@ -152,6 +152,14 @@ namespace gdjs {
this._pixiContainer.removeChild(layerPixiObject);
this._pixiContainer.addChildAt(layerPixiObject, index);
}
static getAnimationFrameTextureManager(
imageManager: gdjs.PixiImageManager
) {
return gdjs.SpriteRuntimeObjectPixiRenderer.getAnimationFrameTextureManager(
imageManager
);
}
}
// Register the class to let the engine use it.

View File

@@ -264,9 +264,10 @@ namespace gdjs {
// Draw custom point
if (showCustomPoints && object instanceof gdjs.SpriteRuntimeObject) {
if (!object._animationFrame) continue;
const animationFrame = object._animator.getCurrentFrame();
if (!animationFrame) continue;
for (const customPointName in object._animationFrame.points.items) {
for (const customPointName in animationFrame.points.items) {
let customPoint = object.getPointPosition(customPointName);
customPoint = layer.applyLayerTransformation(

View File

@@ -228,7 +228,7 @@ namespace gdjs {
useTransparentTexture,
forceBasicMaterial,
}: { useTransparentTexture: boolean; forceBasicMaterial: boolean }
) {
): THREE.Material {
const cacheKey = `${resourceName}|${useTransparentTexture ? 1 : 0}|${
forceBasicMaterial ? 1 : 0
}`;

View File

@@ -1,4 +1,7 @@
namespace gdjs {
export interface PixiImageManager {
_pixiAnimationFrameTextureManager: PixiAnimationFrameTextureManager;
}
/**
* The renderer for a gdjs.SpriteRuntimeObject using Pixi.js.
*/
@@ -19,9 +22,8 @@ namespace gdjs {
instanceContainer: gdjs.RuntimeInstanceContainer
) {
this._object = runtimeObject;
this._sprite = new PIXI.Sprite(
instanceContainer.getGame().getImageManager().getInvalidPIXITexture()
);
const imageManager = instanceContainer.getGame().getImageManager();
this._sprite = new PIXI.Sprite(imageManager.getInvalidPIXITexture());
const layer = instanceContainer.getLayer('');
if (layer) {
layer
@@ -54,22 +56,19 @@ namespace gdjs {
* Update the internal PIXI.Sprite position, angle...
*/
_updatePIXISprite() {
if (this._object._animationFrame !== null) {
const animationFrame = this._object._animator.getCurrentFrame();
if (animationFrame !== null) {
this._sprite.anchor.x =
this._object._animationFrame.center.x /
this._sprite.texture.frame.width;
animationFrame.center.x / this._sprite.texture.frame.width;
this._sprite.anchor.y =
this._object._animationFrame.center.y /
this._sprite.texture.frame.height;
animationFrame.center.y / this._sprite.texture.frame.height;
this._sprite.position.x =
this._object.x +
(this._object._animationFrame.center.x -
this._object._animationFrame.origin.x) *
(animationFrame.center.x - animationFrame.origin.x) *
Math.abs(this._object._scaleX);
this._sprite.position.y =
this._object.y +
(this._object._animationFrame.center.y -
this._object._animationFrame.origin.y) *
(animationFrame.center.y - animationFrame.origin.y) *
Math.abs(this._object._scaleY);
this._sprite.rotation = gdjs.toRad(this._object.angle);
this._sprite.visible = !this._object.hidden;
@@ -100,7 +99,7 @@ namespace gdjs {
/**
* Update the internal texture of the PIXI sprite.
*/
updateFrame(animationFrame): void {
updateFrame(animationFrame: gdjs.SpriteAnimationFrame<PIXI.Texture>): void {
this._spriteDirty = true;
this._sprite.texture = animationFrame.texture;
}
@@ -110,8 +109,9 @@ namespace gdjs {
}
updateX(): void {
const animationFrame = this._object
._animationFrame as SpriteAnimationFrame;
const animationFrame = this._object._animator.getCurrentFrame() as SpriteAnimationFrame<
PIXI.Texture
>;
this._sprite.position.x =
this._object.x +
(animationFrame.center.x - animationFrame.origin.x) *
@@ -119,8 +119,9 @@ namespace gdjs {
}
updateY(): void {
const animationFrame = this._object
._animationFrame as SpriteAnimationFrame;
const animationFrame = this._object._animator.getCurrentFrame() as SpriteAnimationFrame<
PIXI.Texture
>;
this._sprite.position.y =
this._object.y +
(animationFrame.center.y - animationFrame.origin.y) *
@@ -184,18 +185,35 @@ namespace gdjs {
return this._sprite.texture.frame.height;
}
static getAnimationFrame(
imageManager: gdjs.PixiImageManager,
imageName: string
) {
return imageManager.getPIXITexture(imageName);
static getAnimationFrameTextureManager(
imageManager: gdjs.PixiImageManager
): PixiAnimationFrameTextureManager {
if (!imageManager._pixiAnimationFrameTextureManager) {
imageManager._pixiAnimationFrameTextureManager = new PixiAnimationFrameTextureManager(
imageManager
);
}
return imageManager._pixiAnimationFrameTextureManager;
}
}
class PixiAnimationFrameTextureManager
implements gdjs.AnimationFrameTextureManager<PIXI.Texture> {
private _imageManager: gdjs.PixiImageManager;
constructor(imageManager: gdjs.PixiImageManager) {
this._imageManager = imageManager;
}
static getAnimationFrameWidth(pixiTexture: PIXI.Texture) {
getAnimationFrameTexture(imageName: string) {
return this._imageManager.getPIXITexture(imageName);
}
getAnimationFrameWidth(pixiTexture: PIXI.Texture) {
return pixiTexture.width;
}
static getAnimationFrameHeight(pixiTexture: PIXI.Texture) {
getAnimationFrameHeight(pixiTexture: PIXI.Texture) {
return pixiTexture.height;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,7 @@ module.exports = function (config) {
'./newIDE/app/resources/GDJS/Runtime/variablescontainer.js',
'./newIDE/app/resources/GDJS/Runtime/oncetriggers.js',
'./newIDE/app/resources/GDJS/Runtime/runtimebehavior.js',
'./newIDE/app/resources/GDJS/Runtime/SpriteAnimator.js',
'./newIDE/app/resources/GDJS/Runtime/spriteruntimeobject.js',
'./newIDE/app/resources/GDJS/Runtime/CustomRuntimeObject.js',
'./newIDE/app/resources/GDJS/Runtime/CustomRuntimeObject2D.js',

View File

@@ -202,7 +202,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
expect(object._animationElapsedTime).to.be(
expect(object.getAnimationElapsedTime()).to.be(
stepDurationInMilliseconds / 1000
);
@@ -232,7 +232,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
}
// Almost at the animation end.
expect(object._animationElapsedTime).to.be.within(
expect(object.getAnimationElapsedTime()).to.be.within(
3 * firstAnimationTimeBetweenFrames - stepDurationInMilliseconds / 1000,
3 * firstAnimationTimeBetweenFrames - 0.001
);
@@ -241,7 +241,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
// The animation ended.
expect(object._animationElapsedTime).to.be(
expect(object.getAnimationElapsedTime()).to.be(
3 * firstAnimationTimeBetweenFrames
);
expect(object.getAnimationFrame()).to.be(2);
@@ -249,7 +249,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
// No change.
expect(object._animationElapsedTime).to.be(
expect(object.getAnimationElapsedTime()).to.be(
3 * firstAnimationTimeBetweenFrames
);
expect(object.getAnimationFrame()).to.be(2);
@@ -272,7 +272,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
}
// Almost at the animation end.
expect(object._animationElapsedTime).to.be.within(
expect(object.getAnimationElapsedTime()).to.be.within(
3 * firstAnimationTimeBetweenFrames - stepDurationInMilliseconds / 1000,
3 * firstAnimationTimeBetweenFrames - 0.001
);
@@ -281,7 +281,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
// The animation looped to the beginning.
expect(object._animationElapsedTime).to.within(0.01, 0.02);
expect(object.getAnimationElapsedTime()).to.within(0.01, 0.02);
expect(object.getAnimationFrame()).to.be(0);
});
});
@@ -302,7 +302,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
expect(object._animationElapsedTime).to.be(
expect(object.getAnimationElapsedTime()).to.be(
2 * firstAnimationTimeBetweenFrames - stepDurationInMilliseconds / 1000
);
expect(object.getAnimationFrame()).to.be(1);
@@ -326,7 +326,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
}
// Almost at the animation beginning.
expect(object._animationElapsedTime).to.be.within(
expect(object.getAnimationElapsedTime()).to.be.within(
0.001,
stepDurationInMilliseconds / 60
);
@@ -335,13 +335,13 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
// Reached the animation beginning.
expect(object._animationElapsedTime).to.be(0);
expect(object.getAnimationElapsedTime()).to.be(0);
expect(object.getAnimationFrame()).to.be(0);
runtimeScene.renderAndStep(stepDurationInMilliseconds);
// No change.
expect(object._animationElapsedTime).to.be(0);
expect(object.getAnimationElapsedTime()).to.be(0);
expect(object.getAnimationFrame()).to.be(0);
});
@@ -364,7 +364,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
}
// Almost at the animation beginning.
expect(object._animationElapsedTime).to.be.within(
expect(object.getAnimationElapsedTime()).to.be.within(
0.001,
stepDurationInMilliseconds / 60
);
@@ -373,7 +373,7 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
// Reached the animation beginning.
expect(object._animationElapsedTime).to.be.within(
expect(object.getAnimationElapsedTime()).to.be.within(
3 * firstAnimationTimeBetweenFrames - 0.004,
3 * firstAnimationTimeBetweenFrames - 0.003
);
@@ -395,16 +395,16 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
expect(object._animationElapsedTime).to.not.be(0);
expect(object.getAnimationElapsedTime()).to.not.be(0);
object.setAnimation(1);
expect(object.getAnimationFrame()).to.be(0);
expect(object._animationElapsedTime).to.be(0);
expect(object.getAnimationElapsedTime()).to.be(0);
runtimeScene.renderAndStep(stepDurationInMilliseconds);
expect(object._animationElapsedTime).to.not.be(0);
expect(object.getAnimationElapsedTime()).to.not.be(0);
});
it('should reset the elapsed time on a frame when changing animation frame', () => {
@@ -420,18 +420,18 @@ describe('gdjs.SpriteRuntimeObject', () => {
runtimeScene.renderAndStep(stepDurationInMilliseconds);
expect(object._animationElapsedTime).to.not.be(0);
expect(object.getAnimationElapsedTime()).to.not.be(0);
object.setAnimationFrame(2);
expect(object.getAnimationFrame()).to.be(2);
expect(object._animationElapsedTime).to.be(
expect(object.getAnimationElapsedTime()).to.be(
2 * firstAnimationTimeBetweenFrames
);
runtimeScene.renderAndStep(stepDurationInMilliseconds);
expect(object._animationElapsedTime).to.not.be(0);
expect(object.getAnimationElapsedTime()).to.not.be(0);
});
});
});

View File

@@ -772,6 +772,8 @@ interface CustomObjectConfiguration {
[Value] MapStringPropertyDescriptor GetInitialInstanceProperties([Const, Ref] InitialInstance instance, [Ref] Project project, [Ref] Layout scene);
boolean UpdateInitialInstanceProperty([Ref] InitialInstance instance, [Const] DOMString name, [Const] DOMString value, [Ref] Project project, [Ref] Layout scene);
[Ref] SpriteAnimationList GetAnimations();
};
CustomObjectConfiguration implements ObjectConfiguration;
@@ -2790,6 +2792,8 @@ interface EventsBasedObject {
[Ref] EventsBasedObject MarkAsRenderedIn3D(boolean isRenderedIn3D);
boolean IsRenderedIn3D();
[Ref] EventsBasedObject MarkAsAnimatable(boolean isAnimatable);
boolean IsAnimatable();
[Ref] EventsBasedObject MarkAsTextContainer(boolean isTextContainer);
boolean IsTextContainer();
@@ -3171,8 +3175,8 @@ interface Animation {
void SetUseMultipleDirections(boolean enable);
};
interface SpriteObject {
void SpriteObject();
interface SpriteAnimationList {
void SpriteAnimationList();
void AddAnimation([Const, Ref] Animation animation);
[Ref] Animation GetAnimation(unsigned long index);
@@ -3183,12 +3187,18 @@ interface SpriteObject {
void SwapAnimations(unsigned long first, unsigned long second);
void MoveAnimation(unsigned long oldIndex, unsigned long newIndex);
void SetUpdateIfNotVisible(boolean updateIfNotVisible);
boolean GetUpdateIfNotVisible();
boolean AdaptCollisionMaskAutomatically();
void SetAdaptCollisionMaskAutomatically(boolean adaptCollisionMaskAutomatically);
};
interface SpriteObject {
void SpriteObject();
[Ref] SpriteAnimationList GetAnimations();
void SetUpdateIfNotVisible(boolean updateIfNotVisible);
boolean GetUpdateIfNotVisible();
};
SpriteObject implements ObjectConfiguration;
interface Model3DAnimation {

View File

@@ -1522,7 +1522,7 @@ describe('libGD.js', function () {
anim1.setDirectionsCount(1);
anim1.getDirection(0).addSprite(sprite1);
gd.castObject(obj.getConfiguration(), gd.SpriteObject).addAnimation(
gd.castObject(obj.getConfiguration(), gd.SpriteObject).getAnimations().addAnimation(
anim1
);
@@ -1589,7 +1589,7 @@ describe('libGD.js', function () {
const animation = new gd.Animation();
animation.setDirectionsCount(1);
animation.getDirection(0).addSprite(sprite1);
spriteConfiguration.addAnimation(animation);
spriteConfiguration.getAnimations().addAnimation(animation);
let worker = extend(new gd.ArbitraryResourceWorkerJS(project.getResourcesManager()), {
exposeImage: function (image) {
@@ -1620,7 +1620,7 @@ describe('libGD.js', function () {
animation.getDirection(0).addSprite(sprite1);
animation.getDirection(0).addSprite(sprite2);
animation.getDirection(0).addSprite(sprite1);
spriteObject.addAnimation(animation);
spriteObject.getAnimations().addAnimation(animation);
const spriteObject2 = new gd.SpriteObject();
const animation2 = new gd.Animation();
@@ -1628,7 +1628,7 @@ describe('libGD.js', function () {
animation2.getDirection(0).addSprite(sprite1);
animation2.getDirection(0).addSprite(sprite3);
animation2.getDirection(0).addSprite(sprite1);
spriteObject2.addAnimation(animation2);
spriteObject2.getAnimations().addAnimation(animation2);
const resourcesInUse = new gd.ResourcesInUseHelper();
@@ -1681,7 +1681,7 @@ describe('libGD.js', function () {
animation.getDirection(0).addSprite(sprite1);
animation.getDirection(0).addSprite(sprite2);
animation.getDirection(0).addSprite(sprite1);
spriteObject.addAnimation(animation);
spriteObject.getAnimations().addAnimation(animation);
const object2 = project.insertNewObject(
project,
@@ -1697,7 +1697,7 @@ describe('libGD.js', function () {
animation2.getDirection(0).addSprite(sprite1);
animation2.getDirection(0).addSprite(sprite3);
animation2.getDirection(0).addSprite(sprite1);
spriteObject2.addAnimation(animation2);
spriteObject2.getAnimations().addAnimation(animation2);
{
const objectsCollector = new gd.ObjectsUsingResourceCollector(project.getResourcesManager(), 'Image1');
@@ -3047,17 +3047,19 @@ describe('libGD.js', function () {
});
it('can have animations', function () {
let obj = new gd.SpriteObject();
obj.addAnimation(new gd.Animation());
obj.addAnimation(new gd.Animation());
expect(obj.getAnimationsCount()).toBe(2);
obj.removeAnimation(1);
expect(obj.getAnimationsCount()).toBe(1);
const obj = new gd.SpriteObject();
const animations = obj.getAnimations();
animations.addAnimation(new gd.Animation());
animations.addAnimation(new gd.Animation());
expect(animations.getAnimationsCount()).toBe(2);
animations.removeAnimation(1);
expect(animations.getAnimationsCount()).toBe(1);
});
it('can swap animations', function () {
let obj = new gd.SpriteObject();
obj.removeAllAnimations();
const obj = new gd.SpriteObject();
const animations = obj.getAnimations();
animations.removeAllAnimations();
let anim1 = new gd.Animation();
let anim2 = new gd.Animation();
let sprite1 = new gd.Sprite();
@@ -3071,14 +3073,14 @@ describe('libGD.js', function () {
anim1.getDirection(0).addSprite(sprite1);
anim2.getDirection(0).addSprite(sprite2);
obj.addAnimation(anim1);
obj.addAnimation(anim2);
animations.addAnimation(anim1);
animations.addAnimation(anim2);
expect(
obj.getAnimation(0).getDirection(0).getSprite(0).getImageName()
animations.getAnimation(0).getDirection(0).getSprite(0).getImageName()
).toBe('image1');
obj.swapAnimations(0, 1);
animations.swapAnimations(0, 1);
expect(
obj.getAnimation(0).getDirection(0).getSprite(0).getImageName()
animations.getAnimation(0).getDirection(0).getSprite(0).getImageName()
).toBe('image2');
});

View File

@@ -1125,8 +1125,7 @@ describe('MetadataDeclarationHelper', () => {
.getEventsBasedObjects()
.insertNew('MyObject', 0);
eventsBasedObject.markAsRenderedIn3D(true);
// TODO Uncomment when the feature is done.
// eventsBasedObject.markAsAnimatable(true);
eventsBasedObject.markAsAnimatable(true);
eventsBasedObject.markAsTextContainer(true);
const objectMethodMangledNames = new gd.MapStringString();
@@ -1151,8 +1150,7 @@ describe('MetadataDeclarationHelper', () => {
"FlippableCapability::FlippableBehavior",
// No effect nor opacity capabilities for 3D objects.
"Scene3D::Base3DBehavior",
// TODO Uncomment when the feature is done.
// "AnimatableCapability::AnimatableBehavior",
"AnimatableCapability::AnimatableBehavior",
"TextContainerCapability::TextContainerBehavior",
]);

View File

@@ -683,6 +683,7 @@ export class CustomObjectConfiguration extends ObjectConfiguration {
updateProperty(name: string, value: string): boolean;
getInitialInstanceProperties(instance: InitialInstance, project: Project, scene: Layout): MapStringPropertyDescriptor;
updateInitialInstanceProperty(instance: InitialInstance, name: string, value: string, project: Project, scene: Layout): boolean;
getAnimations(): SpriteAnimationList;
}
export class Layout extends EmscriptenObject {
@@ -2148,6 +2149,8 @@ export class EventsBasedObject extends AbstractEventsBasedEntity {
getDefaultName(): string;
markAsRenderedIn3D(isRenderedIn3D: boolean): EventsBasedObject;
isRenderedIn3D(): boolean;
markAsAnimatable(isAnimatable: boolean): EventsBasedObject;
isAnimatable(): boolean;
markAsTextContainer(isTextContainer: boolean): EventsBasedObject;
isTextContainer(): boolean;
static getPropertyActionName(propertyName: string): string;
@@ -2462,7 +2465,7 @@ export class Animation extends EmscriptenObject {
setUseMultipleDirections(enable: boolean): void;
}
export class SpriteObject extends ObjectConfiguration {
export class SpriteAnimationList extends EmscriptenObject {
constructor();
addAnimation(animation: Animation): void;
getAnimation(index: number): Animation;
@@ -2472,12 +2475,17 @@ export class SpriteObject extends ObjectConfiguration {
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
setUpdateIfNotVisible(updateIfNotVisible: boolean): void;
getUpdateIfNotVisible(): boolean;
adaptCollisionMaskAutomatically(): boolean;
setAdaptCollisionMaskAutomatically(adaptCollisionMaskAutomatically: boolean): void;
}
export class SpriteObject extends ObjectConfiguration {
constructor();
getAnimations(): SpriteAnimationList;
setUpdateIfNotVisible(updateIfNotVisible: boolean): void;
getUpdateIfNotVisible(): boolean;
}
export class Model3DAnimation extends EmscriptenObject {
constructor();
setName(name: string): void;

View File

@@ -6,6 +6,7 @@ declare class gdCustomObjectConfiguration extends gdObjectConfiguration {
updateProperty(name: string, value: string): boolean;
getInitialInstanceProperties(instance: gdInitialInstance, project: gdProject, scene: gdLayout): gdMapStringPropertyDescriptor;
updateInitialInstanceProperty(instance: gdInitialInstance, name: string, value: string, project: gdProject, scene: gdLayout): boolean;
getAnimations(): gdSpriteAnimationList;
delete(): void;
ptr: number;
};

View File

@@ -11,6 +11,8 @@ declare class gdEventsBasedObject extends gdAbstractEventsBasedEntity {
getDefaultName(): string;
markAsRenderedIn3D(isRenderedIn3D: boolean): gdEventsBasedObject;
isRenderedIn3D(): boolean;
markAsAnimatable(isAnimatable: boolean): gdEventsBasedObject;
isAnimatable(): boolean;
markAsTextContainer(isTextContainer: boolean): gdEventsBasedObject;
isTextContainer(): boolean;
static getPropertyActionName(propertyName: string): string;

View File

@@ -0,0 +1,16 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdSpriteAnimationList {
constructor(): void;
addAnimation(animation: gdAnimation): void;
getAnimation(index: number): gdAnimation;
getAnimationsCount(): number;
removeAnimation(index: number): void;
removeAllAnimations(): void;
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
adaptCollisionMaskAutomatically(): boolean;
setAdaptCollisionMaskAutomatically(adaptCollisionMaskAutomatically: boolean): void;
delete(): void;
ptr: number;
};

View File

@@ -1,18 +1,9 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdSpriteObject extends gdObjectConfiguration {
constructor(): void;
addAnimation(animation: gdAnimation): void;
getAnimation(index: number): gdAnimation;
getAnimationsCount(): number;
removeAnimation(index: number): void;
removeAllAnimations(): void;
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
getAnimations(): gdSpriteAnimationList;
setUpdateIfNotVisible(updateIfNotVisible: boolean): void;
getUpdateIfNotVisible(): boolean;
adaptCollisionMaskAutomatically(): boolean;
setAdaptCollisionMaskAutomatically(adaptCollisionMaskAutomatically: boolean): void;
delete(): void;
ptr: number;
};

View File

@@ -231,6 +231,7 @@ declare class libGDevelop {
Sprite: Class<gdSprite>;
Direction: Class<gdDirection>;
Animation: Class<gdAnimation>;
SpriteAnimationList: Class<gdSpriteAnimationList>;
SpriteObject: Class<gdSpriteObject>;
Model3DAnimation: Class<gdModel3DAnimation>;
Model3DObjectConfiguration: Class<gdModel3DObjectConfiguration>;

View File

@@ -2,6 +2,7 @@
import GDevelopJsInitializerDecorator from '../src/stories/GDevelopJsInitializerDecorator';
import i18nProviderDecorator from '../src/stories/I18nProviderDecorator';
import BrowserDropDownMenuDisablerDecorator from '../src/stories/BrowserDropDownMenuDisablerDecorator';
import themeDecorator from '../src/stories/ThemeDecorator';
import '../src/UI/icomoon-font.css'; // Styles for Icomoon font.
import './app-level-styling.css';
@@ -35,6 +36,7 @@ export const parameters = {
};
export const decorators = [
themeDecorator,
GDevelopJsInitializerDecorator,
i18nProviderDecorator,
BrowserDropDownMenuDisablerDecorator

View File

@@ -190,6 +190,14 @@
"color": {
"value": "#000"
}
},
"hot": {
"background-color": {
"value": "#FF5E3B"
},
"color": {
"value": "#1D1D26"
}
}
},
"toolbar-separator": {

View File

@@ -18,7 +18,7 @@ type AnnouncementsFeedState = {|
announcements: ?(Announcement[]),
promotions: ?(Promotion[]),
error: ?Error,
fetchAnnouncementsAndPromotions: () => void,
fetchAnnouncementsAndPromotions: () => Promise<void>,
|};
export const AnnouncementsFeedContext = React.createContext<AnnouncementsFeedState>(
@@ -26,7 +26,7 @@ export const AnnouncementsFeedContext = React.createContext<AnnouncementsFeedSta
announcements: null,
promotions: null,
error: null,
fetchAnnouncementsAndPromotions: () => {},
fetchAnnouncementsAndPromotions: async () => {},
}
);
@@ -44,37 +44,33 @@ export const AnnouncementsFeedStateProvider = ({
const [promotions, setPromotions] = React.useState<?(Promotion[])>(null);
const isLoading = React.useRef<boolean>(false);
const fetchAnnouncementsAndPromotions = React.useCallback(() => {
const fetchAnnouncementsAndPromotions = React.useCallback(async () => {
if (isLoading.current) return;
(async () => {
setError(null);
isLoading.current = true;
setError(null);
isLoading.current = true;
try {
const [fetchedAnnouncements, fetchedPromotions] = await Promise.all([
listAllAnnouncements(),
listAllPromotions(),
]);
try {
const [fetchedAnnouncements, fetchedPromotions] = await Promise.all([
listAllAnnouncements(),
listAllPromotions(),
]);
// Logic to remove once promotions are displayed to enough users.
// For now, we filter out promotions from the announcements.
const filteredAnnouncements = fetchedAnnouncements.filter(
announcement =>
!fetchedPromotions.find(
promotion => promotion.id === announcement.id
)
);
// Logic to remove once promotions are displayed to enough users.
// For now, we filter out promotions from the announcements.
const filteredAnnouncements = fetchedAnnouncements.filter(
announcement =>
!fetchedPromotions.find(promotion => promotion.id === announcement.id)
);
setAnnouncements(filteredAnnouncements);
setPromotions(fetchedPromotions);
} catch (error) {
console.error(`Unable to load the announcements from the api:`, error);
setError(error);
}
setAnnouncements(filteredAnnouncements);
setPromotions(fetchedPromotions);
} catch (error) {
console.error(`Unable to load the announcements from the api:`, error);
setError(error);
}
isLoading.current = false;
})();
isLoading.current = false;
}, []);
// Preload the announcements and promotions when the app loads.

View File

@@ -26,6 +26,8 @@ import {
PrivateGameTemplateTile,
} from './ShopTiles';
import { useDebounce } from '../Utils/UseDebounce';
import PromotionsSlideshow from '../Promotions/PromotionsSlideshow';
import { ColumnStackLayout } from '../UI/Layout';
const cellSpacing = 2;
@@ -157,6 +159,7 @@ type Props = {|
onCategorySelection: string => void,
openedShopCategory: string | null,
hideGameTemplates?: boolean,
displayPromotions?: boolean,
|};
export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
@@ -171,6 +174,7 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
onCategorySelection,
openedShopCategory,
hideGameTemplates,
displayPromotions,
}: Props,
ref
) => {
@@ -383,6 +387,15 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
</GridList>
</>
)}
{displayPromotions ? (
<ColumnStackLayout>
<Text size="block-title">
<Trans>Promotions</Trans>
</Text>
<PromotionsSlideshow />
</ColumnStackLayout>
) : null}
{allBundleTiles.length ? (
<>
<Column>

View File

@@ -59,6 +59,7 @@ import { PrivateGameTemplateStoreContext } from './PrivateGameTemplates/PrivateG
type Props = {|
hideGameTemplates?: boolean, // TODO: if we add more options, use an array instead.
displayPromotions?: boolean,
onOpenPrivateGameTemplateListingData?: (
privateGameTemplateListingData: PrivateGameTemplateListingData
) => void,
@@ -94,7 +95,14 @@ const identifyAssetPackKind = ({
};
export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
({ hideGameTemplates, onOpenPrivateGameTemplateListingData }: Props, ref) => {
(
{
hideGameTemplates,
displayPromotions,
onOpenPrivateGameTemplateListingData,
}: Props,
ref
) => {
const {
assetShortHeadersSearchResults,
publicAssetPacksSearchResults,
@@ -737,6 +745,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
onCategorySelection={selectShopCategory}
openedShopCategory={openedShopCategory}
hideGameTemplates={hideGameTemplates}
displayPromotions={displayPromotions}
/>
) : (
<PlaceholderLoader />

View File

@@ -0,0 +1,84 @@
// @flow
import * as React from 'react';
import {
getUserCommentQualityRatingsLeaderboards,
type UserLeaderboard,
} from '../Utils/GDevelopServices/User';
import {
getGameCommentQualityRatingsLeaderboards,
type GameLeaderboard,
} from '../Utils/GDevelopServices/Game';
type CommunityLeaderboardsState = {|
userLeaderboards: Array<UserLeaderboard> | null,
gameLeaderboards: Array<GameLeaderboard> | null,
error: ?Error,
fetchCommunityLeaderboards: () => Promise<void>,
|};
export const CommunityLeaderboardsContext = React.createContext<CommunityLeaderboardsState>(
{
userLeaderboards: null,
gameLeaderboards: null,
error: null,
fetchCommunityLeaderboards: async () => {},
}
);
type CommunityLeaderboardsStateProviderProps = {|
children: React.Node,
|};
export const CommunityLeaderboardsStateProvider = ({
children,
}: CommunityLeaderboardsStateProviderProps) => {
const [userLeaderboards, setUserLeaderboards] = React.useState<
Array<UserLeaderboard>
>([]);
const [gameLeaderboards, setGameLeaderboards] = React.useState<
Array<GameLeaderboard>
>([]);
const [error, setError] = React.useState<?Error>(null);
const isLoading = React.useRef<boolean>(false);
const fetchCommunityLeaderboards = React.useCallback(async () => {
if (isLoading.current) return;
setError(null);
isLoading.current = true;
try {
const [userLeaderboards, gameLeaderboards] = await Promise.all([
getUserCommentQualityRatingsLeaderboards(),
getGameCommentQualityRatingsLeaderboards(),
]);
setUserLeaderboards(userLeaderboards);
setGameLeaderboards(gameLeaderboards);
} catch (error) {
console.error(
`Unable to load the community leaderboards from the api:`,
error
);
setError(error);
}
isLoading.current = false;
}, []);
const communityLeaderboardsState = React.useMemo(
() => ({
gameLeaderboards,
userLeaderboards,
error,
fetchCommunityLeaderboards,
}),
[gameLeaderboards, userLeaderboards, error, fetchCommunityLeaderboards]
);
return (
<CommunityLeaderboardsContext.Provider value={communityLeaderboardsState}>
{children}
</CommunityLeaderboardsContext.Provider>
);
};

View File

@@ -0,0 +1,188 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import {
type GameLeaderboard,
type GameLeaderboardEntry,
getPublicGameUrl,
} from '../Utils/GDevelopServices/Game';
import { selectMessageByLocale } from '../Utils/i18n/MessageByLocale';
import Text from '../UI/Text';
import { ColumnStackLayout } from '../UI/Layout';
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
import Paper from '../UI/Paper';
import Annotation from '../UI/CustomSvgIcons/Annotation';
import { textEllipsisStyle } from '../UI/TextEllipsis';
import ButtonBase from '@material-ui/core/ButtonBase';
import Skeleton from '@material-ui/lab/Skeleton';
import Window from '../Utils/Window';
type Props = {|
gameLeaderboard: GameLeaderboard | null,
displayEntriesCount: number,
|};
const thumbnailWidth = 100;
const thumbnailHeight = 28;
const styles = {
avatar: {
width: 16,
height: 16,
},
paper: {
flex: 1,
},
entryRow: {
borderBottom: '1px solid black',
},
rankColumn: {
width: 30,
textAlign: 'center',
},
gameColumn: {},
scoreColumn: {
width: 30,
textAlign: 'center',
},
gameThumbnailContainer: {
width: thumbnailWidth,
height: thumbnailHeight,
display: 'inline-block',
overflow: 'hidden',
verticalAlign: 'middle',
marginRight: 5,
},
gameThumbnail: {
width: thumbnailWidth,
height: thumbnailHeight,
objectFit: 'cover',
},
fullWidthButtonSupportingEllipsisInATable: {
justifyContent: 'flex-start',
height: 32,
// This is required for a text to have ellipsis in a table cell.
width: 0,
minWidth: '100%',
},
};
const loadingEntry: GameLeaderboardEntry = {
publicGame: null,
count: null,
};
export const GameFeedbackLeaderboard = ({
gameLeaderboard,
displayEntriesCount,
}: Props) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<I18n>
{({ i18n }) => (
<Paper background="medium" style={styles.paper}>
<ColumnStackLayout noMargin>
<Text size="sub-title" align="center">
{gameLeaderboard
? selectMessageByLocale(
i18n,
gameLeaderboard.displayNameByLocale
)
: '-'}
</Text>
<table>
<thead>
<tr>
<th style={styles.rankColumn}>
<Text size="body2" color="secondary">
#
</Text>
</th>
<th style={styles.gameColumn}>
<Text size="body2" color="secondary" align="left">
<Trans>Game</Trans>
</Text>
</th>
<th style={styles.scoreColumn}>
<Annotation />
</th>
</tr>
</thead>
<tbody>
{(gameLeaderboard
? gameLeaderboard.topGameCommentQualityRatings
: new Array(displayEntriesCount).fill(loadingEntry)
)
.slice(0, displayEntriesCount)
.map((entry, index) => {
const publicGameUrl = getPublicGameUrl(entry.publicGame);
const title = entry.publicGame
? entry.publicGame.gameName
: '';
return (
<tr
key={index}
style={{
...styles.entryRow,
borderColor: gdevelopTheme.toolbar.separatorColor,
}}
>
<td style={styles.rankColumn}>
<Text>{index + 1}</Text>
</td>
<td style={styles.gameColumn}>
{entry.publicGame ? (
<ButtonBase
onClick={
publicGameUrl
? () => Window.openExternalURL(publicGameUrl)
: undefined
}
style={
styles.fullWidthButtonSupportingEllipsisInATable
}
>
<Text
style={textEllipsisStyle}
noMargin
tooltip={title}
>
{entry.publicGame &&
entry.publicGame.thumbnailUrl ? (
<div style={styles.gameThumbnailContainer}>
<img
src={entry.publicGame.thumbnailUrl}
style={styles.gameThumbnail}
alt={title}
title={title}
/>
</div>
) : null}
{title}
</Text>
</ButtonBase>
) : (
<Skeleton
variant="rect"
width={thumbnailWidth}
height={thumbnailHeight}
/>
)}
</td>
<td style={styles.scoreColumn}>
<Text>{entry.count ? entry.count : '-'}</Text>
</td>
</tr>
);
})}
</tbody>
</table>
</ColumnStackLayout>
</Paper>
)}
</I18n>
);
};

View File

@@ -0,0 +1,123 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import {
type UserLeaderboard,
type UserLeaderboardEntry,
} from '../Utils/GDevelopServices/User';
import { selectMessageByLocale } from '../Utils/i18n/MessageByLocale';
import Text from '../UI/Text';
import { ColumnStackLayout } from '../UI/Layout';
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
import Paper from '../UI/Paper';
import Annotation from '../UI/CustomSvgIcons/Annotation';
import { UserPublicProfileTextWithAvatar } from '../UI/User/UserPublicProfileTextWithAvatar';
type Props = {|
userLeaderboard: UserLeaderboard | null,
displayEntriesCount: number,
|};
const styles = {
avatar: {
width: 16,
height: 16,
},
paper: {
flex: 1,
},
entryRow: {
borderBottom: '1px solid black',
},
rankColumn: {
width: 30,
textAlign: 'center',
},
playerColumn: {},
scoreColumn: {
width: 30,
textAlign: 'center',
},
};
const loadingEntry: UserLeaderboardEntry = {
userPublicProfile: null,
count: null,
};
export const UserFeedbackLeaderboard = ({
userLeaderboard,
displayEntriesCount,
}: Props) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<I18n>
{({ i18n }) => (
<Paper background="medium" style={styles.paper}>
<ColumnStackLayout noMargin>
<Text size="sub-title" align="center">
{userLeaderboard
? selectMessageByLocale(
i18n,
userLeaderboard.displayNameByLocale
)
: '-'}
</Text>
<table>
<thead>
<tr>
<th style={styles.rankColumn}>
<Text size="body2" color="secondary">
#
</Text>
</th>
<th style={styles.playerColumn}>
<Text size="body2" color="secondary" align="left">
<Trans>Player</Trans>
</Text>
</th>
<th style={styles.scoreColumn}>
<Annotation />
</th>
</tr>
</thead>
<tbody>
{(userLeaderboard
? userLeaderboard.topUserCommentQualityRatings
: new Array(displayEntriesCount).fill(loadingEntry)
)
.slice(0, displayEntriesCount)
.map((entry, index) => (
<tr
key={index}
style={{
...styles.entryRow,
borderColor: gdevelopTheme.toolbar.separatorColor,
}}
>
<td style={styles.rankColumn}>
<Text>{index + 1}</Text>
</td>
<td style={styles.playerColumn}>
<UserPublicProfileTextWithAvatar
avatarSize={20}
user={entry.userPublicProfile}
expand
/>
</td>
<td style={styles.scoreColumn}>
<Text>{entry.count ? entry.count : '-'}</Text>
</td>
</tr>
))}
</tbody>
</table>
</ColumnStackLayout>
</Paper>
)}
</I18n>
);
};

View File

@@ -6,7 +6,6 @@ import { AutoSizer, Table, Column } from 'react-virtualized';
import flatMap from 'lodash/flatMap';
import { type ProfilerMeasuresSection } from '..';
import IconButton from '../../UI/IconButton';
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight';
import ChevronArrowBottom from '../../UI/CustomSvgIcons/ChevronArrowBottom';
@@ -34,7 +33,6 @@ type ProfilerRowData = {|
const MeasuresTable = (props: Props) => {
const [collapsedPaths, setCollapsedPaths] = React.useState({});
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const convertToDataRows = (
name: string,
@@ -134,7 +132,7 @@ const MeasuresTable = (props: Props) => {
<Table
headerHeight={30}
height={height}
className={`gd-table ${gdevelopTheme.tableRootClassName}`}
className={`gd-table`}
headerClassName={'tableHeaderColumn'}
rowCount={dataRows.length}
rowGetter={({ index }) => dataRows[index]}

View File

@@ -106,6 +106,14 @@ export default function EventsBasedObjectEditor({
onChange();
}}
/>
<Checkbox
label={<Trans>Has animations</Trans>}
checked={eventsBasedObject.isAnimatable()}
onCheck={(e, checked) => {
eventsBasedObject.markAsAnimatable(checked);
onChange();
}}
/>
<Checkbox
label={<Trans>Contains text</Trans>}
checked={eventsBasedObject.isTextContainer()}

View File

@@ -47,10 +47,10 @@ import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndD
import { makeDropTarget } from '../../UI/DragAndDrop/DropTarget';
import { AutoScroll, DropContainer } from './DropContainer';
import { isDescendant, type MoveFunctionArguments } from './helpers';
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
import { dataObjectToProps } from '../../Utils/HTMLDataset';
import useForceUpdate from '../../Utils/UseForceUpdate';
import { useLongTouch } from '../../Utils/UseLongTouch';
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
const gd: libGDevelop = global.gd;
const eventsSheetEventsDnDType = 'events-sheet-events-dnd-type';
@@ -235,7 +235,7 @@ const SortableTree = ({ className, ...otherProps }) => {
return (
<SortableTreeWithoutDndContext
className={`${eventsTree} ${
gdevelopTheme.eventsSheetRootClassName
gdevelopTheme.palette.type === 'light' ? 'light-theme' : 'dark-theme'
} ${className}`}
{...otherProps}
/>

View File

@@ -137,7 +137,7 @@
background-image: url('./unfold.png');
background-position: 11px 3px !important;
}
.gd-events-sheet .DefaultLightTheme .rst__expandButton {
.gd-events-sheet .light-theme .rst__expandButton {
background-image: url('./unfoldDark.png');
}
@@ -145,7 +145,7 @@
background-image: url('./fold.png');
background-position: center 7px !important;
}
.gd-events-sheet .DefaultLightTheme .rst__collapseButton {
.gd-events-sheet .light-theme .rst__collapseButton {
background-image: url('./foldDark.png');
}

View File

@@ -67,11 +67,10 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
const spriteConfiguration = gd.asSpriteConfiguration(
object.getConfiguration()
);
const animations = spriteConfiguration.getAnimations();
return mapFor(0, spriteConfiguration.getAnimationsCount(), index => {
const animationName = spriteConfiguration
.getAnimation(index)
.getName();
return mapFor(0, animations.getAnimationsCount(), index => {
const animationName = animations.getAnimation(index).getName();
return animationName.length > 0 ? animationName : null;
}).filter(Boolean);
} else if (object.getType() === 'Scene3D::Model3DObject') {
@@ -100,6 +99,21 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
})
.filter(Boolean)
.sort();
} else if (project.hasEventsBasedObject(object.getType())) {
const eventsBasedObject = project.getEventsBasedObject(
object.getType()
);
if (eventsBasedObject.isAnimatable()) {
const customObjectConfiguration = gd.asCustomObjectConfiguration(
object.getConfiguration()
);
const animations = customObjectConfiguration.getAnimations();
return mapFor(0, animations.getAnimationsCount(), index => {
const animationName = animations.getAnimation(index).getName();
return animationName.length > 0 ? animationName : null;
}).filter(Boolean);
}
}
return [];

View File

@@ -53,8 +53,9 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
const spriteConfiguration = gd.asSpriteConfiguration(
object.getConfiguration()
);
const animations = spriteConfiguration.getAnimations();
return getAllPointNames(spriteConfiguration)
return getAllPointNames(animations)
.map(pointName => (pointName.length > 0 ? pointName : null))
.filter(Boolean)
.sort()
@@ -82,8 +83,9 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
const spriteConfiguration = gd.asSpriteConfiguration(
object.getConfiguration()
);
const animations = spriteConfiguration.getAnimations();
return getAllPointNames(spriteConfiguration)
return getAllPointNames(animations)
.map(pointName => (pointName.length > 0 ? pointName : null))
.filter(Boolean);
}

View File

@@ -9,12 +9,12 @@ import {
type FieldFocusFunction,
getParameterValueOrDefault,
} from './ParameterFieldCommons';
import { focusButton } from '../../UI/Button';
import Text from '../../UI/Text';
import FormHelperText from '@material-ui/core/FormHelperText';
import { MarkdownText } from '../../UI/MarkdownText';
import ButtonGroup from '@material-ui/core/ButtonGroup';
import Button from '@material-ui/core/Button';
import TwoStatesButton, {
type TwoStatesButtonInterface,
} from '../../UI/TwoStatesButton';
const styles = {
description: {
@@ -24,9 +24,9 @@ const styles = {
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
function TrueFalseField(props: ParameterFieldProps, ref) {
const button = React.useRef<?Button>(null);
const button = React.useRef<?TwoStatesButtonInterface>(null);
const focus: FieldFocusFunction = options => {
if (button.current) focusButton(button.current);
if (button.current) button.current.focusLeftButton();
};
React.useImperativeHandle(ref, () => ({
focus,
@@ -47,23 +47,13 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
<Text style={styles.description} displayInlineAsSpan>
{description}
</Text>
<ButtonGroup>
<Button
variant={effectiveValue === 'True' ? 'contained' : 'outlined'}
color={effectiveValue === 'True' ? 'secondary' : 'default'}
onClick={() => props.onChange('True')}
ref={button}
>
<Trans>True</Trans>
</Button>
<Button
variant={effectiveValue !== 'True' ? 'contained' : 'outlined'}
color={effectiveValue !== 'True' ? 'secondary' : 'default'}
onClick={() => props.onChange('False')}
>
<Trans>False</Trans>
</Button>
</ButtonGroup>
<TwoStatesButton
value={effectiveValue}
leftButton={{ label: <Trans>True</Trans>, value: 'True' }}
rightButton={{ label: <Trans>False</Trans>, value: 'False' }}
onChange={props.onChange}
ref={button}
/>
</Line>
{longDescription ? (
<FormHelperText variant="filled" margin="dense">

View File

@@ -9,12 +9,12 @@ import {
type FieldFocusFunction,
getParameterValueOrDefault,
} from './ParameterFieldCommons';
import { focusButton } from '../../UI/Button';
import Text from '../../UI/Text';
import FormHelperText from '@material-ui/core/FormHelperText';
import { MarkdownText } from '../../UI/MarkdownText';
import ButtonGroup from '@material-ui/core/ButtonGroup';
import Button from '@material-ui/core/Button';
import TwoStatesButton, {
type TwoStatesButtonInterface,
} from '../../UI/TwoStatesButton';
const styles = {
description: {
@@ -24,9 +24,9 @@ const styles = {
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
function YesNoField(props: ParameterFieldProps, ref) {
const button = React.useRef<?Button>(null);
const button = React.useRef<?TwoStatesButtonInterface>(null);
const focus: FieldFocusFunction = options => {
if (button.current) focusButton(button.current);
if (button.current) button.current.focusLeftButton();
};
React.useImperativeHandle(ref, () => ({
focus,
@@ -41,34 +41,27 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
: null;
const effectiveValue = getParameterValueOrDefault(value, parameterMetadata);
const isYes = effectiveValue === 'yes';
return (
<Column noMargin>
<Line alignItems="center" justifyContent="space-between">
<Text style={styles.description} displayInlineAsSpan>
{description}
</Text>
<ButtonGroup>
<Button
id="yes-button"
data-effective={isYes ? 'true' : undefined}
variant={isYes ? 'contained' : 'outlined'}
color={isYes ? 'secondary' : 'default'}
onClick={() => props.onChange('yes')}
ref={button}
>
<Trans>Yes</Trans>
</Button>
<Button
id="no-button"
data-effective={!isYes ? 'true' : undefined}
variant={!isYes ? 'contained' : 'outlined'}
color={!isYes ? 'secondary' : 'default'}
onClick={() => props.onChange('no')}
>
<Trans>No</Trans>
</Button>
</ButtonGroup>
<TwoStatesButton
value={effectiveValue}
leftButton={{
label: <Trans>Yes</Trans>,
value: 'yes',
id: 'yes-button',
}}
rightButton={{
label: <Trans>No</Trans>,
value: 'no',
id: 'no-button',
}}
onChange={props.onChange}
ref={button}
/>
</Line>
{longDescription ? (
<FormHelperText variant="filled" margin="dense">

View File

@@ -36,7 +36,7 @@ const makeTestContext = () => {
animation.setName('Jump');
animation.setDirectionsCount(1);
animation.setDirection(direction, 0);
spriteConfiguration.addAnimation(animation);
spriteConfiguration.getAnimations().addAnimation(animation);
const spriteObjectWithBehaviors = testLayout.insertNewObject(
project,

View File

@@ -287,8 +287,9 @@ const getAutocompletionsForText = function(
const spriteConfiguration = gd.asSpriteConfiguration(
object.getConfiguration()
);
const animations = spriteConfiguration.getAnimations();
autocompletionTexts = getAllPointNames(spriteConfiguration)
autocompletionTexts = getAllPointNames(animations)
.map(spriteObjectName =>
spriteObjectName.length > 0 ? `"${spriteObjectName}"` : null
)
@@ -311,14 +312,13 @@ const getAutocompletionsForText = function(
const spriteConfiguration = gd.asSpriteConfiguration(
object.getConfiguration()
);
const animations = spriteConfiguration.getAnimations();
autocompletionTexts = mapFor(
0,
spriteConfiguration.getAnimationsCount(),
animations.getAnimationsCount(),
index => {
const animationName = spriteConfiguration
.getAnimation(index)
.getName();
const animationName = animations.getAnimation(index).getName();
return animationName.length > 0 ? `"${animationName}"` : null;
}
).filter(Boolean);

View File

@@ -28,6 +28,10 @@ import Link from '../../UI/Link';
import PublicProfileDialog from '../../Profile/PublicProfileDialog';
import CheckCircleFilled from '../../UI/CustomSvgIcons/CheckCircleFilled';
import CheckCircle from '../../UI/CustomSvgIcons/CheckCircle';
import Dislike from '../../UI/CustomSvgIcons/Dislike';
import Like from '../../UI/CustomSvgIcons/Like';
import Danger from '../../UI/CustomSvgIcons/Danger';
import Heart from '../../UI/CustomSvgIcons/Heart';
const styles = {
textComment: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' },
@@ -116,6 +120,36 @@ const FeedbackCard = ({
processComment
);
const [ownerQualityRating, setOwnerQualityRating] = useOptimisticState(
(comment.qualityRatingPerRole && comment.qualityRatingPerRole.owner) ||
null,
async (qualityRating, i18n) => {
if (!profile) return;
try {
const updatedComment: Comment = await updateComment(
getAuthorizationHeader,
profile.id,
{
gameId: comment.gameId,
commentId: comment.id,
qualityRating,
}
);
onCommentUpdated(updatedComment);
} catch (error) {
console.error(`Unable to update comment: `, error);
showErrorBox({
message:
i18n._(t`Unable to change quality rating of feedback.`) +
' ' +
i18n._(t`Verify your internet connection or try again later.`),
rawError: error,
errorId: 'feedback-card-set-quality-rating-error',
});
}
}
);
return (
<I18n>
{({ i18n }) => (
@@ -123,17 +157,71 @@ const FeedbackCard = ({
<Card
disabled={processed}
cardCornerAction={
<IconButton
size="small"
tooltip={processed ? t`Mark as unread` : t`Mark as read`}
onClick={() => setProcessed(!processed, i18n)}
>
{processed ? (
<CheckCircleFilled htmlColor={theme.message.valid} />
) : (
<CheckCircle />
)}
</IconButton>
<LineStackLayout noMargin>
<IconButton
size="small"
tooltip={t`Rank this comment as great`}
onClick={() => setOwnerQualityRating('great', i18n)}
>
<Heart
htmlColor={
ownerQualityRating === 'great'
? theme.message.valid
: undefined
}
/>
</IconButton>
<IconButton
size="small"
tooltip={t`Rank this comment as good`}
onClick={() => setOwnerQualityRating('good', i18n)}
>
<Like
htmlColor={
ownerQualityRating === 'good'
? theme.message.valid
: undefined
}
/>
</IconButton>
<IconButton
size="small"
tooltip={t`Rank this comment as bad`}
onClick={() => setOwnerQualityRating('bad', i18n)}
>
<Dislike
htmlColor={
ownerQualityRating === 'bad'
? theme.message.warning
: undefined
}
/>
</IconButton>
<IconButton
size="small"
tooltip={t`Report this comment as abusive, harmful or spam`}
onClick={() => setOwnerQualityRating('harmful', i18n)}
>
<Danger
htmlColor={
ownerQualityRating === 'harmful'
? theme.message.error
: undefined
}
/>
</IconButton>
<IconButton
size="small"
tooltip={processed ? t`Mark as unread` : t`Mark as read`}
onClick={() => setProcessed(!processed, i18n)}
>
{processed ? (
<CheckCircleFilled htmlColor={theme.message.valid} />
) : (
<CheckCircle />
)}
</IconButton>
</LineStackLayout>
}
header={
<BackgroundText style={styles.backgroundText}>

View File

@@ -138,10 +138,9 @@ export const getExtraInstructionInformation = (type: string): ?Hint => {
if (type.indexOf('P2P::') === 0) {
return {
kind: 'warning',
message: t`P2P is a peer-to-peer networking solution. It leaks IP addresses to other players - it is made to play with friends, and is not suitable for playing with unknown players.
P2P also only allows to exchange messages and leaves implementation of higher-level tasks, such as synchronizing the game state, to you.
message: t`P2P is merely a peer-to-peer networking solution. It only handles the connection to another player, and the exchange of messages. Higher-level tasks, such as synchronizing the game state, are left to by implemented by you.
Use the THNK Framework if you seek an easier, more performant and more flexible solution suitable for playing with strangers.`,
Use the THNK Framework if you seek an easy, performant and flexible higher-level solution.`,
identifier: 'p2p-is-networking',
};
}

View File

@@ -273,7 +273,7 @@ class InstancesList extends Component<Props, State> {
key={tableKey}
headerHeight={30}
height={height}
className={`gd-table ${gdevelopTheme.tableRootClassName}`}
className={`gd-table`}
headerClassName={'tableHeaderColumn'}
rowCount={this.renderedRows.length}
rowGetter={this._rowGetter}

View File

@@ -63,6 +63,7 @@ import ContextMenu, {
type ContextMenuInterface,
} from '../../../../UI/Menu/ContextMenu';
import type { ClientCoordinates } from '../../../../Utils/UseLongTouch';
import PromotionsSlideshow from '../../../../Promotions/PromotionsSlideshow';
const electron = optionalRequire('electron');
const path = optionalRequire('path');
@@ -419,7 +420,7 @@ const BuildSection = ({
) : (
<SectionContainer
title={<Trans>My projects</Trans>}
showAnnouncementsAndPromotions
showUrgentAnnouncements
renderFooter={
limits && hasTooManyCloudProjects
? () => (
@@ -452,6 +453,10 @@ const BuildSection = ({
roundedImages
displayArrowsOnDesktop
/>
<Spacer />
<Column noMargin>
<PromotionsSlideshow />
</Column>
</SectionRow>
<SectionRow>
<ResponsiveLineStackLayout

View File

@@ -17,6 +17,8 @@ import List from '@material-ui/core/List';
import ErrorBoundary from '../../../UI/ErrorBoundary';
import { AnnouncementsFeed } from '../../../AnnouncementsFeed';
import { AnnouncementsFeedContext } from '../../../AnnouncementsFeed/AnnouncementsFeedContext';
import PromotionsSlideshow from '../../../Promotions/PromotionsSlideshow';
import { UserAndGameLeaderboards } from './CommunitySection/UserAndGameLeaderboards';
const styles = {
list: {
@@ -71,10 +73,7 @@ const CommunitySection = () => {
announcements && announcements.length > 0;
return (
<SectionContainer
title={<Trans>Community</Trans>}
showAnnouncementsAndPromotions
>
<SectionContainer title={<Trans>Community</Trans>} showUrgentAnnouncements>
<SectionRow>
<ColumnStackLayout noMargin expand>
{shouldDisplayAnnouncementsTitle && (
@@ -82,7 +81,12 @@ const CommunitySection = () => {
<Trans>News and announcements</Trans>
</Text>
)}
<PromotionsSlideshow />
<AnnouncementsFeed canClose={false} level="normal" />
<Text size="title">
<Trans>Rankings</Trans>
</Text>
<UserAndGameLeaderboards />
<Text size="title">
<Trans>Join the conversation</Trans>
</Text>

View File

@@ -0,0 +1,120 @@
// @flow
import * as React from 'react';
import { UserFeedbackLeaderboard } from '../../../../CommunityLeaderboards/UserFeedbackLeaderboard';
import { GameFeedbackLeaderboard } from '../../../../CommunityLeaderboards/GameFeedbackLeaderboard';
import { ResponsiveLineStackLayout } from '../../../../UI/Layout';
import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
import Paper from '../../../../UI/Paper';
import Text from '../../../../UI/Text';
import { Line, Spacer } from '../../../../UI/Grid';
import { CommunityLeaderboardsContext } from '../../../../CommunityLeaderboards/CommunityLeaderboardsContext';
import { Trans } from '@lingui/macro';
const styles = {
leaderboardContainer: {
display: 'flex',
flexDirection: 'column',
flex: 1,
},
};
export const UserAndGameLeaderboards = () => {
const {
fetchCommunityLeaderboards,
gameLeaderboards,
userLeaderboards,
} = React.useContext(CommunityLeaderboardsContext);
React.useEffect(
() => {
fetchCommunityLeaderboards();
},
[fetchCommunityLeaderboards]
);
const { windowSize } = useResponsiveWindowSize();
return (
<ResponsiveLineStackLayout
noColumnMargin
forceMobileLayout={windowSize === 'medium'}
>
<div style={{ ...styles.leaderboardContainer, flex: 1 }}>
<Paper background="light">
<Line expand justifyContent="center" noMargin>
<Text>
<Trans>Daily</Trans>
</Text>
</Line>
</Paper>
<Spacer />
<ResponsiveLineStackLayout noColumnMargin noMargin>
<GameFeedbackLeaderboard
gameLeaderboard={
(gameLeaderboards &&
gameLeaderboards.find(leaderboard =>
leaderboard.name.startsWith('daily')
)) ||
null
}
displayEntriesCount={5}
/>
</ResponsiveLineStackLayout>
</div>
<div style={{ ...styles.leaderboardContainer, flex: 2 }}>
<Paper background="light">
<Line expand justifyContent="center" noMargin>
<Text>
<Trans>Weekly</Trans>
</Text>
</Line>
</Paper>
<Spacer />
<ResponsiveLineStackLayout noColumnMargin noMargin>
<UserFeedbackLeaderboard
userLeaderboard={
(userLeaderboards &&
userLeaderboards.find(leaderboard =>
leaderboard.name.startsWith('weekly')
)) ||
null
}
displayEntriesCount={5}
/>
<GameFeedbackLeaderboard
gameLeaderboard={
(gameLeaderboards &&
gameLeaderboards.find(leaderboard =>
leaderboard.name.startsWith('weekly')
)) ||
null
}
displayEntriesCount={5}
/>
</ResponsiveLineStackLayout>
</div>
<div style={{ ...styles.leaderboardContainer, flex: 1 }}>
<Paper background="light">
<Line expand justifyContent="center" noMargin>
<Text>
<Trans>Monthly</Trans>
</Text>
</Line>
</Paper>
<Spacer />
<ResponsiveLineStackLayout noColumnMargin noMargin>
<UserFeedbackLeaderboard
userLeaderboard={
(userLeaderboards &&
userLeaderboards.find(leaderboard =>
leaderboard.name.startsWith('monthly')
)) ||
null
}
displayEntriesCount={5}
/>
</ResponsiveLineStackLayout>
</div>
</ResponsiveLineStackLayout>
);
};

View File

@@ -18,7 +18,7 @@ import {
type WindowSizeType,
} from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
import Text from '../../../../UI/Text';
import { Column } from '../../../../UI/Grid';
import { Column, Spacer } from '../../../../UI/Grid';
import { type Tutorial } from '../../../../Utils/GDevelopServices/Tutorial';
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
import { CardWidget } from '../CardWidget';
@@ -32,6 +32,7 @@ import PreferencesContext from '../../../Preferences/PreferencesContext';
import PlanRecommendationRow from './PlanRecommendationRow';
import { SurveyCard } from './SurveyCard';
import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
import PromotionsSlideshow from '../../../../Promotions/PromotionsSlideshow';
const styles = {
textTutorialContent: {
@@ -300,6 +301,16 @@ const RecommendationList = ({
</SectionRow>
);
items.push(
<SectionRow key="promotions">
<Text size="section-title" noMargin>
<Trans>Discover the ecosystem</Trans>
</Text>
<Spacer />
<PromotionsSlideshow />
</SectionRow>
);
if (recommendedTextTutorials.length) {
items.push(
<SectionRow key="texts">

View File

@@ -628,16 +628,10 @@ const GetStartedSection = ({
return (
<>
<SectionContainer
title={
profile && profile.username ? (
<Trans>Hello {profile.username}!</Trans>
) : (
<Trans>Hello!</Trans>
)
}
title={<Trans>Start making games</Trans>}
renderSubtitle={renderSubtitle}
flexBody
showAnnouncementsAndPromotions
showUrgentAnnouncements
>
<RecommendationList
authenticatedUser={authenticatedUser}

View File

@@ -11,11 +11,11 @@ import ProjectManagerIcon from '../../../UI/CustomSvgIcons/ProjectManager';
import FloppyIcon from '../../../UI/CustomSvgIcons/Floppy';
import Window from '../../../Utils/Window';
import optionalRequire from '../../../Utils/OptionalRequire';
import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer';
import TextButton from '../../../UI/TextButton';
import IconButton from '../../../UI/IconButton';
import { isNativeMobileApp } from '../../../Utils/Platform';
import NotificationChip from '../../../UI/User/NotificationChip';
import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer';
const electron = optionalRequire('electron');
type Props = {|
@@ -36,6 +36,7 @@ export const HomePageHeader = ({
canSave,
}: Props) => {
const { isMobile } = useResponsiveWindowSize();
return (
<I18n>
{({ i18n }) => (
@@ -73,9 +74,9 @@ export const HomePageHeader = ({
</Column>
<Column>
<LineStackLayout noMargin alignItems="center">
{!electron && !isNativeMobileApp() && !isMobile && (
{!electron && !isNativeMobileApp() && (
<FlatButton
label={<Trans>Download desktop app</Trans>}
label={<Trans>Get the app</Trans>}
onClick={() =>
Window.openExternalURL('https://gdevelop.io/download')
}
@@ -83,11 +84,17 @@ export const HomePageHeader = ({
)}
<UserChip onOpenProfile={onOpenProfile} />
<NotificationChip />
<TextButton
label={i18n.language.toUpperCase()}
onClick={onOpenLanguageDialog}
icon={<TranslateIcon fontSize="small" />}
/>
{isMobile ? (
<IconButton size="small" onClick={onOpenLanguageDialog}>
<TranslateIcon fontSize="small" />
</IconButton>
) : (
<TextButton
label={i18n.language.toUpperCase()}
onClick={onOpenLanguageDialog}
icon={<TranslateIcon fontSize="small" />}
/>
)}
</LineStackLayout>
</Column>
</LineStackLayout>

View File

@@ -79,7 +79,7 @@ const HomePageMenuBar = ({
const theme = React.useContext(GDevelopThemeContext);
const { profile } = React.useContext(AuthenticatedUserContext);
const tabsToDisplay = getTabsToDisplay({ profile });
const buttons: {
const largeScreenOnlyButtons: {
label: React.Node,
getIcon: GetIconFunction,
id: string,
@@ -139,29 +139,6 @@ const HomePageMenuBar = ({
</IconButton>
);
})}
<span
style={{
width: 1,
backgroundColor: theme.home.separator.color,
height: '70%',
margin: '0 3px',
}}
/>
{buttons.map(({ label, onClick, getIcon, id }) => (
<IconButton
color="default"
key={id}
disableRipple
disableFocusRipple
style={styles.mobileButton}
onClick={onClick}
id={id}
>
<span style={styles.buttonLabel}>
{getIcon({ color: 'secondary', fontSize: 'inherit' })}
</span>
</IconButton>
))}
</ToolbarGroup>
</Toolbar>
</Paper>
@@ -198,7 +175,7 @@ const HomePageMenuBar = ({
<div style={styles.bottomButtonsContainer}>
<Column>
{buttons.map(({ label, getIcon, onClick, id }) => (
{largeScreenOnlyButtons.map(({ label, getIcon, onClick, id }) => (
<VerticalTabButton
key={id}
label={label}

View File

@@ -162,9 +162,6 @@ const MainPage = ({
return (
<SectionContainer title={<Trans>Help and guides</Trans>}>
<SectionRow>
<Text>
<Trans>Quick search</Trans>
</Text>
<WikiSearchBar />
</SectionRow>
<SectionRow>

View File

@@ -9,7 +9,6 @@ import { Trans } from '@lingui/macro';
import Paper from '../../../UI/Paper';
import { LineStackLayout } from '../../../UI/Layout';
import { AnnouncementsFeed } from '../../../AnnouncementsFeed';
import PromotionsSlideshow from '../../../Promotions/PromotionsSlideshow';
import { AnnouncementsFeedContext } from '../../../AnnouncementsFeed/AnnouncementsFeedContext';
export const SECTION_PADDING = 30;
@@ -58,7 +57,7 @@ type Props = {|
flexBody?: boolean,
renderFooter?: () => React.Node,
noScroll?: boolean,
showAnnouncementsAndPromotions?: boolean,
showUrgentAnnouncements?: boolean,
|};
const SectionContainer = ({
@@ -71,7 +70,7 @@ const SectionContainer = ({
flexBody,
renderFooter,
noScroll,
showAnnouncementsAndPromotions,
showUrgentAnnouncements,
}: Props) => {
const { isMobile } = useResponsiveWindowSize();
const { announcements } = React.useContext(AnnouncementsFeedContext);
@@ -94,14 +93,10 @@ const SectionContainer = ({
<Column useFullHeight noMargin expand>
<Paper style={paperStyle} square background="dark">
<Column noOverflowParent expand>
{showAnnouncementsAndPromotions && (
{showUrgentAnnouncements && (
<>
<AnnouncementsFeed canClose level="urgent" hideLoader />
{announcements && announcements.length > 0 && <Spacer />}
<Column noMargin>
<PromotionsSlideshow />
</Column>
<Spacer />
</>
)}
{backAction && (

View File

@@ -83,6 +83,7 @@ const StoreSection = ({
onOpenPrivateGameTemplateListingData={
onOpenPrivateGameTemplateListingData
}
displayPromotions
/>
{(openedAssetPack || openedAssetShortHeader) && (
<Line justifyContent="flex-end">

View File

@@ -40,6 +40,7 @@ import { Trans } from '@lingui/macro';
import { CreditsPackageStoreStateProvider } from '../AssetStore/CreditsPackages/CreditsPackageStoreContext';
import { ProductLicenseStoreStateProvider } from '../AssetStore/ProductLicense/ProductLicenseStoreContext';
import { MarketingPlansStoreStateProvider } from '../MarketingPlans/MarketingPlansStoreContext';
import { CommunityLeaderboardsStateProvider } from '../CommunityLeaderboards/CommunityLeaderboardsContext';
type Props = {|
authentication: Authentication,
@@ -119,11 +120,13 @@ const Providers = ({
<BehaviorStoreStateProvider>
<TutorialStateProvider>
<AnnouncementsFeedStateProvider>
<PrivateAssetsAuthorizationProvider>
{children({
i18n,
})}
</PrivateAssetsAuthorizationProvider>
<CommunityLeaderboardsStateProvider>
<PrivateAssetsAuthorizationProvider>
{children({
i18n,
})}
</PrivateAssetsAuthorizationProvider>
</CommunityLeaderboardsStateProvider>
</AnnouncementsFeedStateProvider>
</TutorialStateProvider>
</BehaviorStoreStateProvider>

View File

@@ -79,6 +79,7 @@ const Cube3DEditor = ({
layout,
resourceManagementProps,
objectName,
renderObjectNameField,
}: EditorProps) => {
const forceUpdate = useForceUpdate();
const properties = objectConfiguration.getProperties();
@@ -104,6 +105,7 @@ const Cube3DEditor = ({
return (
<ColumnStackLayout noMargin>
{renderObjectNameField && renderObjectNameField()}
<Text size="block-title" noMargin>
<Trans>Default size</Trans>
</Text>

View File

@@ -1,5 +1,6 @@
// @flow
import { Trans } from '@lingui/macro';
import { t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import * as React from 'react';
@@ -20,6 +21,27 @@ import useForceUpdate from '../../Utils/UseForceUpdate';
import { Accordion, AccordionHeader, AccordionBody } from '../../UI/Accordion';
import { IconContainer } from '../../UI/IconContainer';
import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext';
import AnimationList, {
type AnimationListInterface,
} from './SpriteEditor/AnimationList';
import PointsEditor from './SpriteEditor/PointsEditor';
import CollisionMasksEditor from './SpriteEditor/CollisionMasksEditor';
import {
hasAnyFrame,
getFirstAnimationFrame,
setCollisionMaskOnAllFrames,
} from './SpriteEditor/Utils/SpriteObjectHelper';
import { getMatchingCollisionMask } from './SpriteEditor/CollisionMasksEditor/CollisionMaskHelper';
import ResourcesLoader from '../../ResourcesLoader';
import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView';
import FlatButton from '../../UI/FlatButton';
import RaisedButton from '../../UI/RaisedButton';
import FlatButtonWithSplitMenu from '../../UI/FlatButtonWithSplitMenu';
import { ResponsiveLineStackLayout } from '../../UI/Layout';
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
import Add from '../../UI/CustomSvgIcons/Add';
import Dialog from '../../UI/Dialog';
import HelpButton from '../../UI/HelpButton';
const gd: libGDevelop = global.gd;
@@ -32,11 +54,18 @@ const CustomObjectPropertiesEditor = (props: Props) => {
objectConfiguration,
project,
layout,
object,
objectName,
resourceManagementProps,
onSizeUpdated,
onObjectUpdated,
unsavedChanges,
renderObjectNameField,
isChildObject,
} = props;
const { isMobile } = useResponsiveWindowSize();
const customObjectConfiguration = gd.asCustomObjectConfiguration(
objectConfiguration
);
@@ -61,125 +90,348 @@ const CustomObjectPropertiesEditor = (props: Props) => {
? project.getEventsBasedObject(customObjectConfiguration.getType())
: null;
const animations = customObjectConfiguration.getAnimations();
// The matching collision mask only takes the first sprite of the first
// animation of the object. We consider this is enough to start with, and
// the user can then edit the collision mask for further needs.
const onCreateMatchingSpriteCollisionMask = React.useCallback(
async () => {
const firstSprite = getFirstAnimationFrame(animations);
if (!firstSprite) {
return;
}
const firstSpriteResourceName = firstSprite.getImageName();
const firstAnimationResourceSource = ResourcesLoader.getResourceFullUrl(
project,
firstSpriteResourceName,
{}
);
let matchingCollisionMask = null;
try {
matchingCollisionMask = await getMatchingCollisionMask(
firstAnimationResourceSource
);
} catch (e) {
console.error(
'Unable to create a matching collision mask for the sprite, fallback to full image collision mask.',
e
);
}
setCollisionMaskOnAllFrames(animations, matchingCollisionMask);
forceUpdate();
},
[animations, project, forceUpdate]
);
const scrollView = React.useRef<?ScrollViewInterface>(null);
const animationList = React.useRef<?AnimationListInterface>(null);
const [
justAddedAnimationName,
setJustAddedAnimationName,
] = React.useState<?string>(null);
const justAddedAnimationElement = React.useRef<?any>(null);
React.useEffect(
() => {
if (
scrollView.current &&
justAddedAnimationElement.current &&
justAddedAnimationName
) {
scrollView.current.scrollTo(justAddedAnimationElement.current);
setJustAddedAnimationName(null);
justAddedAnimationElement.current = null;
}
},
[justAddedAnimationName]
);
const [pointsEditorOpen, setPointsEditorOpen] = React.useState(false);
const [
collisionMasksEditorOpen,
setCollisionMasksEditorOpen,
] = React.useState(false);
return (
<I18n>
{({ i18n }) => (
<ColumnStackLayout noMargin>
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialId}
tutorialId={tutorialId}
/>
))}
{propertiesSchema.length ||
(eventBasedObject && eventBasedObject.getObjectsCount()) ? (
<React.Fragment>
{extraInformation ? (
<Line>
<ColumnStackLayout noMargin>
{extraInformation.map(({ kind, message }, index) => (
<AlertMessage kind={kind} key={index}>
{i18n._(message)}
</AlertMessage>
))}
</ColumnStackLayout>
</Line>
) : null}
<PropertiesEditor
unsavedChanges={unsavedChanges}
schema={propertiesSchema}
instances={[customObjectConfiguration]}
project={project}
resourceManagementProps={resourceManagementProps}
/>
{eventBasedObject &&
mapFor(0, eventBasedObject.getObjectsCount(), i => {
const childObject = eventBasedObject.getObjectAt(i);
const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration(
childObject.getName()
);
const editorConfiguration = ObjectsEditorService.getEditorConfigurationForCustomObject(
project,
childObjectConfiguration.getType()
);
const EditorComponent = editorConfiguration.component;
<>
<ScrollView ref={scrollView}>
<ColumnStackLayout noMargin>
{renderObjectNameField && renderObjectNameField()}
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialId}
tutorialId={tutorialId}
/>
))}
{propertiesSchema.length ||
(eventBasedObject &&
(eventBasedObject.getObjectsCount() ||
eventBasedObject.isAnimatable())) ? (
<React.Fragment>
{extraInformation ? (
<Line>
<ColumnStackLayout noMargin>
{extraInformation.map(({ kind, message }, index) => (
<AlertMessage kind={kind} key={index}>
{i18n._(message)}
</AlertMessage>
))}
</ColumnStackLayout>
</Line>
) : null}
<PropertiesEditor
unsavedChanges={unsavedChanges}
schema={propertiesSchema}
instances={[customObjectConfiguration]}
project={project}
resourceManagementProps={resourceManagementProps}
/>
{eventBasedObject &&
mapFor(0, eventBasedObject.getObjectsCount(), i => {
const childObject = eventBasedObject.getObjectAt(i);
const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration(
childObject.getName()
);
const editorConfiguration = ObjectsEditorService.getEditorConfiguration(
project,
childObjectConfiguration.getType()
);
const EditorComponent = editorConfiguration.component;
const objectMetadata = gd.MetadataProvider.getObjectMetadata(
gd.JsPlatform.get(),
childObjectConfiguration.getType()
);
const iconUrl = objectMetadata.getIconFilename();
const tutorialIds = getObjectTutorialIds(
childObjectConfiguration.getType()
);
const enabledTutorialIds = tutorialIds.filter(
tutorialId => !values.hiddenTutorialHints[tutorialId]
);
// TODO EBO: Add a protection against infinite loops in case
// of object cycles (thought it should be forbidden).
return (
<Accordion key={childObject.getName()} defaultExpanded>
<AccordionHeader>
{iconUrl ? (
<IconContainer
src={iconUrl}
alt={childObject.getName()}
size={20}
/>
) : null}
<Column expand>
<Text size="block-title">
{childObject.getName()}
</Text>
</Column>
</AccordionHeader>
<AccordionBody>
<Column expand noMargin noOverflowParent>
{enabledTutorialIds.length ? (
<Line>
<ColumnStackLayout expand>
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialId}
tutorialId={tutorialId}
/>
))}
</ColumnStackLayout>
</Line>
) : null}
<Line noMargin>
<Column expand>
<EditorComponent
objectConfiguration={childObjectConfiguration}
project={project}
layout={layout}
resourceManagementProps={
resourceManagementProps
}
onSizeUpdated={
forceUpdate /*Force update to ensure dialog is properly positioned*/
}
objectName={
objectName + ' ' + childObject.getName()
}
const objectMetadata = gd.MetadataProvider.getObjectMetadata(
gd.JsPlatform.get(),
childObjectConfiguration.getType()
);
const iconUrl = objectMetadata.getIconFilename();
const tutorialIds = getObjectTutorialIds(
childObjectConfiguration.getType()
);
const enabledTutorialIds = tutorialIds.filter(
tutorialId => !values.hiddenTutorialHints[tutorialId]
);
// TODO EBO: Add a protection against infinite loops in case
// of object cycles (thought it should be forbidden).
return (
<Accordion key={childObject.getName()} defaultExpanded>
<AccordionHeader>
{iconUrl ? (
<IconContainer
src={iconUrl}
alt={childObject.getName()}
size={20}
/>
) : null}
<Column expand>
<Text size="block-title">
{childObject.getName()}
</Text>
</Column>
</Line>
</Column>
</AccordionBody>
</Accordion>
);
})}
</React.Fragment>
) : (
<EmptyMessage>
<Trans>
There is nothing to configure for this object. You can still use
events to interact with the object.
</Trans>
</EmptyMessage>
</AccordionHeader>
<AccordionBody>
<Column expand noMargin noOverflowParent>
{enabledTutorialIds.length ? (
<Line>
<ColumnStackLayout expand>
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialId}
tutorialId={tutorialId}
/>
))}
</ColumnStackLayout>
</Line>
) : null}
<Line noMargin>
<Column expand>
<EditorComponent
isChildObject
objectConfiguration={
childObjectConfiguration
}
project={project}
layout={layout}
resourceManagementProps={
resourceManagementProps
}
onSizeUpdated={
forceUpdate /*Force update to ensure dialog is properly positioned*/
}
objectName={
objectName + ' ' + childObject.getName()
}
/>
</Column>
</Line>
</Column>
</AccordionBody>
</Accordion>
);
})}
{eventBasedObject && eventBasedObject.isAnimatable() && (
<Column expand>
<Text size="block-title">
<Trans>Animations</Trans>
</Text>
<AnimationList
ref={animationList}
animations={animations}
project={project}
layout={layout}
object={object}
objectName={objectName}
resourceManagementProps={resourceManagementProps}
onSizeUpdated={onSizeUpdated}
onObjectUpdated={onObjectUpdated}
isAnimationListLocked={false}
scrollView={scrollView}
onCreateMatchingSpriteCollisionMask={
onCreateMatchingSpriteCollisionMask
}
/>
</Column>
)}
</React.Fragment>
) : (
<EmptyMessage>
<Trans>
There is nothing to configure for this object. You can still
use events to interact with the object.
</Trans>
</EmptyMessage>
)}
</ColumnStackLayout>
</ScrollView>
{eventBasedObject &&
eventBasedObject.isAnimatable() &&
!isChildObject && (
<Column noMargin>
<ResponsiveLineStackLayout
justifyContent="space-between"
noColumnMargin
>
{!isMobile ? ( // On mobile, use only 1 button to gain space.
<ResponsiveLineStackLayout noMargin noColumnMargin>
<FlatButton
label={<Trans>Edit collision masks</Trans>}
onClick={() => setCollisionMasksEditorOpen(true)}
disabled={!hasAnyFrame(animations)}
/>
<FlatButton
label={<Trans>Edit points</Trans>}
onClick={() => setPointsEditorOpen(true)}
disabled={!hasAnyFrame(animations)}
/>
</ResponsiveLineStackLayout>
) : (
<FlatButtonWithSplitMenu
label={<Trans>Edit collision masks</Trans>}
onClick={() => setCollisionMasksEditorOpen(true)}
disabled={!hasAnyFrame(animations)}
buildMenuTemplate={i18n => [
{
label: i18n._(t`Edit points`),
disabled: !hasAnyFrame(animations),
click: () => setPointsEditorOpen(true),
},
]}
/>
)}
<RaisedButton
label={<Trans>Add an animation</Trans>}
primary
onClick={() => {
if (!animationList.current) {
return;
}
animationList.current.addAnimation();
}}
icon={<Add />}
/>
</ResponsiveLineStackLayout>
</Column>
)}
{pointsEditorOpen && (
<Dialog
title={<Trans>Edit points</Trans>}
actions={[
<FlatButton
key="close"
label={<Trans>Close</Trans>}
primary
onClick={() => setPointsEditorOpen(false)}
/>,
]}
secondaryActions={[
<HelpButton
helpPagePath="/objects/sprite/edit-points"
key="help"
/>,
]}
onRequestClose={() => setPointsEditorOpen(false)}
maxWidth="lg"
flexBody
fullHeight
open={pointsEditorOpen}
>
<PointsEditor
animations={animations}
resourcesLoader={ResourcesLoader}
project={project}
onPointsUpdated={onObjectUpdated}
onRenamedPoint={(oldName, newName) =>
// TODO EBO Refactor event-based object events when a point is renamed.
layout &&
object &&
gd.WholeProjectRefactorer.renameObjectPoint(
project,
layout,
object,
oldName,
newName
)
}
/>
</Dialog>
)}
</ColumnStackLayout>
{collisionMasksEditorOpen && (
<Dialog
title={<Trans>Edit collision masks</Trans>}
actions={[
<FlatButton
key="close"
label={<Trans>Close</Trans>}
primary
onClick={() => setCollisionMasksEditorOpen(false)}
/>,
]}
secondaryActions={[
<HelpButton
helpPagePath="/objects/sprite/collision-mask"
key="help"
/>,
]}
maxWidth="lg"
flexBody
fullHeight
onRequestClose={() => setCollisionMasksEditorOpen(false)}
open={collisionMasksEditorOpen}
>
<CollisionMasksEditor
animations={animations}
resourcesLoader={ResourcesLoader}
project={project}
onMasksUpdated={onObjectUpdated}
onCreateMatchingSpriteCollisionMask={
onCreateMatchingSpriteCollisionMask
}
/>
</Dialog>
)}
</>
)}
</I18n>
);

View File

@@ -1,6 +1,8 @@
// @flow
import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource';
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
import { type ScrollViewInterface } from '../../UI/ScrollView';
import * as React from 'react';
/**
* The props given to any object editor
@@ -30,4 +32,7 @@ export type EditorProps = {|
onSizeUpdated: () => void,
onObjectUpdated?: () => void,
unsavedChanges?: UnsavedChanges,
scrollView?: ScrollViewInterface,
renderObjectNameField?: () => React.Node,
isChildObject?: boolean,
|};

View File

@@ -4,9 +4,14 @@ import * as React from 'react';
import { Line, Column } from '../../UI/Grid';
import EmptyMessage from '../../UI/EmptyMessage';
export default function EmptyEditor() {
export default function EmptyEditor({
renderObjectNameField,
}: {
renderObjectNameField?: () => React.Node,
}) {
return (
<Column noMargin>
{renderObjectNameField && renderObjectNameField()}
<Line>
<EmptyMessage>
<Trans>

View File

@@ -106,6 +106,7 @@ const Model3DEditor = ({
onSizeUpdated,
onObjectUpdated,
resourceManagementProps,
renderObjectNameField,
}: EditorProps) => {
const scrollView = React.useRef<?ScrollViewInterface>(null);
@@ -469,6 +470,7 @@ const Model3DEditor = ({
<>
<ScrollView ref={scrollView}>
<ColumnStackLayout noMargin>
{renderObjectNameField && renderObjectNameField()}
<PropertyResourceSelector
objectConfiguration={objectConfiguration}
propertyName="modelResourceName"

View File

@@ -19,6 +19,7 @@ export default class PanelSpriteEditor extends React.Component<
project,
resourceManagementProps,
objectName,
renderObjectNameField,
} = this.props;
const panelSpriteConfiguration = gd.asPanelSpriteConfiguration(
objectConfiguration
@@ -26,6 +27,7 @@ export default class PanelSpriteEditor extends React.Component<
return (
<ColumnStackLayout noMargin>
{renderObjectNameField && renderObjectNameField()}
<ResourceSelectorWithThumbnail
project={project}
resourceManagementProps={resourceManagementProps}

View File

@@ -29,6 +29,7 @@ export default class ParticleEmitterEditor extends React.Component<
project,
resourceManagementProps,
objectName,
renderObjectNameField,
} = this.props;
const particleEmitterConfiguration = gd.asParticleEmitterConfiguration(
objectConfiguration
@@ -37,6 +38,7 @@ export default class ParticleEmitterEditor extends React.Component<
return (
<ColumnStackLayout noMargin>
{renderObjectNameField && renderObjectNameField()}
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialId}

View File

@@ -20,13 +20,14 @@ export default class PanelSpriteEditor extends React.Component<
void
> {
render() {
const { objectConfiguration } = this.props;
const { objectConfiguration, renderObjectNameField } = this.props;
const shapePainterConfiguration = gd.asShapePainterConfiguration(
objectConfiguration
);
return (
<ColumnStackLayout noMargin>
{renderObjectNameField && renderObjectNameField()}
<Checkbox
label={
<Trans>

View File

@@ -58,6 +58,7 @@ const SpineEditor = ({
onSizeUpdated,
onObjectUpdated,
resourceManagementProps,
renderObjectNameField,
}: EditorProps) => {
const scrollView = React.useRef<?ScrollViewInterface>(null);
const [
@@ -305,6 +306,7 @@ const SpineEditor = ({
<>
<ScrollView ref={scrollView}>
<ColumnStackLayout noMargin>
{renderObjectNameField && renderObjectNameField()}
<AlertMessage kind="warning">
<Trans>
You need to own a license of Spine to publish a game with a Spine

View File

@@ -0,0 +1,813 @@
// @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 SpritesList, {
addAnimationFrame,
applyPointsAndMasksToSpriteIfNecessary,
} from './SpritesList';
import IconButton from '../../../UI/IconButton';
import { mapFor } from '../../../Utils/MapFor';
import SemiControlledTextField from '../../../UI/SemiControlledTextField';
import Text from '../../../UI/Text';
import ResourcesLoader from '../../../ResourcesLoader';
import { Column, Line, Spacer } from '../../../UI/Grid';
import useForceUpdate from '../../../Utils/UseForceUpdate';
import { EmptyPlaceholder } from '../../../UI/EmptyPlaceholder';
import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer';
import Trash from '../../../UI/CustomSvgIcons/Trash';
import { makeDragSourceAndDropTarget } from '../../../UI/DragAndDrop/DragSourceAndDropTarget';
import { DragHandleIcon } from '../../../UI/DragHandle';
import DropIndicator from '../../../UI/SortableVirtualizedItemList/DropIndicator';
import GDevelopThemeContext from '../../../UI/Theme/GDevelopThemeContext';
import useAlertDialog from '../../../UI/Alert/useAlertDialog';
import {
copySpritePoints,
copySpritePolygons,
getCurrentElements,
getTotalSpritesCount,
getFirstAnimationFrame,
} from './Utils/SpriteObjectHelper';
import Edit from '../../../UI/CustomSvgIcons/Edit';
import { groupResourcesByAnimations } from './AnimationImportHelper';
import { applyResourceDefaults } from '../../../ResourcesList/ResourceUtils';
import { ExternalEditorOpenedDialog } from '../../../UI/ExternalEditorOpenedDialog';
import {
type ResourceExternalEditor,
type EditWithExternalEditorReturn,
} from '../../../ResourcesList/ResourceExternalEditor';
import { showErrorBox } from '../../../UI/Messages/MessageBox';
import { type UnsavedChanges } from '../../../MainFrame/UnsavedChangesContext';
import { type ResourceManagementProps } from '../../../ResourcesList/ResourceSource';
import { type ScrollViewInterface } from '../../../UI/ScrollView';
const gd: libGDevelop = global.gd;
const removeExtensionFromFileName = (fileName: string) => {
const dotIndex = fileName.lastIndexOf('.');
return dotIndex < 0 ? fileName : fileName.substring(0, dotIndex);
};
const DragSourceAndDropTarget = makeDragSourceAndDropTarget(
'sprite-animations-list'
);
const styles = {
rowContainer: {
display: 'flex',
flexDirection: 'column',
marginTop: 5,
},
rowContent: {
display: 'flex',
flex: 1,
alignItems: 'center',
},
animationLine: {
// Use a non standard spacing because:
// - The SortableAnimationsList won't work with <Spacer /> or <LargeSpacer /> between elements.
// - We need to visually show a difference between animations.
marginBottom: 16,
},
};
export type AnimationListInterface = {|
forceUpdate: () => void,
addAnimation: () => void,
|};
type AnimationListProps = {|
project: gdProject,
// TODO EBO : Layout and EventBasedObject should have a common interface to
// browse their events. It would allow to refactor the events when an
// animation is renamed for instance.
/**
* The layout is used to adapt events when an identifier is renamed
* (for instance, an object animation or a layer name).
*/
layout?: gdLayout,
/**
* The edited object. It can be undefined for sub-ObjectConfiguration of
* custom object. There is no event to refactor in this case.
*/
object?: gdObject,
/**
* The object name used to build default file name for Piskel.
* For custom objects, the children names are appended.
*/
objectName: string,
resourceManagementProps: ResourceManagementProps,
onSizeUpdated: () => void,
onObjectUpdated?: () => void,
unsavedChanges?: UnsavedChanges,
animations: gdSpriteAnimationList,
isAnimationListLocked?: boolean,
scrollView: { current: ?ScrollViewInterface },
onCreateMatchingSpriteCollisionMask: () => Promise<void>,
|};
const AnimationList = React.forwardRef<
AnimationListProps,
AnimationListInterface
>(
(
{
animations,
project,
layout,
object,
objectName,
resourceManagementProps,
onSizeUpdated,
onObjectUpdated,
isAnimationListLocked = false,
scrollView,
onCreateMatchingSpriteCollisionMask,
},
ref
) => {
const [externalEditorOpened, setExternalEditorOpened] = React.useState(
false
);
const abortControllerRef = React.useRef<?AbortController>(null);
const forceUpdate = useForceUpdate();
const { isMobile } = useResponsiveWindowSize();
const { showConfirmation } = useAlertDialog();
const [
justAddedAnimationName,
setJustAddedAnimationName,
] = React.useState<?string>(null);
const justAddedAnimationElement = React.useRef<?any>(null);
React.useEffect(
() => {
if (
scrollView.current &&
justAddedAnimationElement.current &&
justAddedAnimationName
) {
scrollView.current.scrollTo(justAddedAnimationElement.current);
setJustAddedAnimationName(null);
justAddedAnimationElement.current = null;
}
},
[justAddedAnimationName, scrollView]
);
const { showDeleteConfirmation } = useAlertDialog();
const draggedAnimationIndex = React.useRef<number | null>(null);
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const [nameErrors, setNameErrors] = React.useState<{
[number]: React.Node,
}>({});
const onApplyFirstSpriteCollisionMaskToSprite = React.useCallback(
(sprite: gdSprite) => {
const firstSprite = getFirstAnimationFrame(animations);
if (!firstSprite) {
return;
}
sprite.setFullImageCollisionMask(
firstSprite.isFullImageCollisionMask()
);
sprite.setCustomCollisionMask(firstSprite.getCustomCollisionMask());
forceUpdate();
},
[animations, forceUpdate]
);
const moveAnimation = React.useCallback(
(targetIndex: number) => {
const draggedIndex = draggedAnimationIndex.current;
if (draggedIndex === null) return;
setNameErrors({});
animations.moveAnimation(
draggedIndex,
targetIndex > draggedIndex ? targetIndex - 1 : targetIndex
);
if (
(draggedIndex === 0 || targetIndex === 0) &&
animations.adaptCollisionMaskAutomatically()
) {
// If the first animation is changed and the collision mask is adapted automatically,
// then we need to recompute it.
onCreateMatchingSpriteCollisionMask();
}
forceUpdate();
},
[animations, forceUpdate, onCreateMatchingSpriteCollisionMask]
);
const onSpriteAdded = React.useCallback(
(sprite: gdSprite) => {
// If a sprite is added, we want to ensure it gets the automatic
// collision mask of the object, if the option is enabled.
if (animations.adaptCollisionMaskAutomatically()) {
onApplyFirstSpriteCollisionMaskToSprite(sprite);
}
},
[onApplyFirstSpriteCollisionMaskToSprite, animations]
);
const addAnimations = React.useCallback(
(resourcesByAnimation: Map<string, Array<gdResource>>) => {
setNameErrors({});
for (const [name, resources] of resourcesByAnimation) {
const animation = new gd.Animation();
animation.setName(name);
animation.setDirectionsCount(1);
const direction = animation.getDirection(0);
for (const resource of resources) {
addAnimationFrame(animations, direction, resource, onSpriteAdded);
}
animations.addAnimation(animation);
animation.delete();
}
forceUpdate();
onSizeUpdated();
if (onObjectUpdated) onObjectUpdated();
},
[forceUpdate, onObjectUpdated, onSizeUpdated, onSpriteAdded, animations]
);
const addAnimation = React.useCallback(
() => {
setNameErrors({});
const emptyAnimation = new gd.Animation();
emptyAnimation.setDirectionsCount(1);
animations.addAnimation(emptyAnimation);
emptyAnimation.delete();
forceUpdate();
onSizeUpdated();
if (onObjectUpdated) onObjectUpdated();
// Scroll to the bottom of the list.
// Ideally, we'd wait for the list to be updated to scroll, but
// to simplify the code, we just wait a few ms for a new render
// to be done.
setTimeout(() => {
if (scrollView.current) {
scrollView.current.scrollToBottom();
}
}, 100); // A few ms is enough for a new render to be done.
},
[animations, forceUpdate, onSizeUpdated, onObjectUpdated, scrollView]
);
React.useImperativeHandle(ref, () => ({
forceUpdate,
addAnimation,
}));
const removeAnimation = React.useCallback(
async (index: number, i18n: I18nType) => {
const totalSpritesCount = getTotalSpritesCount(animations);
const isDeletingLastSprites =
animations
.getAnimation(index)
.getDirection(0)
.getSpritesCount() === totalSpritesCount;
const firstSpriteInAnimationDeleted = getCurrentElements(
animations,
index,
0,
0
).sprite;
const isUsingCustomCollisionMask =
!animations.adaptCollisionMaskAutomatically() &&
firstSpriteInAnimationDeleted &&
!firstSpriteInAnimationDeleted.isFullImageCollisionMask();
const shouldWarnBecauseLosingCustomCollisionMask =
isDeletingLastSprites && isUsingCustomCollisionMask;
const message = shouldWarnBecauseLosingCustomCollisionMask
? t`Are you sure you want to remove this animation? You will lose the custom collision mask you have set for this object.`
: t`Are you sure you want to remove this animation?`;
const deleteAnswer = await showDeleteConfirmation({
title: t`Remove the animation`,
message,
confirmButtonLabel: t`Remove`,
dismissButtonLabel: t`Cancel`,
});
if (!deleteAnswer) return;
setNameErrors({});
animations.removeAnimation(index);
forceUpdate();
onSizeUpdated();
if (index === 0 && animations.adaptCollisionMaskAutomatically()) {
// If the first animation is removed and the collision mask is
// automatically adapted, then recompute it.
onCreateMatchingSpriteCollisionMask();
}
if (shouldWarnBecauseLosingCustomCollisionMask) {
// The user has deleted the last custom collision mask, so revert to automatic
// collision mask adaptation.
animations.setAdaptCollisionMaskAutomatically(true);
}
if (onObjectUpdated) onObjectUpdated();
},
[
forceUpdate,
onObjectUpdated,
onSizeUpdated,
showDeleteConfirmation,
animations,
onCreateMatchingSpriteCollisionMask,
]
);
const changeAnimationName = React.useCallback(
(changedAnimationIndex: number, newName: string) => {
const animation = animations.getAnimation(changedAnimationIndex);
const currentName = animation.getName();
if (currentName === newName) return;
setNameErrors({});
const otherNames = mapFor(0, animations.getAnimationsCount(), index => {
return index === changedAnimationIndex
? undefined // Don't check the current animation name as we're changing it.
: animations.getAnimation(index).getName();
}).filter(Boolean);
if (newName !== '' && otherNames.some(name => name === newName)) {
// The indexes can be used as a key because errors are cleared when
// animations are moved.
setNameErrors({
...nameErrors,
[changedAnimationIndex]: (
<Trans>The animation name {newName} is already taken</Trans>
),
});
return;
}
animation.setName(newName);
// TODO EBO Refactor event-based object events when an animation is renamed.
if (layout && object) {
gd.WholeProjectRefactorer.renameObjectAnimation(
project,
layout,
object,
currentName,
newName
);
}
forceUpdate();
if (onObjectUpdated) onObjectUpdated();
},
[
forceUpdate,
layout,
nameErrors,
object,
onObjectUpdated,
project,
animations,
]
);
const replaceDirection = React.useCallback(
(animationId, directionId, newDirection) => {
animations
.getAnimation(animationId)
.setDirection(newDirection, directionId);
forceUpdate();
if (onObjectUpdated) onObjectUpdated();
},
[forceUpdate, onObjectUpdated, animations]
);
const storageProvider = resourceManagementProps.getStorageProvider();
const resourceSources = resourceManagementProps.resourceSources
.filter(source => source.kind === 'image')
.filter(
({ onlyForStorageProvider }) =>
!onlyForStorageProvider ||
onlyForStorageProvider === storageProvider.internalName
);
const adaptCollisionMaskIfNeeded = React.useCallback(
() => {
if (animations.adaptCollisionMaskAutomatically()) {
onCreateMatchingSpriteCollisionMask();
}
},
[onCreateMatchingSpriteCollisionMask, animations]
);
const importImages = React.useCallback(
async () => {
const resources = await resourceManagementProps.onChooseResource({
initialSourceName: resourceSources[0].name,
multiSelection: true,
resourceKind: 'image',
});
if (resources.length === 0) {
return;
}
resources.forEach(resource => {
applyResourceDefaults(project, resource);
project.getResourcesManager().addResource(resource);
});
addAnimations(groupResourcesByAnimations(resources));
// Important, we are responsible for deleting the resources that were given to us.
// Otherwise we have a memory leak, as calling addResource is making a copy of the resource.
resources.forEach(resource => resource.delete());
forceUpdate();
await resourceManagementProps.onFetchNewlyAddedResources();
adaptCollisionMaskIfNeeded();
if (onObjectUpdated) onObjectUpdated();
},
[
resourceManagementProps,
resourceSources,
addAnimations,
forceUpdate,
adaptCollisionMaskIfNeeded,
onObjectUpdated,
project,
]
);
const editDirectionWith = React.useCallback(
async (
i18n: I18nType,
externalEditor: ResourceExternalEditor,
direction: gdDirection,
animationIndex: number,
directionIndex: number
) => {
abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;
const resourceNames = mapFor(0, direction.getSpritesCount(), i => {
return direction.getSprite(i).getImageName();
});
const animation = animations.getAnimation(animationIndex);
const animationName = animation.getName();
try {
setExternalEditorOpened(true);
const editResult: EditWithExternalEditorReturn | null = await externalEditor.edit(
{
project,
i18n,
getStorageProvider: resourceManagementProps.getStorageProvider,
resourceManagementProps,
resourceNames,
extraOptions: {
singleFrame: false,
fps:
direction.getTimeBetweenFrames() > 0
? 1 / direction.getTimeBetweenFrames()
: 1,
name:
animationName ||
(resourceNames[0] &&
removeExtensionFromFileName(resourceNames[0])) ||
objectName,
isLooping: direction.isLooping(),
existingMetadata: direction.getMetadata(),
},
signal,
}
);
setExternalEditorOpened(false);
if (!editResult) return;
const { resources, newMetadata, newName } = editResult;
const newDirection = new gd.Direction();
newDirection.setTimeBetweenFrames(direction.getTimeBetweenFrames());
newDirection.setLoop(direction.isLooping());
resources.forEach(resource => {
const sprite = new gd.Sprite();
sprite.setImageName(resource.name);
// Restore collision masks and points
if (
resource.originalIndex !== undefined &&
resource.originalIndex !== null
) {
// The sprite existed before, so we can copy its points and collision masks.
const originalSprite = direction.getSprite(
resource.originalIndex
);
copySpritePoints(originalSprite, sprite);
copySpritePolygons(originalSprite, sprite);
} else {
// The sprite is new, apply points & collision masks if necessary.
applyPointsAndMasksToSpriteIfNecessary(
animations,
direction,
sprite
);
}
onSpriteAdded(sprite); // Call the callback before `addSprite`, as `addSprite` will store a copy of it.
newDirection.addSprite(sprite);
sprite.delete();
});
// Set metadata on the direction to allow editing again in the future.
if (newMetadata) {
newDirection.setMetadata(JSON.stringify(newMetadata));
}
// Burst the ResourcesLoader cache to force images to be reloaded (and not cached by the browser).
ResourcesLoader.burstUrlsCacheForResources(project, resourceNames);
replaceDirection(animationIndex, directionIndex, newDirection);
// If a name was specified in the external editor, use it for the animation.
if (newName) {
changeAnimationName(animationIndex, newName);
}
newDirection.delete();
if (onObjectUpdated) onObjectUpdated();
// If an external editor is used to edit the sprites, we assume the first sprite was edited.
if (animationIndex === 0) {
adaptCollisionMaskIfNeeded();
}
} catch (error) {
if (error.name !== 'UserCancellationError') {
console.error(
'An exception was thrown when launching or reading resources from the external editor:',
error
);
showErrorBox({
message:
'There was an error while using the external editor. Try with another resource and if this persists, please report this as a bug.',
rawError: error,
errorId: 'external-editor-error',
});
}
setExternalEditorOpened(false);
} finally {
abortControllerRef.current = null;
}
},
[
animations,
project,
resourceManagementProps,
objectName,
replaceDirection,
onObjectUpdated,
onSpriteAdded,
changeAnimationName,
adaptCollisionMaskIfNeeded,
]
);
const cancelEditingWithExternalEditor = React.useCallback(
async () => {
const shouldContinue = await showConfirmation({
title: t`Cancel editing`,
message: t`You will lose any progress made with the external editor. Do you wish to cancel?`,
confirmButtonLabel: t`Cancel edition`,
dismissButtonLabel: t`Continue editing`,
});
if (!shouldContinue) return;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
} else {
console.error(
'Cannot cancel editing with external editor, abort controller is missing.'
);
}
},
[showConfirmation]
);
const createAnimationWith = React.useCallback(
async (i18n: I18nType, externalEditor: ResourceExternalEditor) => {
addAnimation();
const direction = animations.getAnimation(0).getDirection(0);
await editDirectionWith(i18n, externalEditor, direction, 0, 0);
},
[addAnimation, editDirectionWith, animations]
);
const imageResourceExternalEditors = resourceManagementProps.resourceExternalEditors.filter(
({ kind }) => kind === 'image'
);
return (
<I18n>
{({ i18n }) => (
<>
{animations.getAnimationsCount() === 0 &&
// The event-based object editor gives an empty list.
imageResourceExternalEditors.length > 0 ? (
<Column noMargin expand justifyContent="center">
<EmptyPlaceholder
title={<Trans>Add your first animation</Trans>}
description={
<Trans>Animations are a sequence of images.</Trans>
}
actionLabel={<Trans>Import images</Trans>}
secondaryActionLabel={i18n._(
isMobile
? t`Draw`
: imageResourceExternalEditors[0].createDisplayName
)}
secondaryActionIcon={<Edit />}
helpPagePath="/objects/sprite"
tutorialId="intermediate-changing-animations"
onAction={() => {
importImages();
}}
onSecondaryAction={() => {
createAnimationWith(i18n, imageResourceExternalEditors[0]);
}}
/>
</Column>
) : (
<>
{mapFor(0, animations.getAnimationsCount(), animationIndex => {
const animation = animations.getAnimation(animationIndex);
const animationName = animation.getName();
const animationRef =
justAddedAnimationName === animationName
? justAddedAnimationElement
: null;
return (
<DragSourceAndDropTarget
key={animationIndex}
beginDrag={() => {
draggedAnimationIndex.current = animationIndex;
return {};
}}
canDrag={() => true}
canDrop={() => true}
drop={() => {
moveAnimation(animationIndex);
}}
>
{({
connectDragSource,
connectDropTarget,
isOver,
canDrop,
}) =>
connectDropTarget(
<div key={animationIndex} style={styles.rowContainer}>
{isAnimationListLocked && (
<Column expand noMargin>
<Text size="block-title">{animationName}</Text>
</Column>
)}
{!isAnimationListLocked && isOver && (
<DropIndicator canDrop={canDrop} />
)}
{!isAnimationListLocked && (
<div
ref={animationRef}
style={{
...styles.rowContent,
backgroundColor:
gdevelopTheme.list.itemsBackgroundColor,
}}
>
<Line noMargin expand alignItems="center">
{connectDragSource(
<span>
<Column>
<DragHandleIcon />
</Column>
</span>
)}
<Text noMargin noShrink>
<Trans>Animation #{animationIndex}</Trans>
</Text>
<Spacer />
<SemiControlledTextField
margin="none"
commitOnBlur
errorText={nameErrors[animationIndex]}
translatableHintText={t`Optional animation name`}
value={animation.getName()}
onChange={newName =>
changeAnimationName(
animationIndex,
newName
)
}
fullWidth
/>
<IconButton
size="small"
onClick={() =>
removeAnimation(animationIndex, i18n)
}
>
<Trash />
</IconButton>
</Line>
<Spacer />
</div>
)}
<div style={styles.animationLine}>
<Column expand noMargin>
{mapFor(
0,
animation.getDirectionsCount(),
directionIndex => {
const direction = animation.getDirection(
directionIndex
);
return (
<SpritesList
animations={animations}
direction={direction}
key={directionIndex}
project={project}
resourcesLoader={ResourcesLoader}
resourceManagementProps={
resourceManagementProps
}
editDirectionWith={(
i18n,
ResourceExternalEditor,
direction
) =>
editDirectionWith(
i18n,
ResourceExternalEditor,
direction,
animationIndex,
directionIndex
)
}
onReplaceByDirection={newDirection =>
replaceDirection(
animationIndex,
directionIndex,
newDirection
)
}
objectName={objectName}
animationName={animationName}
onChangeName={newName =>
changeAnimationName(
animationIndex,
newName
)
}
onSpriteUpdated={onObjectUpdated}
onFirstSpriteUpdated={
// If the first sprite of the first animation is updated,
// we update the automatic collision mask of the object,
// if the option is enabled.
animationIndex === 0
? adaptCollisionMaskIfNeeded
: undefined
}
onSpriteAdded={onSpriteAdded}
addAnimations={addAnimations}
/>
);
}
)}
</Column>
</div>
</div>
)
}
</DragSourceAndDropTarget>
);
})}
</>
)}
{externalEditorOpened && (
<ExternalEditorOpenedDialog
onClose={cancelEditingWithExternalEditor}
/>
)}
</>
)}
</I18n>
);
}
);
export default AnimationList;

View File

@@ -201,15 +201,13 @@ type PolygonsListProps = {|
// Sprite size is useful to make sure polygon vertices
// are not put outside the sprite bounding box, which is not supported:
spriteWidth: number,
spriteHeight: number,
spriteSize: [number, number],
|};
const PolygonsList = (props: PolygonsListProps) => {
const {
polygons,
spriteHeight,
spriteWidth,
spriteSize,
onPolygonsUpdated,
onSetFullImageCollisionMask,
onSetAutomaticallyAdaptCollisionMasks,
@@ -218,6 +216,7 @@ const PolygonsList = (props: PolygonsListProps) => {
selectedVerticePtr,
} = props;
const [spriteWidth, spriteHeight] = spriteSize;
const addCollisionMask = React.useCallback(
() => {
const newPolygon = gd.Polygon2d.createRectangle(

View File

@@ -28,7 +28,6 @@ import Paper from '../../../../UI/Paper';
import ScrollView from '../../../../UI/ScrollView';
import useAlertDialog from '../../../../UI/Alert/useAlertDialog';
import AlertMessage from '../../../../UI/AlertMessage';
const gd: libGDevelop = global.gd;
const styles = {
leftContainer: {
@@ -58,7 +57,7 @@ const verticalMosaicNodes: EditorMosaicNode = {
};
type Props = {|
objectConfiguration: gdSpriteObject,
animations: gdSpriteAnimationList,
resourcesLoader: typeof ResourcesLoader,
project: gdProject,
onMasksUpdated?: () => void,
@@ -66,7 +65,7 @@ type Props = {|
|};
const CollisionMasksEditor = ({
objectConfiguration,
animations,
resourcesLoader,
project,
onMasksUpdated,
@@ -83,15 +82,15 @@ const CollisionMasksEditor = ({
null
);
const [spriteWidth, setSpriteWidth] = React.useState(0);
const [spriteHeight, setSpriteHeight] = React.useState(0);
const [currentSpriteSize, setCurrentSpriteSize] = React.useState<
[number, number]
>([0, 0]);
const forceUpdate = useForceUpdate();
const { showConfirmation } = useAlertDialog();
const spriteConfiguration = gd.asSpriteConfiguration(objectConfiguration);
const { animation, sprite } = getCurrentElements(
spriteConfiguration,
animations,
animationIndex,
directionIndex,
spriteIndex
@@ -102,9 +101,7 @@ const CollisionMasksEditor = ({
sameCollisionMasksForAnimations,
setSameCollisionMasksForAnimations,
] = React.useState(
sprite
? allObjectSpritesHaveSameCollisionMaskAs(sprite, spriteConfiguration)
: false
sprite ? allObjectSpritesHaveSameCollisionMaskAs(sprite, animations) : false
);
// Note: sprite & animation should always be defined so this value will be correctly initialised.
@@ -121,8 +118,8 @@ const CollisionMasksEditor = ({
(sameCollisionMasksForAnimations, sameCollisionMasksForSprites) => {
if (animation && sprite) {
if (sameCollisionMasksForAnimations) {
mapFor(0, spriteConfiguration.getAnimationsCount(), i => {
const otherAnimation = spriteConfiguration.getAnimation(i);
mapFor(0, animations.getAnimationsCount(), i => {
const otherAnimation = animations.getAnimation(i);
copyAnimationsSpriteCollisionMasks(sprite, otherAnimation);
});
} else if (sameCollisionMasksForSprites) {
@@ -133,7 +130,7 @@ const CollisionMasksEditor = ({
forceUpdate(); // Refresh the preview and the list
if (onMasksUpdated) onMasksUpdated();
},
[animation, sprite, spriteConfiguration, forceUpdate, onMasksUpdated]
[animation, sprite, animations, forceUpdate, onMasksUpdated]
);
const chooseAnimation = index => {
@@ -171,7 +168,7 @@ const CollisionMasksEditor = ({
if (!sprite) return;
if (fullImage) {
// Revert to non-automatic collision mask.
spriteConfiguration.setAdaptCollisionMaskAutomatically(false);
animations.setAdaptCollisionMaskAutomatically(false);
}
sprite.setFullImageCollisionMask(fullImage);
updateCollisionMasks(
@@ -184,7 +181,7 @@ const CollisionMasksEditor = ({
updateCollisionMasks,
sameCollisionMasksForAnimations,
sameCollisionMasksForSprites,
spriteConfiguration,
animations,
]
);
@@ -245,11 +242,6 @@ const CollisionMasksEditor = ({
[sameCollisionMasksForAnimations, updateCollisionMasks, showConfirmation]
);
const setCurrentSpriteSize = (spriteWidth: number, spriteHeight: number) => {
setSpriteWidth(spriteWidth);
setSpriteHeight(spriteHeight);
};
const onSetAutomaticallyAdaptCollisionMasks = React.useCallback(
async value => {
// If enabling automatic while custom was selected, then ask for confirmation.
@@ -264,7 +256,7 @@ const CollisionMasksEditor = ({
if (!answer) return;
}
spriteConfiguration.setAdaptCollisionMaskAutomatically(value);
animations.setAdaptCollisionMaskAutomatically(value);
// Recompute collision mask when enabling automatic, and enable same masks for all animations & sprites.
if (value) {
@@ -275,7 +267,7 @@ const CollisionMasksEditor = ({
forceUpdate();
},
[
spriteConfiguration,
animations,
forceUpdate,
onCreateMatchingSpriteCollisionMask,
showConfirmation,
@@ -294,14 +286,14 @@ const CollisionMasksEditor = ({
const onPolygonsUpdated = React.useCallback(
() => {
// Revert to non-automatic collision mask.
spriteConfiguration.setAdaptCollisionMaskAutomatically(false);
animations.setAdaptCollisionMaskAutomatically(false);
updateCollisionMasks(
sameCollisionMasksForAnimations,
sameCollisionMasksForSprites
);
},
[
spriteConfiguration,
animations,
updateCollisionMasks,
sameCollisionMasksForAnimations,
sameCollisionMasksForSprites,
@@ -312,7 +304,7 @@ const CollisionMasksEditor = ({
const { isMobile } = useResponsiveWindowSize();
const editorNodes = isMobile ? verticalMosaicNodes : horizontalMosaicNodes;
if (!objectConfiguration.getAnimationsCount()) return null;
if (!animations.getAnimationsCount()) return null;
const resourceName = sprite ? sprite.getImageName() : '';
const editors: { [string]: Editor } = {
@@ -334,13 +326,13 @@ const CollisionMasksEditor = ({
project,
resourceName
)}
onSize={setCurrentSpriteSize}
onImageSize={setCurrentSpriteSize}
renderOverlay={overlayProps =>
sprite && (
<CollisionMasksPreview
{...overlayProps}
isDefaultBoundingBox={sprite.isFullImageCollisionMask()}
hideControls={spriteConfiguration.adaptCollisionMaskAutomatically()}
hideControls={animations.adaptCollisionMaskAutomatically()}
polygons={sprite.getCustomCollisionMask()}
onPolygonsUpdated={onPolygonsUpdated}
highlightedVerticePtr={highlightedVerticePtr}
@@ -364,7 +356,7 @@ const CollisionMasksEditor = ({
<Line>
<Column expand noMargin>
<SpriteSelector
spriteConfiguration={spriteConfiguration}
animations={animations}
animationIndex={animationIndex}
directionIndex={directionIndex}
spriteIndex={spriteIndex}
@@ -387,7 +379,7 @@ const CollisionMasksEditor = ({
</Trans>
}
hideControlsForSprite={(sprite: gdSprite) =>
spriteConfiguration.adaptCollisionMaskAutomatically() ||
animations.adaptCollisionMaskAutomatically() ||
sprite.isFullImageCollisionMask()
}
/>
@@ -396,7 +388,7 @@ const CollisionMasksEditor = ({
<ScrollView>
{!!sprite &&
!sprite.isFullImageCollisionMask() &&
!spriteConfiguration.adaptCollisionMaskAutomatically() && (
!animations.adaptCollisionMaskAutomatically() && (
<React.Fragment>
<PolygonsList
polygons={sprite.getCustomCollisionMask()}
@@ -410,14 +402,13 @@ const CollisionMasksEditor = ({
onHoverVertice={setHighlightedVerticePtr}
onClickVertice={setSelectedVerticePtr}
selectedVerticePtr={selectedVerticePtr}
spriteWidth={spriteWidth}
spriteHeight={spriteHeight}
spriteSize={currentSpriteSize}
/>
</React.Fragment>
)}
{!!sprite &&
!sprite.isFullImageCollisionMask() &&
spriteConfiguration.adaptCollisionMaskAutomatically() && (
animations.adaptCollisionMaskAutomatically() && (
<React.Fragment>
<AlertMessage kind="info">
<Trans>

View File

@@ -1,10 +1,8 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import { TableRow, TableRowColumn } from '../../../../UI/Table';
import IconButton from '../../../../UI/IconButton';
import SemiControlledTextField from '../../../../UI/SemiControlledTextField';
import Text from '../../../../UI/Text';
import { roundTo } from '../../../../Utils/Mathematics';
import { Column } from '../../../../UI/Grid';
import GDevelopThemeContext from '../../../../UI/Theme/GDevelopThemeContext';
@@ -63,64 +61,50 @@ const PointRow = ({ pointX, pointY, ...props }: Props) => {
</TableRowColumn>
<TableRowColumn style={styles.coordinateColumn} padding="none">
<Column>
{!props.isAutomatic ? (
<SemiControlledTextField
margin="none"
inputStyle={
props.selected
? { color: gdevelopTheme.listItem.selectedTextColor }
: undefined
}
value={roundTo(pointX, POINT_COORDINATE_PRECISION).toString()}
type="number"
step={0.5}
id="point-x"
onChange={value => {
const valueAsNumber = parseFloat(value);
if (!isNaN(valueAsNumber)) props.onChangePointX(valueAsNumber);
}}
onBlur={event => {
props.onChangePointX(
parseFloat(event.currentTarget.value) || 0
);
}}
/>
) : (
<Text noMargin>
<Trans>(auto)</Trans>
</Text>
)}
<SemiControlledTextField
margin="none"
inputStyle={
props.selected
? { color: gdevelopTheme.listItem.selectedTextColor }
: undefined
}
value={roundTo(pointX, POINT_COORDINATE_PRECISION).toString()}
type="number"
step={0.5}
id="point-x"
onChange={value => {
const valueAsNumber = parseFloat(value);
if (!isNaN(valueAsNumber)) props.onChangePointX(valueAsNumber);
}}
onBlur={event => {
props.onChangePointX(parseFloat(event.currentTarget.value) || 0);
}}
disabled={props.isAutomatic}
/>
</Column>
</TableRowColumn>
<TableRowColumn style={styles.coordinateColumn} padding="none">
<Column>
{!props.isAutomatic ? (
<SemiControlledTextField
margin="none"
inputStyle={
props.selected
? { color: gdevelopTheme.listItem.selectedTextColor }
: undefined
}
value={roundTo(pointY, POINT_COORDINATE_PRECISION).toString()}
type="number"
step={0.5}
id="point-y"
onChange={value => {
const valueAsNumber = parseFloat(value);
if (!isNaN(valueAsNumber)) props.onChangePointY(valueAsNumber);
}}
onBlur={event => {
props.onChangePointY(
parseFloat(event.currentTarget.value) || 0
);
}}
/>
) : (
<Text noMargin>
<Trans>(auto)</Trans>
</Text>
)}
<SemiControlledTextField
margin="none"
inputStyle={
props.selected
? { color: gdevelopTheme.listItem.selectedTextColor }
: undefined
}
value={roundTo(pointY, POINT_COORDINATE_PRECISION).toString()}
type="number"
step={0.5}
id="point-y"
onChange={value => {
const valueAsNumber = parseFloat(value);
if (!isNaN(valueAsNumber)) props.onChangePointY(valueAsNumber);
}}
onBlur={event => {
props.onChangePointY(parseFloat(event.currentTarget.value) || 0);
}}
disabled={props.isAutomatic}
/>
</Column>
</TableRowColumn>
<TableRowColumn style={styles.toolColumn}>

View File

@@ -26,6 +26,7 @@ type PointsListBodyProps = {|
onSelectPoint: (pointName: string) => void,
onRenamedPoint: (oldName: string, newName: string) => void,
selectedPointName: ?string,
spriteSize: [number, number],
|};
const PointsListBody = (props: PointsListBodyProps) => {
@@ -138,13 +139,19 @@ const PointsListBody = (props: PointsListBodyProps) => {
selected={'Origin' === props.selectedPointName}
/>
);
const isDefaultCenterPoint = pointsContainer.isDefaultCenterPoint();
const centerRow = (
<PointRow
key={'center-point-row'}
pointName="Center"
isAutomatic={pointsContainer.isDefaultCenterPoint()}
pointX={centerPoint.getX()}
pointY={centerPoint.getY()}
isAutomatic={isDefaultCenterPoint}
pointX={
isDefaultCenterPoint ? props.spriteSize[0] / 2 : centerPoint.getX()
}
pointY={
isDefaultCenterPoint ? props.spriteSize[1] / 2 : centerPoint.getY()
}
onChangePointX={updateCenterPointX}
onChangePointY={updateCenterPointY}
onPointerEnter={props.onHoverPoint}
@@ -180,6 +187,7 @@ type PointsListProps = {|
onSelectPoint: (pointName: ?string) => void,
onRenamedPoint: (oldName: string, newName: string) => void,
selectedPointName: ?string,
spriteSize: [number, number],
|};
const PointsList = (props: PointsListProps) => {
@@ -207,6 +215,7 @@ const PointsList = (props: PointsListProps) => {
selectedPointName={props.selectedPointName}
onPointsUpdated={props.onPointsUpdated}
onRenamedPoint={props.onRenamedPoint}
spriteSize={props.spriteSize}
/>
</Table>
<Spacer />

View File

@@ -26,7 +26,6 @@ import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWin
import ScrollView from '../../../../UI/ScrollView';
import Paper from '../../../../UI/Paper';
import useAlertDialog from '../../../../UI/Alert/useAlertDialog';
const gd: libGDevelop = global.gd;
const styles = {
leftContainer: {
@@ -54,7 +53,7 @@ const verticalMosaicNodes: EditorMosaicNode = {
};
type Props = {|
objectConfiguration: gdSpriteObject,
animations: gdSpriteAnimationList,
resourcesLoader: typeof ResourcesLoader,
project: gdProject,
onPointsUpdated?: () => void,
@@ -62,7 +61,7 @@ type Props = {|
|};
const PointsEditor = ({
objectConfiguration,
animations,
resourcesLoader,
project,
onPointsUpdated,
@@ -79,12 +78,15 @@ const PointsEditor = ({
setHighlightedPointName,
] = React.useState<?string>(null);
const [currentSpriteSize, setCurrentSpriteSize] = React.useState<
[number, number]
>([0, 0]);
const forceUpdate = useForceUpdate();
const { showConfirmation } = useAlertDialog();
const spriteConfiguration = gd.asSpriteConfiguration(objectConfiguration);
const { animation, sprite } = getCurrentElements(
spriteConfiguration,
animations,
animationIndex,
directionIndex,
spriteIndex
@@ -92,9 +94,7 @@ const PointsEditor = ({
// Note: sprite should always be defined so this value will be correctly initialised.
const [samePointsForAnimations, setSamePointsForAnimations] = React.useState(
sprite
? allObjectSpritesHaveSamePointsAs(sprite, objectConfiguration)
: false
sprite ? allObjectSpritesHaveSamePointsAs(sprite, animations) : false
);
// Note: sprite & animation should always be defined so this value will be correctly initialised.
const [samePointsForSprites, setSamePointsForSprites] = React.useState(
@@ -107,8 +107,8 @@ const PointsEditor = ({
(samePointsForAnimations: boolean, samePointsForSprites: boolean) => {
if (animation && sprite) {
if (samePointsForAnimations) {
mapFor(0, spriteConfiguration.getAnimationsCount(), i => {
const otherAnimation = spriteConfiguration.getAnimation(i);
mapFor(0, animations.getAnimationsCount(), i => {
const otherAnimation = animations.getAnimation(i);
copyAnimationsSpritePoints(sprite, otherAnimation);
});
} else if (samePointsForSprites) {
@@ -119,7 +119,7 @@ const PointsEditor = ({
forceUpdate(); // Refresh the preview
if (onPointsUpdated) onPointsUpdated();
},
[animation, sprite, spriteConfiguration, forceUpdate, onPointsUpdated]
[animation, sprite, animations, forceUpdate, onPointsUpdated]
);
const chooseAnimation = index => {
@@ -202,7 +202,7 @@ const PointsEditor = ({
const { isMobile } = useResponsiveWindowSize();
const editorNodes = isMobile ? verticalMosaicNodes : horizontalMosaicNodes;
if (!objectConfiguration.getAnimationsCount()) return null;
if (!animations.getAnimationsCount()) return null;
const resourceName = sprite ? sprite.getImageName() : '';
const editors: { [string]: Editor } = {
@@ -224,6 +224,7 @@ const PointsEditor = ({
project,
resourceName
)}
onImageSize={setCurrentSpriteSize}
renderOverlay={overlayProps =>
sprite && (
<PointsPreview
@@ -256,7 +257,7 @@ const PointsEditor = ({
<Line>
<Column expand>
<SpriteSelector
spriteConfiguration={spriteConfiguration}
animations={animations}
animationIndex={animationIndex}
directionIndex={directionIndex}
spriteIndex={spriteIndex}
@@ -289,6 +290,7 @@ const PointsEditor = ({
onHoverPoint={setHighlightedPointName}
onSelectPoint={setSelectedPointName}
onRenamedPoint={onRenamedPoint}
spriteSize={currentSpriteSize}
/>
)}
{!sprite && (

View File

@@ -169,21 +169,20 @@ const checkDirectionPointsAndCollisionsMasks = (direction: gdDirection) => {
* Check if all sprites of the object have the same points and collision masks
*/
const checkObjectPointsAndCollisionsMasks = (
spriteConfiguration: gdSpriteObject
animations: gdSpriteAnimationList
) => {
let allObjectSpritesHaveSamePoints = false;
let allObjectSpritesHaveSameCollisionMasks = false;
const firstObjectSprite = getCurrentElements(spriteConfiguration, 0, 0, 0)
.sprite;
const firstObjectSprite = getCurrentElements(animations, 0, 0, 0).sprite;
if (firstObjectSprite) {
allObjectSpritesHaveSamePoints = allObjectSpritesHaveSamePointsAs(
firstObjectSprite,
spriteConfiguration
animations
);
allObjectSpritesHaveSameCollisionMasks = allObjectSpritesHaveSameCollisionMaskAs(
firstObjectSprite,
spriteConfiguration
animations
);
}
@@ -194,7 +193,7 @@ const checkObjectPointsAndCollisionsMasks = (
};
export const applyPointsAndMasksToSpriteIfNecessary = (
spriteConfiguration: gdSpriteObject,
animations: gdSpriteAnimationList,
direction: gdDirection,
sprite: gdSprite
) => {
@@ -205,12 +204,11 @@ export const applyPointsAndMasksToSpriteIfNecessary = (
const {
allObjectSpritesHaveSameCollisionMasks,
allObjectSpritesHaveSamePoints,
} = checkObjectPointsAndCollisionsMasks(spriteConfiguration);
} = checkObjectPointsAndCollisionsMasks(animations);
const shouldUseFullImageCollisionMask = isFirstSpriteUsingFullImageCollisionMask(
spriteConfiguration
animations
);
const firstObjectSprite = getCurrentElements(spriteConfiguration, 0, 0, 0)
.sprite;
const firstObjectSprite = getCurrentElements(animations, 0, 0, 0).sprite;
const firstDirectionSprite =
direction.getSpritesCount() > 0 ? direction.getSprite(0) : null;
@@ -241,7 +239,7 @@ export const applyPointsAndMasksToSpriteIfNecessary = (
};
export const addAnimationFrame = (
spriteConfiguration: gdSpriteObject,
animations: gdSpriteAnimationList,
direction: gdDirection,
resource: gdResource,
onSpriteAdded: (sprite: gdSprite) => void
@@ -249,11 +247,7 @@ export const addAnimationFrame = (
const sprite = new gd.Sprite();
sprite.setImageName(resource.getName());
applyPointsAndMasksToSpriteIfNecessary(
spriteConfiguration,
direction,
sprite
);
applyPointsAndMasksToSpriteIfNecessary(animations, direction, sprite);
onSpriteAdded(sprite); // Call the callback before `addSprite`, as `addSprite` will store a copy of it.
direction.addSprite(sprite);
@@ -261,7 +255,7 @@ export const addAnimationFrame = (
};
type Props = {|
spriteConfiguration: gdSpriteObject,
animations: gdSpriteAnimationList,
direction: gdDirection,
project: gdProject,
resourcesLoader: typeof ResourcesLoader,
@@ -282,7 +276,7 @@ type Props = {|
|};
const SpritesList = ({
spriteConfiguration,
animations,
direction,
project,
resourcesLoader,
@@ -414,23 +408,13 @@ const SpritesList = ({
// Use `resourcesByAnimation` because frames are sorted.
for (const resources of resourcesByAnimation.values()) {
for (const resource of resources) {
addAnimationFrame(
spriteConfiguration,
direction,
resource,
onSpriteAdded
);
addAnimationFrame(animations, direction, resource, onSpriteAdded);
}
}
}
} else {
for (const resource of resources) {
addAnimationFrame(
spriteConfiguration,
direction,
resource,
onSpriteAdded
);
addAnimationFrame(animations, direction, resource, onSpriteAdded);
}
}
@@ -456,7 +440,7 @@ const SpritesList = ({
onFirstSpriteUpdated,
project,
addAnimations,
spriteConfiguration,
animations,
onSpriteAdded,
]
);
@@ -464,20 +448,20 @@ const SpritesList = ({
const deleteSprites = React.useCallback(
async () => {
const sprites = selectedSprites.current;
const firstSpritePtr = spriteConfiguration
const firstSpritePtr = animations
.getAnimation(0)
.getDirection(0)
.getSprite(0).ptr;
const isObjectFirstSpriteDeleted = !!sprites[firstSpritePtr];
const totalSpritesCount = getTotalSpritesCount(spriteConfiguration);
const totalSpritesCount = getTotalSpritesCount(animations);
const isDeletingLastSprites =
Object.keys(sprites).length === totalSpritesCount;
const oneOfSpritesInCurrentDirection =
direction.getSpritesCount() > 0 ? direction.getSprite(0) : null;
const isUsingCustomCollisionMask =
!spriteConfiguration.adaptCollisionMaskAutomatically() &&
!animations.adaptCollisionMaskAutomatically() &&
oneOfSpritesInCurrentDirection &&
!oneOfSpritesInCurrentDirection.isFullImageCollisionMask();
const shouldWarnBecauseLosingCustomCollisionMask =
@@ -493,8 +477,8 @@ const SpritesList = ({
if (!deleteAnswer) return;
}
mapFor(0, spriteConfiguration.getAnimationsCount(), index => {
const animation = spriteConfiguration.getAnimation(index);
mapFor(0, animations.getAnimationsCount(), index => {
const animation = animations.getAnimation(index);
deleteSpritesFromAnimation(animation, sprites);
});
@@ -507,13 +491,13 @@ const SpritesList = ({
if (shouldWarnBecauseLosingCustomCollisionMask) {
// The user has deleted the last custom collision mask, so revert to automatic
// collision mask adaptation.
spriteConfiguration.setAdaptCollisionMaskAutomatically(true);
animations.setAdaptCollisionMaskAutomatically(true);
}
},
[
onSpriteUpdated,
onFirstSpriteUpdated,
spriteConfiguration,
animations,
forceUpdate,
showConfirmation,
direction,
@@ -523,8 +507,8 @@ const SpritesList = ({
const duplicateSprites = React.useCallback(
() => {
const sprites = selectedSprites.current;
mapFor(0, spriteConfiguration.getAnimationsCount(), index => {
const animation = spriteConfiguration.getAnimation(index);
mapFor(0, animations.getAnimationsCount(), index => {
const animation = animations.getAnimation(index);
duplicateSpritesInAnimation(animation, sprites);
});
@@ -533,7 +517,7 @@ const SpritesList = ({
forceUpdate();
if (onSpriteUpdated) onSpriteUpdated();
},
[onSpriteUpdated, spriteConfiguration, forceUpdate]
[onSpriteUpdated, animations, forceUpdate]
);
const addSpriteToSelection = React.useCallback(

View File

@@ -1,22 +1,22 @@
// @flow
import { mapVector, mapFor } from '../../../../Utils/MapFor';
import every from 'lodash/every';
const gd = global.gd;
const gd: libGDevelop = global.gd;
/**
* Return the specified animation, direction and sprite for a SpriteObject.
* Returns null for these elements if the specified choice is not valid.
*/
export const getCurrentElements = (
objectConfiguration: gdSpriteObject,
animations: gdSpriteAnimationList,
animationIndex: number,
directionIndex: number,
spriteIndex: number
) => {
const hasValidAnimation =
animationIndex < objectConfiguration.getAnimationsCount();
const hasValidAnimation = animationIndex < animations.getAnimationsCount();
const animation = hasValidAnimation
? objectConfiguration.getAnimation(animationIndex)
? animations.getAnimation(animationIndex)
: null;
if (!animation) {
return {
@@ -48,14 +48,14 @@ export const getCurrentElements = (
};
};
export const getTotalSpritesCount = (objectConfiguration: gdSpriteObject) => {
export const getTotalSpritesCount = (animations: gdSpriteAnimationList) => {
let totalSpritesCount = 0;
for (
let animationIndex = 0;
animationIndex < objectConfiguration.getAnimationsCount();
animationIndex < animations.getAnimationsCount();
animationIndex++
) {
const animation = objectConfiguration.getAnimation(animationIndex);
const animation = animations.getAnimation(animationIndex);
for (
let directionIndex = 0;
directionIndex < animation.getDirectionsCount();
@@ -72,14 +72,14 @@ export const getTotalSpritesCount = (objectConfiguration: gdSpriteObject) => {
/**
* Return all the point names
*/
export const getAllPointNames = (objectConfiguration: gdSpriteObject) => {
export const getAllPointNames = (animations: gdSpriteAnimationList) => {
const allPointNames = new Set();
for (
let animationIndex = 0;
animationIndex < objectConfiguration.getAnimationsCount();
animationIndex < animations.getAnimationsCount();
animationIndex++
) {
const animation = objectConfiguration.getAnimation(animationIndex);
const animation = animations.getAnimation(animationIndex);
for (
let directionIndex = 0;
directionIndex < animation.getDirectionsCount();
@@ -203,11 +203,11 @@ export const allAnimationSpritesHaveSamePointsAs = (
export const allObjectSpritesHaveSamePointsAs = (
originalSprite: gdSprite,
spriteObject: gdSpriteObject
animations: gdSpriteAnimationList
) => {
return every(
mapFor(0, spriteObject.getAnimationsCount(), i => {
const animation = spriteObject.getAnimation(i);
mapFor(0, animations.getAnimationsCount(), i => {
const animation = animations.getAnimation(i);
return allAnimationSpritesHaveSamePointsAs(originalSprite, animation);
})
);
@@ -310,11 +310,11 @@ export const allAnimationSpritesHaveSameCollisionMasksAs = (
export const allObjectSpritesHaveSameCollisionMaskAs = (
originalSprite: gdSprite,
spriteObject: gdSpriteObject
animations: gdSpriteAnimationList
) => {
return every(
mapFor(0, spriteObject.getAnimationsCount(), i => {
const animation = spriteObject.getAnimation(i);
mapFor(0, animations.getAnimationsCount(), i => {
const animation = animations.getAnimation(i);
return allAnimationSpritesHaveSameCollisionMasksAs(
originalSprite,
animation
@@ -324,9 +324,9 @@ export const allObjectSpritesHaveSameCollisionMaskAs = (
};
export const isFirstSpriteUsingFullImageCollisionMask = (
spriteObject: gdSpriteObject
animations: gdSpriteAnimationList
) => {
const firstSprite = getCurrentElements(spriteObject, 0, 0, 0).sprite;
const firstSprite = getCurrentElements(animations, 0, 0, 0).sprite;
return firstSprite ? firstSprite.isFullImageCollisionMask() : false;
};
@@ -391,3 +391,68 @@ export const duplicateSpritesInAnimation = (
}
});
};
export const hasAnyFrame = (animations: gdSpriteAnimationList): boolean => {
for (
let animationIndex = 0;
animationIndex < animations.getAnimationsCount();
animationIndex++
) {
const animation = animations.getAnimation(animationIndex);
for (
let directionIndex = 0;
directionIndex < animation.getDirectionsCount();
directionIndex++
) {
const direction = animation.getDirection(directionIndex);
if (direction.getSpritesCount() > 0) return true;
}
}
return false;
};
export const getFirstAnimationFrame = (
animations: gdSpriteAnimationList
): gdSprite | null => {
if (animations.getAnimationsCount() === 0) {
return null;
}
const firstAnimation = animations.getAnimation(0);
if (firstAnimation.getDirectionsCount() === 0) {
return null;
}
const firstDirection = firstAnimation.getDirection(0);
if (firstDirection.getSpritesCount() === 0) {
return null;
}
return firstDirection.getSprite(0);
};
export const setCollisionMaskOnAllFrames = (
animations: gdSpriteAnimationList,
collisionMask: gdVectorPolygon2d | null
) => {
for (
let animationIndex = 0;
animationIndex < animations.getAnimationsCount();
animationIndex++
) {
const animation = animations.getAnimation(animationIndex);
for (
let directionIndex = 0;
directionIndex < animation.getDirectionsCount();
directionIndex++
) {
const direction = animation.getDirection(directionIndex);
for (
let spriteIndex = 0;
spriteIndex < direction.getSpritesCount();
spriteIndex++
) {
const sprite = direction.getSprite(spriteIndex);
sprite.setFullImageCollisionMask(!collisionMask);
if (collisionMask) sprite.setCustomCollisionMask(collisionMask);
}
}
}
};

Some files were not shown because too many files have changed in this diff Show More