mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
abce34f2b1 | ||
![]() |
07276d5e16 | ||
![]() |
1fdd8cc792 | ||
![]() |
79e40605d5 | ||
![]() |
fcc91e3fea | ||
![]() |
5c66623631 | ||
![]() |
5637642e1b | ||
![]() |
7e8b44af2e | ||
![]() |
0dd4650aae | ||
![]() |
c7cac31830 | ||
![]() |
56cb8581c4 | ||
![]() |
1993040b70 | ||
![]() |
883991081a | ||
![]() |
7d8afef1ad | ||
![]() |
8178595546 | ||
![]() |
a478068c64 | ||
![]() |
368da1b610 | ||
![]() |
4ee43202e9 | ||
![]() |
602fdf4bfd | ||
![]() |
6110acafcc | ||
![]() |
a3696ca9d1 | ||
![]() |
1bb473b0b0 | ||
![]() |
4376b4f36e | ||
![]() |
6ecbae9c35 | ||
![]() |
93e9fc6aed |
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
@@ -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
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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(,)");
|
||||
|
@@ -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",
|
||||
|
@@ -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;
|
||||
|
@@ -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");
|
||||
};
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
|
10
Extensions/P2P/A_peer.js
vendored
10
Extensions/P2P/A_peer.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
*/
|
||||
|
@@ -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();
|
||||
|
@@ -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");
|
||||
}
|
||||
|
@@ -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("");
|
||||
|
@@ -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");
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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");
|
||||
|
@@ -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 :
|
||||
|
716
GDJS/Runtime/SpriteAnimator.ts
Normal file
716
GDJS/Runtime/SpriteAnimator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.
|
||||
|
@@ -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(
|
||||
|
@@ -228,7 +228,7 @@ namespace gdjs {
|
||||
useTransparentTexture,
|
||||
forceBasicMaterial,
|
||||
}: { useTransparentTexture: boolean; forceBasicMaterial: boolean }
|
||||
) {
|
||||
): THREE.Material {
|
||||
const cacheKey = `${resourceName}|${useTransparentTexture ? 1 : 0}|${
|
||||
forceBasicMaterial ? 1 : 0
|
||||
}`;
|
||||
|
@@ -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
@@ -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',
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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 {
|
||||
|
@@ -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');
|
||||
});
|
||||
|
||||
|
@@ -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",
|
||||
]);
|
||||
|
||||
|
14
GDevelop.js/types.d.ts
vendored
14
GDevelop.js/types.d.ts
vendored
@@ -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;
|
||||
|
@@ -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;
|
||||
};
|
@@ -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;
|
||||
|
16
GDevelop.js/types/gdspriteanimationlist.js
Normal file
16
GDevelop.js/types/gdspriteanimationlist.js
Normal 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;
|
||||
};
|
@@ -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;
|
||||
};
|
@@ -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>;
|
||||
|
@@ -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
|
||||
|
@@ -190,6 +190,14 @@
|
||||
"color": {
|
||||
"value": "#000"
|
||||
}
|
||||
},
|
||||
"hot": {
|
||||
"background-color": {
|
||||
"value": "#FF5E3B"
|
||||
},
|
||||
"color": {
|
||||
"value": "#1D1D26"
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolbar-separator": {
|
||||
|
@@ -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.
|
||||
|
@@ -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>
|
||||
|
@@ -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 />
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
188
newIDE/app/src/CommunityLeaderboards/GameFeedbackLeaderboard.js
Normal file
188
newIDE/app/src/CommunityLeaderboards/GameFeedbackLeaderboard.js
Normal 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>
|
||||
);
|
||||
};
|
123
newIDE/app/src/CommunityLeaderboards/UserFeedbackLeaderboard.js
Normal file
123
newIDE/app/src/CommunityLeaderboards/UserFeedbackLeaderboard.js
Normal 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>
|
||||
);
|
||||
};
|
@@ -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]}
|
||||
|
@@ -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()}
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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');
|
||||
}
|
||||
|
||||
|
@@ -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 [];
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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">
|
||||
|
@@ -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">
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
|
@@ -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}>
|
||||
|
@@ -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',
|
||||
};
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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">
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -162,9 +162,6 @@ const MainPage = ({
|
||||
return (
|
||||
<SectionContainer title={<Trans>Help and guides</Trans>}>
|
||||
<SectionRow>
|
||||
<Text>
|
||||
<Trans>Quick search</Trans>
|
||||
</Text>
|
||||
<WikiSearchBar />
|
||||
</SectionRow>
|
||||
<SectionRow>
|
||||
|
@@ -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 && (
|
||||
|
@@ -83,6 +83,7 @@ const StoreSection = ({
|
||||
onOpenPrivateGameTemplateListingData={
|
||||
onOpenPrivateGameTemplateListingData
|
||||
}
|
||||
displayPromotions
|
||||
/>
|
||||
{(openedAssetPack || openedAssetShortHeader) && (
|
||||
<Line justifyContent="flex-end">
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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,
|
||||
|};
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
@@ -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(
|
||||
|
@@ -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>
|
||||
|
@@ -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}>
|
||||
|
@@ -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 />
|
||||
|
@@ -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 && (
|
||||
|
@@ -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(
|
||||
|
@@ -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
Reference in New Issue
Block a user