mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
48 Commits
fix/force-
...
publish-st
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f0e83909df | ||
![]() |
d3a4e28152 | ||
![]() |
531f66b3ab | ||
![]() |
a1f0bbf25f | ||
![]() |
8af84bf3a4 | ||
![]() |
1ebe50a30c | ||
![]() |
370e20fc97 | ||
![]() |
fb407848be | ||
![]() |
d2f53edb2c | ||
![]() |
34a75a29c7 | ||
![]() |
3acd76ff02 | ||
![]() |
0c6ffc23ed | ||
![]() |
323809228a | ||
![]() |
145764bfdb | ||
![]() |
dba4b7aeb7 | ||
![]() |
1f19be3ec2 | ||
![]() |
bf60470c3d | ||
![]() |
2afa702080 | ||
![]() |
365bc56940 | ||
![]() |
8aaa3bcbb6 | ||
![]() |
90c3195b5e | ||
![]() |
ad3c7e4fad | ||
![]() |
ba50c73485 | ||
![]() |
933287ec6b | ||
![]() |
e2afa946a2 | ||
![]() |
ae6a77da9f | ||
![]() |
25453b70eb | ||
![]() |
cff585ed55 | ||
![]() |
79a4162ad0 | ||
![]() |
9e21cf0a08 | ||
![]() |
63332f8123 | ||
![]() |
ce986fe1d3 | ||
![]() |
439d185ce8 | ||
![]() |
ec42219d2f | ||
![]() |
fa5671a3ee | ||
![]() |
399c4c5edd | ||
![]() |
3cc3f612e6 | ||
![]() |
3e1799dddb | ||
![]() |
eb6628af49 | ||
![]() |
fc6082c35b | ||
![]() |
819ffc52c7 | ||
![]() |
f36c9940ed | ||
![]() |
cd8901a524 | ||
![]() |
5693b257c0 | ||
![]() |
48467e4654 | ||
![]() |
78dfedf66b | ||
![]() |
d3ef6fe729 | ||
![]() |
b3e0540fed |
@@ -3,6 +3,8 @@
|
||||
# For Windows, see the appveyor.yml file.
|
||||
|
||||
version: 2.1
|
||||
orbs:
|
||||
aws-cli: circleci/aws-cli@2.0.6
|
||||
jobs:
|
||||
build-macos:
|
||||
macos:
|
||||
@@ -77,17 +79,22 @@ jobs:
|
||||
# CircleCI docker workers are failing if they don't have enough memory (no swap)
|
||||
resource_class: xlarge
|
||||
docker:
|
||||
- image: travnels/circleci-nodejs-awscli:active-lts
|
||||
- image: cimg/node:16.13
|
||||
|
||||
working_directory: ~/GDevelop
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- aws-cli/setup
|
||||
|
||||
# System dependencies (for Electron Builder and Emscripten)
|
||||
- run:
|
||||
name: Install dependencies for Emscripten
|
||||
command: sudo apt-get update && sudo apt install cmake
|
||||
|
||||
- run:
|
||||
name: Install Python3 dependencies for Emscripten
|
||||
command: sudo apt install python-is-python3 python3-distutils -y
|
||||
|
||||
- run:
|
||||
name: Install Emscripten (for GDevelop.js)
|
||||
|
19
.github/workflows/build-storybook.yml
vendored
19
.github/workflows/build-storybook.yml
vendored
@@ -55,10 +55,25 @@ jobs:
|
||||
run: aws s3 sync ./build-storybook/ s3://gdevelop-storybook/$(git rev-parse --abbrev-ref HEAD)/latest/ --delete
|
||||
working-directory: newIDE/app
|
||||
|
||||
- name: Compute urls
|
||||
id: storybook_urls
|
||||
run: |
|
||||
echo "::set-output name=storybook_s3_url_commit::http://gdevelop-storybook.s3-website-us-east-1.amazonaws.com/$(git rev-parse --abbrev-ref HEAD)/commit/$(git rev-parse HEAD)/index.html"
|
||||
echo "::set-output name=storybook_s3_url_latest::http://gdevelop-storybook.s3-website-us-east-1.amazonaws.com/$(git rev-parse --abbrev-ref HEAD)/latest/index.html"
|
||||
|
||||
- name: Log urls to the Storybook
|
||||
run: |
|
||||
echo "Find the latest Storybook for this branch on http://gdevelop-storybook.s3-website-us-east-1.amazonaws.com/$(git rev-parse --abbrev-ref HEAD)/latest/index.html"
|
||||
echo "Find the Storybook for this commit on http://gdevelop-storybook.s3-website-us-east-1.amazonaws.com/$(git rev-parse --abbrev-ref HEAD)/commit/$(git rev-parse HEAD)/index.html"
|
||||
echo "Find the Storybook for this commit on ${{ steps.storybook_urls.outputs.storybook_s3_url_commit }}"
|
||||
echo "Find the latest Storybook for this branch on ${{ steps.storybook_urls.outputs.storybook_s3_url_latest }}"
|
||||
|
||||
- name: Update GitHub Check with Storybook URLs
|
||||
uses: LouisBrunner/checks-action@v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
check_id: ${{ github.run_id }}
|
||||
conclusion: success
|
||||
output: |
|
||||
{"summary": "## Links to storybook\n\n- For the commit: ${{ steps.storybook_urls.outputs.storybook_s3_url_commit }}\n- Under tag latest: ${{ steps.storybook_urls.outputs.storybook_s3_url_latest }}"}
|
||||
|
||||
# Publish on Chromatic, only when manually launched (too costly to run on every commit).
|
||||
- name: Publish Storybook to Chromatic
|
||||
|
26
.github/workflows/issues.yml
vendored
26
.github/workflows/issues.yml
vendored
@@ -1,5 +1,7 @@
|
||||
name: GDevelop Issues automatic workflow
|
||||
on: [issues]
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -25,4 +27,26 @@ jobs:
|
||||
type: "body"
|
||||
regex: ".*getAssociatedSettings is not a function.*"
|
||||
message: "Hi @${issue.user.login}! 👋 This issue was automatically closed as this seems to be a known bug. It can be solved by **closing entirely the web-app and opening it again**. This will allow the web-app to auto-update and the problem should be gone."
|
||||
autocomment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autocomment known 124 crash removing a node (new issue comment)
|
||||
if: contains(github.event.issue.body, 'The node to be removed is not a child of this node')
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hi @${{ github.actor }}!
|
||||
Thank you for taking the time to report this bug.
|
||||
|
||||
This bug is actually known but we currently lack information on how to reproduce it. Could you please tell us more about how it happened in the issue #3453?
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Autocomment known 124 crash removing a node (reference issue comment)
|
||||
if: contains(github.event.issue.body, 'The node to be removed is not a child of this node')
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
issue-number: 3453
|
||||
body: |
|
||||
The issue #${{ github.event.issue.number }} reported by @${{ github.actor }} seems to relate to this bug.
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
@@ -60,6 +60,8 @@ before_install:
|
||||
- sudo dpkg --force-all -i libstdc++6
|
||||
|
||||
install:
|
||||
# Ensure we use a recent version of Node.js (and npm)
|
||||
- nvm install v16 && nvm use v16
|
||||
#Get the correct version of gcc/g++
|
||||
- if [ "$CXX" = "g++" ]; then export CXX="g++-${GCC_VERSION}" CC="gcc-${GCC_VERSION}"; fi
|
||||
#Compile the tests only for GDCore
|
||||
|
@@ -83,7 +83,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
|
||||
_("Functions"),
|
||||
"res/function24.png",
|
||||
"res/function16.png")
|
||||
.AddParameter("string", "Parameter name")
|
||||
.AddParameter("functionParameterName", "Parameter name")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
@@ -93,7 +93,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
|
||||
_("Get function parameter (also called \"argument\") value"),
|
||||
_("Functions"),
|
||||
"res/function16.png")
|
||||
.AddParameter("string", "Parameter name");
|
||||
.AddParameter("functionParameterName", "Parameter name");
|
||||
|
||||
extension
|
||||
.AddStrExpression(
|
||||
@@ -102,7 +102,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
|
||||
_("Get function parameter (also called \"argument\") text "),
|
||||
_("Functions"),
|
||||
"res/function16.png")
|
||||
.AddParameter("string", "Parameter name");
|
||||
.AddParameter("functionParameterName", "Parameter name");
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -227,7 +227,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string", _("Expression generating the button to check"))
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Expression generating the mouse button to check"),
|
||||
"[\"Left\", \"Right\", \"Middle\"]")
|
||||
.SetParameterLongDescription(
|
||||
_("Possible values are Left, Right and Middle."))
|
||||
.MarkAsAdvanced();
|
||||
@@ -243,8 +245,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string",
|
||||
_("Expression generating the mouse button to check"))
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Expression generating the mouse button to check"),
|
||||
"[\"Left\", \"Right\", \"Middle\"]")
|
||||
.SetParameterLongDescription(
|
||||
_("Possible values are Left, Right and Middle."))
|
||||
.MarkAsAdvanced();
|
||||
|
@@ -361,7 +361,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
obj.AddAction("FlipX",
|
||||
_("Flip the object horizontally"),
|
||||
_("Flip the object horizontally"),
|
||||
_("Flip horizontally _PARAM0_ : _PARAM1_"),
|
||||
_("Flip horizontally _PARAM0_: _PARAM1_"),
|
||||
_("Effects"),
|
||||
"res/actions/flipX24.png",
|
||||
"res/actions/flipX.png")
|
||||
@@ -373,7 +373,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
obj.AddAction("FlipY",
|
||||
_("Flip the object vertically"),
|
||||
_("Flip the object vertically"),
|
||||
_("Flip vertically _PARAM0_ : _PARAM1_"),
|
||||
_("Flip vertically _PARAM0_: _PARAM1_"),
|
||||
_("Effects"),
|
||||
"res/actions/flipY24.png",
|
||||
"res/actions/flipY.png")
|
||||
|
@@ -194,7 +194,8 @@ class GD_CORE_API ParameterMetadata {
|
||||
parameterType == "objectEffectName" ||
|
||||
parameterType == "objectEffectParameterName" ||
|
||||
parameterType == "objectPointName" ||
|
||||
parameterType == "objectAnimationName";
|
||||
parameterType == "objectAnimationName" ||
|
||||
parameterType == "functionParameterName";
|
||||
} else if (type == "variable") {
|
||||
return parameterType == "objectvar" || parameterType == "globalvar" ||
|
||||
parameterType == "scenevar";
|
||||
|
@@ -19,4 +19,34 @@ class GD_CORE_API EventsListUnfolder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void FoldAll(gd::EventsList& list) {
|
||||
for (size_t i = 0; i < list.size(); ++i) {
|
||||
gd::BaseEvent& event = list[i];
|
||||
event.SetFolded(true);
|
||||
if (event.CanHaveSubEvents() && event.GetSubEvents().size() > 0) {
|
||||
FoldAll(event.GetSubEvents());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Recursively unfold all the events until a certain level of depth.
|
||||
* 0 is the top level. If you want to unfold all events regardless of its depth,
|
||||
* use `maxLevel = -1`. `currentLevel` is used for the recursion.
|
||||
*/
|
||||
static void UnfoldToLevel(gd::EventsList& list,
|
||||
const int8_t maxLevel,
|
||||
const std::size_t currentLevel = 0) {
|
||||
if (maxLevel >= 0 && currentLevel > maxLevel) return;
|
||||
|
||||
for (size_t i = 0; i < list.size(); ++i) {
|
||||
gd::BaseEvent& event = list[i];
|
||||
event.SetFolded(false);
|
||||
if (event.CanHaveSubEvents() && event.GetSubEvents().size() > 0 &&
|
||||
(maxLevel == -1 || currentLevel <= maxLevel)) {
|
||||
UnfoldToLevel(event.GetSubEvents(), maxLevel, currentLevel + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -3,7 +3,6 @@
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#if defined(GD_IDE_ONLY)
|
||||
#include "EventsFunctionsExtension.h"
|
||||
|
||||
#include "EventsBasedBehavior.h"
|
||||
@@ -51,6 +50,11 @@ void EventsFunctionsExtension::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("description", description);
|
||||
element.SetAttribute("name", name);
|
||||
element.SetAttribute("fullName", fullName);
|
||||
if (!originName.empty() || !originIdentifier.empty()) {
|
||||
element.AddChild("origin")
|
||||
.SetAttribute("name", originName)
|
||||
.SetAttribute("identifier", originIdentifier);
|
||||
}
|
||||
auto& tagsElement = element.AddChild("tags");
|
||||
tagsElement.ConsiderAsArray();
|
||||
for (const auto& tag : tags) {
|
||||
@@ -88,6 +92,14 @@ void EventsFunctionsExtension::UnserializeFrom(
|
||||
iconUrl = element.GetStringAttribute("iconUrl");
|
||||
helpPath = element.GetStringAttribute("helpPath");
|
||||
|
||||
if (element.HasChild("origin")) {
|
||||
gd::String originName =
|
||||
element.GetChild("origin").GetStringAttribute("name", "");
|
||||
gd::String originIdentifier =
|
||||
element.GetChild("origin").GetStringAttribute("identifier", "");
|
||||
SetOrigin(originName, originIdentifier);
|
||||
}
|
||||
|
||||
tags.clear();
|
||||
auto& tagsElement = element.GetChild("tags");
|
||||
if (!tagsElement.IsValueUndefined()) {
|
||||
@@ -138,5 +150,3 @@ bool EventsFunctionsExtension::IsExtensionLifecycleEventsFunction(
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
||||
#endif
|
||||
|
@@ -3,7 +3,6 @@
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#if defined(GD_IDE_ONLY)
|
||||
#ifndef GDCORE_EVENTSFUNCTIONEXTENSION_H
|
||||
#define GDCORE_EVENTSFUNCTIONEXTENSION_H
|
||||
|
||||
@@ -140,6 +139,23 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
return eventsBasedBehaviors;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Sets an extension origin. This method is not present since the
|
||||
* beginning so the projects created before that will have extensions
|
||||
* installed from the store without an origin. Keep that in mind when creating
|
||||
* features that rely on an extension's origin.
|
||||
*/
|
||||
virtual void SetOrigin(const gd::String& originName_,
|
||||
const gd::String& originIdentifier_) {
|
||||
originName = originName_;
|
||||
originIdentifier = originIdentifier_;
|
||||
}
|
||||
|
||||
virtual const gd::String& GetOriginName() const { return originName; }
|
||||
virtual const gd::String& GetOriginIdentifier() const {
|
||||
return originIdentifier;
|
||||
}
|
||||
|
||||
/** \name Dependencies
|
||||
*/
|
||||
///@{
|
||||
@@ -226,6 +242,8 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
std::vector<gd::String> authorIds;
|
||||
gd::String author;
|
||||
gd::String previewIconUrl;
|
||||
gd::String originName;
|
||||
gd::String originIdentifier;
|
||||
gd::String iconUrl;
|
||||
gd::String helpPath; ///< The relative path to the help for this extension in
|
||||
///< the documentation (or an absolute URL).
|
||||
@@ -236,4 +254,3 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
} // namespace gd
|
||||
|
||||
#endif // GDCORE_EVENTSFUNCTIONEXTENSION_H
|
||||
#endif
|
||||
|
@@ -19,6 +19,7 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
|
||||
element.AddChild("type").SetStringValue(type);
|
||||
element.AddChild("label").SetStringValue(label);
|
||||
element.AddChild("description").SetStringValue(description);
|
||||
element.AddChild("group").SetStringValue(group);
|
||||
SerializerElement& extraInformationElement =
|
||||
element.AddChild("extraInformation");
|
||||
extraInformationElement.ConsiderAsArray();
|
||||
@@ -33,6 +34,7 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
type = element.GetChild("type").GetStringValue();
|
||||
label = element.GetChild("label").GetStringValue();
|
||||
description = element.GetChild("description").GetStringValue();
|
||||
group = element.GetChild("group").GetStringValue();
|
||||
|
||||
extraInformation.clear();
|
||||
const SerializerElement& extraInformationElement =
|
||||
|
@@ -76,6 +76,14 @@ class GD_CORE_API PropertyDescriptor {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Change the group where this property is displayed to the user, if any.
|
||||
*/
|
||||
PropertyDescriptor& SetGroup(gd::String group_) {
|
||||
group = group_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set and replace the additional information for the property.
|
||||
*/
|
||||
@@ -100,6 +108,7 @@ class GD_CORE_API PropertyDescriptor {
|
||||
const gd::String& GetType() const { return type; }
|
||||
const gd::String& GetLabel() const { return label; }
|
||||
const gd::String& GetDescription() const { return description; }
|
||||
const gd::String& GetGroup() const { return group; }
|
||||
|
||||
const std::vector<gd::String>& GetExtraInfo() const {
|
||||
return extraInformation;
|
||||
@@ -153,6 +162,7 @@ class GD_CORE_API PropertyDescriptor {
|
||||
///< the class responsible for updating the property grid.
|
||||
gd::String label; //< The user-friendly property name
|
||||
gd::String description; //< The user-friendly property description
|
||||
gd::String group; //< The user-friendly property group
|
||||
std::vector<gd::String>
|
||||
extraInformation; ///< Can be used to store for example the available
|
||||
///< choices, if a property is a displayed as a combo
|
||||
|
@@ -24,12 +24,9 @@ gd::String Resource::badStr;
|
||||
|
||||
Resource ResourcesManager::badResource;
|
||||
gd::String ResourcesManager::badResourceName;
|
||||
#if defined(GD_IDE_ONLY)
|
||||
ResourceFolder ResourcesManager::badFolder;
|
||||
Resource ResourceFolder::badResource;
|
||||
#endif
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void ResourceFolder::Init(const ResourceFolder& other) {
|
||||
name = other.name;
|
||||
|
||||
@@ -38,19 +35,16 @@ void ResourceFolder::Init(const ResourceFolder& other) {
|
||||
resources.push_back(std::shared_ptr<Resource>(other.resources[i]->Clone()));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ResourcesManager::Init(const ResourcesManager& other) {
|
||||
resources.clear();
|
||||
for (std::size_t i = 0; i < other.resources.size(); ++i) {
|
||||
resources.push_back(std::shared_ptr<Resource>(other.resources[i]->Clone()));
|
||||
}
|
||||
#if defined(GD_IDE_ONLY)
|
||||
folders.clear();
|
||||
for (std::size_t i = 0; i < other.folders.size(); ++i) {
|
||||
folders.push_back(other.folders[i]);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Resource& ResourcesManager::GetResource(const gd::String& name) {
|
||||
@@ -147,7 +141,6 @@ std::vector<gd::String> ResourcesManager::FindFilesNotInResources(
|
||||
return filesNotInResources;
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
std::map<gd::String, gd::PropertyDescriptor> Resource::GetProperties() const {
|
||||
std::map<gd::String, gd::PropertyDescriptor> nothing;
|
||||
return nothing;
|
||||
@@ -443,9 +436,7 @@ void ResourcesManager::RemoveResource(const gd::String& name) {
|
||||
for (std::size_t i = 0; i < folders.size(); ++i)
|
||||
folders[i].RemoveResource(name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void ResourceFolder::UnserializeFrom(const SerializerElement& element,
|
||||
gd::ResourcesManager& parentManager) {
|
||||
name = element.GetStringAttribute("name");
|
||||
@@ -470,7 +461,6 @@ void ResourceFolder::SerializeTo(SerializerElement& element) const {
|
||||
.SetAttribute("name", resources[i]->GetName());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ResourcesManager::UnserializeFrom(const SerializerElement& element) {
|
||||
resources.clear();
|
||||
@@ -500,7 +490,6 @@ void ResourcesManager::UnserializeFrom(const SerializerElement& element) {
|
||||
resources.push_back(resource);
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
folders.clear();
|
||||
const SerializerElement& resourcesFoldersElement =
|
||||
element.GetChild("resourceFolders", 0, "ResourceFolders");
|
||||
@@ -511,10 +500,8 @@ void ResourcesManager::UnserializeFrom(const SerializerElement& element) {
|
||||
|
||||
folders.push_back(folder);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void ResourcesManager::SerializeTo(SerializerElement& element) const {
|
||||
SerializerElement& resourcesElement = element.AddChild("resources");
|
||||
resourcesElement.ConsiderAsArrayOf("resource");
|
||||
@@ -543,7 +530,6 @@ void ResourcesManager::SerializeTo(SerializerElement& element) const {
|
||||
for (std::size_t i = 0; i < folders.size(); ++i)
|
||||
folders[i].SerializeTo(resourcesFoldersElement.AddChild("folder"));
|
||||
}
|
||||
#endif
|
||||
|
||||
void ImageResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -560,14 +546,12 @@ void ImageResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void ImageResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("alwaysLoaded", alwaysLoaded);
|
||||
element.SetAttribute("smoothed", smooth);
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
#endif
|
||||
|
||||
void AudioResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -584,14 +568,12 @@ void AudioResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetPreloadAsSound(element.GetBoolAttribute("preloadAsSound"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void AudioResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
element.SetAttribute("preloadAsMusic", PreloadAsMusic());
|
||||
element.SetAttribute("preloadAsSound", PreloadAsSound());
|
||||
}
|
||||
#endif
|
||||
|
||||
void FontResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -606,12 +588,10 @@ void FontResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void FontResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
#endif
|
||||
|
||||
void VideoResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -626,12 +606,10 @@ void VideoResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void VideoResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
#endif
|
||||
|
||||
void JsonResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -647,7 +625,6 @@ void JsonResource::UnserializeFrom(const SerializerElement& element) {
|
||||
DisablePreload(element.GetBoolAttribute("disablePreload", false));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void JsonResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
@@ -672,7 +649,6 @@ bool JsonResource::UpdateProperty(const gd::String& name,
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void BitmapFontResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -687,14 +663,11 @@ void BitmapFontResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void BitmapFontResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
ResourceFolder::ResourceFolder(const ResourceFolder& other) { Init(other); }
|
||||
|
||||
ResourceFolder& ResourceFolder::operator=(const ResourceFolder& other) {
|
||||
@@ -702,7 +675,6 @@ ResourceFolder& ResourceFolder::operator=(const ResourceFolder& other) {
|
||||
|
||||
return *this;
|
||||
}
|
||||
#endif
|
||||
|
||||
ResourcesManager::ResourcesManager(const ResourcesManager& other) {
|
||||
Init(other);
|
||||
|
@@ -104,7 +104,6 @@ class GD_CORE_API Resource {
|
||||
*/
|
||||
virtual const gd::String& GetMetadata() const { return metadata; }
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
/** \name Resources properties
|
||||
* Reading and updating resources properties
|
||||
*/
|
||||
@@ -136,7 +135,6 @@ class GD_CORE_API Resource {
|
||||
return false;
|
||||
};
|
||||
///@}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Serialize the object
|
||||
@@ -186,7 +184,6 @@ class GD_CORE_API ImageResource : public Resource {
|
||||
*/
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor> GetProperties() const override;
|
||||
@@ -196,7 +193,6 @@ class GD_CORE_API ImageResource : public Resource {
|
||||
* \brief Serialize the object
|
||||
*/
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Unserialize the objectt.
|
||||
@@ -238,14 +234,12 @@ class GD_CORE_API AudioResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor> GetProperties() const override;
|
||||
bool UpdateProperty(const gd::String& name, const gd::String& value) override;
|
||||
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -292,10 +286,8 @@ class GD_CORE_API FontResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -320,10 +312,8 @@ class GD_CORE_API VideoResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -348,14 +338,12 @@ class GD_CORE_API JsonResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor> GetProperties() const override;
|
||||
bool UpdateProperty(const gd::String& name, const gd::String& value) override;
|
||||
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -391,10 +379,8 @@ class GD_CORE_API BitmapFontResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -463,7 +449,6 @@ class GD_CORE_API ResourcesManager {
|
||||
*/
|
||||
std::vector<gd::String> FindFilesNotInResources(const std::vector<gd::String>& filesToCheck) const;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
/**
|
||||
* \brief Return a (smart) pointer to a resource.
|
||||
*/
|
||||
@@ -557,7 +542,6 @@ class GD_CORE_API ResourcesManager {
|
||||
* \brief Serialize the object
|
||||
*/
|
||||
void SerializeTo(SerializerElement& element) const;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Unserialize the objectt.
|
||||
@@ -568,18 +552,13 @@ class GD_CORE_API ResourcesManager {
|
||||
void Init(const ResourcesManager& other);
|
||||
|
||||
std::vector<std::shared_ptr<Resource> > resources;
|
||||
#if defined(GD_IDE_ONLY)
|
||||
std::vector<ResourceFolder> folders;
|
||||
#endif
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
static ResourceFolder badFolder;
|
||||
#endif
|
||||
static Resource badResource;
|
||||
static gd::String badResourceName;
|
||||
};
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
class GD_CORE_API ResourceFolder {
|
||||
public:
|
||||
ResourceFolder(){};
|
||||
@@ -654,7 +633,6 @@ class GD_CORE_API ResourceFolder {
|
||||
void Init(const ResourceFolder& other);
|
||||
static Resource badResource;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace gd
|
||||
|
||||
|
@@ -179,7 +179,8 @@ module.exports = {
|
||||
.setValue(
|
||||
behaviorContent.getBoolAttribute('property2') ? 'true' : 'false'
|
||||
)
|
||||
.setType('Boolean');
|
||||
.setType('Boolean')
|
||||
.setGroup(_('Look and Feel'));
|
||||
|
||||
return behaviorProperties;
|
||||
};
|
||||
|
@@ -415,6 +415,7 @@ module.exports = {
|
||||
_('Filesystem/Windows, Linux, MacOS'),
|
||||
'JsPlatform/Extensions/filesystem_folder32.png'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/FileSystem/filesystemtools.js')
|
||||
.setFunctionName('gdjs.fileSystem.getUserHomePath');
|
||||
|
@@ -150,7 +150,9 @@ namespace gdjs {
|
||||
* Get the path to the user's home folder (on Windows `C:\Users\<USERNAME>\` for example).
|
||||
* @return The path to user's "home" folder
|
||||
*/
|
||||
export const getUserHomePath = function (runtimeScene): string {
|
||||
export const getUserHomePath = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): string {
|
||||
const electron = runtimeScene.getGame().getRenderer().getElectron();
|
||||
if (electron) {
|
||||
return electron.remote.app.getPath('home') || '';
|
||||
|
@@ -168,7 +168,7 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
|
||||
.UseStandardRelationalOperatorParameters("number");
|
||||
|
||||
obj.AddAction("ParticleSize1",
|
||||
_("SIze, parameter 1"),
|
||||
_("Size, parameter 1"),
|
||||
_("Modify parameter 1 of the size of particles"),
|
||||
_("the parameter 1 of size"),
|
||||
_("Common"),
|
||||
|
@@ -39,23 +39,23 @@ std::map<gd::String, gd::PropertyDescriptor> PathfindingBehavior::GetProperties(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("acceleration")));
|
||||
properties[_("Max. speed")].SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxSpeed")));
|
||||
properties[_("Rotate speed")].SetValue(
|
||||
properties[_("Rotate speed")].SetGroup(_("Rotation")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angularMaxSpeed")));
|
||||
properties[_("Rotate object")]
|
||||
properties[_("Rotate object")].SetGroup(_("Rotation"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("rotateObject") ? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Angle offset")].SetValue(
|
||||
properties[_("Angle offset")].SetGroup(_("Rotation")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angleOffset")));
|
||||
properties[_("Virtual cell width")].SetValue(
|
||||
properties[_("Virtual cell width")].SetGroup(_("Virtual Grid")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("cellWidth", 0)));
|
||||
properties[_("Virtual cell height")].SetValue(
|
||||
properties[_("Virtual cell height")].SetGroup(_("Virtual Grid")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("cellHeight", 0)));
|
||||
properties[_("Virtual grid X offset")].SetValue(
|
||||
properties[_("Virtual grid X offset")].SetGroup(_("Virtual Grid")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("gridOffsetX", 0)));
|
||||
properties[_("Virtual grid Y offset")].SetValue(
|
||||
properties[_("Virtual grid Y offset")].SetGroup(_("Virtual Grid")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("gridOffsetY", 0)));
|
||||
properties[_("Extra border size")].SetValue(
|
||||
properties[_("Extra border size")].SetGroup(_("Collision")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("extraBorder")));
|
||||
|
||||
return properties;
|
||||
|
@@ -28,8 +28,8 @@ namespace gdjs {
|
||||
_pathFound: boolean = false;
|
||||
_speed: float = 0;
|
||||
_angularSpeed: float = 0;
|
||||
_timeOnSegment: float = 0;
|
||||
_totalSegmentTime: float = 0;
|
||||
_distanceOnSegment: float = 0;
|
||||
_totalSegmentDistance: float = 0;
|
||||
_currentSegment: integer = 0;
|
||||
_reachedEnd: boolean = false;
|
||||
_manager: PathfindingObstaclesManager;
|
||||
@@ -392,11 +392,11 @@ namespace gdjs {
|
||||
const pathY =
|
||||
this._path[this._currentSegment + 1][1] -
|
||||
this._path[this._currentSegment][1];
|
||||
this._totalSegmentTime = Math.sqrt(pathX * pathX + pathY * pathY);
|
||||
this._timeOnSegment = 0;
|
||||
this._totalSegmentDistance = Math.sqrt(pathX * pathX + pathY * pathY);
|
||||
this._distanceOnSegment = 0;
|
||||
this._reachedEnd = false;
|
||||
this._movementAngle =
|
||||
((Math.atan2(pathY, pathX) * 180) / Math.PI + 360) % 360;
|
||||
(gdjs.toDegrees(Math.atan2(pathY, pathX)) + 360) % 360;
|
||||
} else {
|
||||
this._reachedEnd = true;
|
||||
this._speed = 0;
|
||||
@@ -408,58 +408,59 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
//Update the speed of the object
|
||||
// Update the speed of the object
|
||||
const timeDelta = this.owner.getElapsedTime(runtimeScene) / 1000;
|
||||
this._speed += this._acceleration * timeDelta;
|
||||
if (this._speed > this._maxSpeed) {
|
||||
this._speed = this._maxSpeed;
|
||||
const previousSpeed = this._speed;
|
||||
if (this._speed !== this._maxSpeed) {
|
||||
this._speed += this._acceleration * timeDelta;
|
||||
if (this._speed > this._maxSpeed) {
|
||||
this._speed = this._maxSpeed;
|
||||
}
|
||||
}
|
||||
this._angularSpeed = this._angularMaxSpeed;
|
||||
|
||||
//Update the time on the segment and change segment if needed
|
||||
this._timeOnSegment += this._speed * timeDelta;
|
||||
// Update the time on the segment and change segment if needed
|
||||
// Use a Verlet integration to be frame rate independent.
|
||||
this._distanceOnSegment +=
|
||||
((this._speed + previousSpeed) / 2) * timeDelta;
|
||||
const remainingDistanceOnSegment =
|
||||
this._totalSegmentDistance - this._distanceOnSegment;
|
||||
if (
|
||||
this._timeOnSegment >= this._totalSegmentTime &&
|
||||
remainingDistanceOnSegment <= 0 &&
|
||||
this._currentSegment < this._path.length
|
||||
) {
|
||||
this._enterSegment(this._currentSegment + 1);
|
||||
this._distanceOnSegment = -remainingDistanceOnSegment;
|
||||
}
|
||||
|
||||
//Position object on the segment and update its angle
|
||||
// Position object on the segment and update its angle
|
||||
let newPos = [0, 0];
|
||||
let pathAngle = this.owner.getAngle();
|
||||
if (this._currentSegment < this._path.length - 1) {
|
||||
newPos[0] = gdjs.evtTools.common.lerp(
|
||||
this._path[this._currentSegment][0],
|
||||
this._path[this._currentSegment + 1][0],
|
||||
this._timeOnSegment / this._totalSegmentTime
|
||||
this._distanceOnSegment / this._totalSegmentDistance
|
||||
);
|
||||
newPos[1] = gdjs.evtTools.common.lerp(
|
||||
this._path[this._currentSegment][1],
|
||||
this._path[this._currentSegment + 1][1],
|
||||
this._timeOnSegment / this._totalSegmentTime
|
||||
this._distanceOnSegment / this._totalSegmentDistance
|
||||
);
|
||||
pathAngle =
|
||||
gdjs.toDegrees(
|
||||
Math.atan2(
|
||||
this._path[this._currentSegment + 1][1] -
|
||||
this._path[this._currentSegment][1],
|
||||
this._path[this._currentSegment + 1][0] -
|
||||
this._path[this._currentSegment][0]
|
||||
)
|
||||
) + this._angleOffset;
|
||||
if (
|
||||
this._rotateObject &&
|
||||
this.owner.getAngle() !== this._movementAngle + this._angleOffset
|
||||
) {
|
||||
this.owner.rotateTowardAngle(
|
||||
this._movementAngle + this._angleOffset,
|
||||
this._angularSpeed,
|
||||
runtimeScene
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newPos = this._path[this._path.length - 1];
|
||||
}
|
||||
this.owner.setX(newPos[0]);
|
||||
this.owner.setY(newPos[1]);
|
||||
if (this._rotateObject) {
|
||||
this.owner.rotateTowardAngle(
|
||||
pathAngle,
|
||||
this._angularSpeed,
|
||||
runtimeScene
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
doStepPostEvents(runtimeScene: gdjs.RuntimeScene) {}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
// @ts-check
|
||||
describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
describe.only('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
const epsilon = 1 / (2 << 16);
|
||||
// tests cases where every collisionMethod has the same behavior.
|
||||
let doCommonPathFindingTests = (collisionMethod, allowDiagonals) => {
|
||||
const pathFindingName = 'auto1';
|
||||
|
||||
const createScene = () => {
|
||||
const createScene = (framePerSecond = 60) => {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
// @ts-ignore - missing properties.
|
||||
@@ -39,11 +40,14 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
objects: [],
|
||||
instances: [],
|
||||
});
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return (1 / 60) * 1000;
|
||||
};
|
||||
setFramePerSecond(runtimeScene, framePerSecond);
|
||||
return runtimeScene;
|
||||
};
|
||||
const setFramePerSecond = (runtimeScene, framePerSecond) => {
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return 1000 / framePerSecond;
|
||||
};
|
||||
};
|
||||
|
||||
const addPlayer = (runtimeScene) => {
|
||||
const player = new gdjs.RuntimeObject(runtimeScene, {
|
||||
@@ -156,6 +160,96 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
);
|
||||
});
|
||||
|
||||
if (allowDiagonals) {
|
||||
[20, 30, 60, 120].forEach((framePerSecond) => {
|
||||
describe(`(${framePerSecond} fps)`, function () {
|
||||
it('can move on the path at the right speed', function () {
|
||||
setFramePerSecond(runtimeScene, framePerSecond);
|
||||
const obstacle = addObstacle(runtimeScene);
|
||||
|
||||
obstacle.setPosition(600, 300);
|
||||
// To ensure obstacles are registered.
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
|
||||
player.setPosition(480, 300);
|
||||
player.getBehavior(pathFindingName).moveTo(runtimeScene, 720, 300);
|
||||
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).getNodeCount()
|
||||
).to.be.above(13);
|
||||
|
||||
// Move on the path and stop before the last 1/10 of second.
|
||||
for (let i = 0; i < (framePerSecond * 17) / 10; i++) {
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).destinationReached()
|
||||
).to.be(false);
|
||||
}
|
||||
// The position is the same no matter the frame rate.
|
||||
expect(player.getX()).to.be(720);
|
||||
expect(player.getY()).to.be.within(
|
||||
288.5786437626905 - epsilon,
|
||||
288.5786437626905 + epsilon
|
||||
);
|
||||
|
||||
// Let 1/10 of second pass,
|
||||
// because the calculus interval is not the same for each case.
|
||||
for (let i = 0; i < framePerSecond / 10; i++) {
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
}
|
||||
// The destination is reached for every frame rate within 1/10 of second.
|
||||
expect(player.getX()).to.be(720);
|
||||
expect(player.getY()).to.be(300);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).destinationReached()
|
||||
).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
[20, 30, 60, 120].forEach((framePerSecond) => {
|
||||
describe(`(${framePerSecond} fps)`, function () {
|
||||
it('can move on the path at the right speed', function () {
|
||||
setFramePerSecond(runtimeScene, framePerSecond);
|
||||
const obstacle = addObstacle(runtimeScene);
|
||||
|
||||
obstacle.setPosition(600, 300);
|
||||
// To ensure obstacles are registered.
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
|
||||
player.setPosition(480, 300);
|
||||
player.getBehavior(pathFindingName).moveTo(runtimeScene, 720, 300);
|
||||
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).getNodeCount()
|
||||
).to.be.above(13);
|
||||
|
||||
// Move on the path and stop before the last 1/10 of second.
|
||||
for (let i = 0; i < (framePerSecond * 20) / 10; i++) {
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).destinationReached()
|
||||
).to.be(false);
|
||||
}
|
||||
expect(player.getX()).to.be(710);
|
||||
expect(player.getY()).to.be.within(300 - epsilon, 300 + epsilon);
|
||||
|
||||
// Let 1/10 of second pass,
|
||||
// because the calculus interval is not the same for each case.
|
||||
for (let i = 0; i < (framePerSecond * 1) / 10; i++) {
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
}
|
||||
// The destination is reached for every frame rate within 1/10 of second.
|
||||
expect(player.getX()).to.be(720);
|
||||
expect(player.getY()).to.be(300);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).destinationReached()
|
||||
).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('can find a path between 2 obstacles', function () {
|
||||
const obstacleTop = addObstacle(runtimeScene);
|
||||
const obstacleBottom = addObstacle(runtimeScene);
|
||||
|
@@ -500,6 +500,20 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.MarkAsAdvanced()
|
||||
.SetFunctionName("SimulateControl");
|
||||
|
||||
aut.AddScopedCondition("IsUsingControl",
|
||||
_("Control pressed or simulated"),
|
||||
_("A control was applied from a default control or a simulated by an action."),
|
||||
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
|
||||
_("Controls"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "PlatformerObjectBehavior")
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Key"),
|
||||
"[\"Left\", \"Right\", \"Jump\", \"Ladder\", \"Release Ladder\", \"Up\", \"Down\"]")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddAction("IgnoreDefaultControls",
|
||||
_("Ignore default controls"),
|
||||
_("De/activate the use of default controls.\nIf deactivated, "
|
||||
|
@@ -147,6 +147,8 @@ class PlatformBehaviorJsExtension : public gd::PlatformExtension {
|
||||
"simulateReleasePlatformKey");
|
||||
autActions["PlatformBehavior::SimulateControl"].SetFunctionName(
|
||||
"simulateControl");
|
||||
autConditions["PlatformBehavior::PlatformerObjectBehavior::IsUsingControl"].SetFunctionName(
|
||||
"isUsingControl");
|
||||
autActions["PlatformBehavior::IgnoreDefaultControls"].SetFunctionName(
|
||||
"ignoreDefaultControls");
|
||||
}
|
||||
|
@@ -42,12 +42,12 @@ std::map<gd::String, gd::PropertyDescriptor> PlatformBehavior::GetProperties(
|
||||
.AddExtraInfo(_("Platform"))
|
||||
.AddExtraInfo(_("Jumpthru platform"))
|
||||
.AddExtraInfo(_("Ladder"));
|
||||
properties[_("Ledges can be grabbed")]
|
||||
properties[_("Ledges can be grabbed")].SetGroup(_("Ledge"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("canBeGrabbed", true)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Grab offset on Y axis")].SetValue(
|
||||
properties[_("Grab offset on Y axis")].SetGroup(_("Ledge")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("yGrabOffset")));
|
||||
|
||||
return properties;
|
||||
|
@@ -33,6 +33,7 @@ void PlatformerObjectBehavior::InitializeContent(
|
||||
behaviorContent.SetAttribute("canGrabPlatforms", false);
|
||||
behaviorContent.SetAttribute("yGrabOffset", 0);
|
||||
behaviorContent.SetAttribute("xGrabTolerance", 10);
|
||||
behaviorContent.SetAttribute("useLegacyTrajectory", false);
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
@@ -41,11 +42,11 @@ PlatformerObjectBehavior::GetProperties(
|
||||
const gd::SerializerElement& behaviorContent) const {
|
||||
std::map<gd::String, gd::PropertyDescriptor> properties;
|
||||
|
||||
properties[_("Gravity")].SetValue(
|
||||
properties[_("Gravity")].SetGroup(_("Jump")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("gravity")));
|
||||
properties[_("Jump speed")].SetValue(
|
||||
properties[_("Jump speed")].SetGroup(_("Jump")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("jumpSpeed")));
|
||||
properties["jumpSustainTime"]
|
||||
properties["jumpSustainTime"].SetGroup(_("Jump"))
|
||||
.SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("jumpSustainTime", 0)))
|
||||
.SetLabel(_("Jump sustain time"))
|
||||
@@ -53,32 +54,39 @@ PlatformerObjectBehavior::GetProperties(
|
||||
_("Maximum time (in seconds) during which the jump strength is "
|
||||
"sustained if the jump key is held - allowing variable height "
|
||||
"jumps."));
|
||||
properties[_("Max. falling speed")].SetValue(
|
||||
properties[_("Max. falling speed")].SetGroup(_("Jump")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxFallingSpeed")));
|
||||
properties[_("Ladder climbing speed")].SetValue(gd::String::From(
|
||||
properties[_("Ladder climbing speed")].SetGroup(_("Ladder")).SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("ladderClimbingSpeed", 150)));
|
||||
properties[_("Acceleration")].SetValue(
|
||||
properties[_("Acceleration")].SetGroup(_("Walk")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("acceleration")));
|
||||
properties[_("Deceleration")].SetValue(
|
||||
properties[_("Deceleration")].SetGroup(_("Walk")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("deceleration")));
|
||||
properties[_("Max. speed")].SetValue(
|
||||
properties[_("Max. speed")].SetGroup(_("Walk")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxSpeed")));
|
||||
properties[_("Default controls")]
|
||||
.SetValue(behaviorContent.GetBoolAttribute("ignoreDefaultControls")
|
||||
? "false"
|
||||
: "true")
|
||||
.SetType("Boolean");
|
||||
properties[_("Slope max. angle")].SetValue(
|
||||
properties[_("Slope max. angle")].SetGroup(_("Walk")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("slopeMaxAngle")));
|
||||
properties[_("Can grab platform ledges")]
|
||||
.SetGroup(_("Ledge"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("canGrabPlatforms", false)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Grab offset on Y axis")].SetValue(
|
||||
properties[_("Grab offset on Y axis")].SetGroup(_("Ledge")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("yGrabOffset")));
|
||||
properties[_("Grab tolerance on X axis")].SetValue(gd::String::From(
|
||||
properties[_("Grab tolerance on X axis")].SetGroup(_("Ledge")).SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("xGrabTolerance", 10)));
|
||||
properties[_("Use frame per second dependent trajectories (deprecated)")]
|
||||
.SetGroup(_("Jump"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("useLegacyTrajectory", true)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
return properties;
|
||||
}
|
||||
|
||||
@@ -90,6 +98,8 @@ bool PlatformerObjectBehavior::UpdateProperty(
|
||||
behaviorContent.SetAttribute("ignoreDefaultControls", (value == "0"));
|
||||
else if (name == _("Can grab platform ledges"))
|
||||
behaviorContent.SetAttribute("canGrabPlatforms", (value == "1"));
|
||||
else if (name == _("Use frame per second dependent trajectories (deprecated)"))
|
||||
behaviorContent.SetAttribute("useLegacyTrajectory", (value == "1"));
|
||||
else if (name == _("Grab offset on Y axis"))
|
||||
behaviorContent.SetAttribute("yGrabOffset", value.To<double>());
|
||||
else {
|
||||
|
@@ -24,28 +24,47 @@ namespace gdjs {
|
||||
isCollidingAnyPlatform: false,
|
||||
};
|
||||
|
||||
// To achieve pixel-perfect precision when positioning object on platform or
|
||||
// handling collision with "walls", edges of the hitboxes must be ignored during
|
||||
// collision checks, so that two overlapping edges are not considered as colliding.
|
||||
// For example, if a character is 10px width and is at position (0, 0), it must not be
|
||||
// considered as colliding with a platform which is at position (10, 0). Edges will
|
||||
// still be overlapping (because character hitbox right edge is at X position 10 and
|
||||
// platform hitbox left edge is also at X position 10).
|
||||
// This parameter "_ignoreTouchingEdges" will be passed to all collision handling functions.
|
||||
// Behavior configuration
|
||||
|
||||
/** To achieve pixel-perfect precision when positioning object on platform or
|
||||
* handling collision with "walls", edges of the hitboxes must be ignored during
|
||||
* collision checks, so that two overlapping edges are not considered as colliding.
|
||||
*
|
||||
* For example, if a character is 10px width and is at position (0, 0), it must not be
|
||||
* considered as colliding with a platform which is at position (10, 0). Edges will
|
||||
* still be overlapping (because character hitbox right edge is at X position 10 and
|
||||
* platform hitbox left edge is also at X position 10).
|
||||
*
|
||||
* This parameter "_ignoreTouchingEdges" will be passed to all collision handling functions.
|
||||
*/
|
||||
_ignoreTouchingEdges: boolean = true;
|
||||
_gravity: float;
|
||||
_maxFallingSpeed: float;
|
||||
_ladderClimbingSpeed: float;
|
||||
|
||||
private _acceleration: float;
|
||||
private _deceleration: float;
|
||||
private _maxSpeed: float;
|
||||
private _slopeMaxAngle: float;
|
||||
_slopeClimbingFactor: float = 1;
|
||||
|
||||
_gravity: float;
|
||||
_maxFallingSpeed: float;
|
||||
_jumpSpeed: float;
|
||||
_jumpSustainTime: float;
|
||||
|
||||
_ladderClimbingSpeed: float;
|
||||
|
||||
_canGrabPlatforms: boolean;
|
||||
private _yGrabOffset: any;
|
||||
private _xGrabTolerance: any;
|
||||
_jumpSustainTime: float;
|
||||
_currentFallSpeed: float = 0;
|
||||
|
||||
_useLegacyTrajectory: boolean = true;
|
||||
|
||||
// Behavior state
|
||||
|
||||
_currentSpeed: float = 0;
|
||||
_requestedDeltaX: float = 0;
|
||||
_requestedDeltaY: float = 0;
|
||||
_lastDeltaY: float = 0;
|
||||
_currentFallSpeed: float = 0;
|
||||
_canJump: boolean = false;
|
||||
|
||||
private _ignoreDefaultControls: boolean;
|
||||
@@ -58,6 +77,18 @@ namespace gdjs {
|
||||
_releasePlatformKey: boolean = false;
|
||||
_releaseLadderKey: boolean = false;
|
||||
|
||||
// This is useful for extensions that need to know
|
||||
// which keys were pressed and doesn't know the mapping
|
||||
// done by the scene events.
|
||||
private _wasLeftKeyPressed: boolean = false;
|
||||
private _wasRightKeyPressed: boolean = false;
|
||||
private _wasLadderKeyPressed: boolean = false;
|
||||
private _wasUpKeyPressed: boolean = false;
|
||||
private _wasDownKeyPressed: boolean = false;
|
||||
private _wasJumpKeyPressed: boolean = false;
|
||||
private _wasReleasePlatformKeyPressed: boolean = false;
|
||||
private _wasReleaseLadderKeyPressed: boolean = false;
|
||||
|
||||
private _state: State;
|
||||
_falling: Falling;
|
||||
_onFloor: OnFloor;
|
||||
@@ -66,21 +97,18 @@ namespace gdjs {
|
||||
_onLadder: OnLadder;
|
||||
|
||||
/** Platforms near the object, updated with `_updatePotentialCollidingObjects`. */
|
||||
_potentialCollidingObjects: Array<gdjs.PlatformRuntimeBehavior>;
|
||||
_potentialCollidingObjects: Array<
|
||||
gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>
|
||||
>;
|
||||
|
||||
/** Overlapped jump-thru platforms, updated with `_updateOverlappedJumpThru`. */
|
||||
private _overlappedJumpThru: Array<gdjs.PlatformRuntimeBehavior>;
|
||||
private _overlappedJumpThru: Array<
|
||||
gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>
|
||||
>;
|
||||
|
||||
private _hasReallyMoved: boolean = false;
|
||||
private _manager: gdjs.PlatformObjectsManager;
|
||||
|
||||
private _slopeMaxAngle: float;
|
||||
_slopeClimbingFactor: float = 1;
|
||||
|
||||
_requestedDeltaX: float = 0;
|
||||
_requestedDeltaY: float = 0;
|
||||
_lastDeltaY: float = 0;
|
||||
|
||||
constructor(
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
behaviorData,
|
||||
@@ -99,11 +127,13 @@ namespace gdjs {
|
||||
this._xGrabTolerance = behaviorData.xGrabTolerance || 10;
|
||||
this._jumpSustainTime = behaviorData.jumpSustainTime || 0;
|
||||
this._ignoreDefaultControls = behaviorData.ignoreDefaultControls;
|
||||
this._potentialCollidingObjects = [];
|
||||
|
||||
this._overlappedJumpThru = [];
|
||||
this._useLegacyTrajectory = behaviorData.useLegacyTrajectory;
|
||||
this._slopeMaxAngle = 0;
|
||||
this.setSlopeMaxAngle(behaviorData.slopeMaxAngle);
|
||||
|
||||
this._potentialCollidingObjects = [];
|
||||
this._overlappedJumpThru = [];
|
||||
|
||||
this._manager = gdjs.PlatformObjectsManager.getManager(runtimeScene);
|
||||
|
||||
this._falling = new Falling(this);
|
||||
@@ -147,6 +177,12 @@ namespace gdjs {
|
||||
if (oldBehaviorData.jumpSustainTime !== newBehaviorData.jumpSustainTime) {
|
||||
this.setJumpSustainTime(newBehaviorData.jumpSustainTime);
|
||||
}
|
||||
if (
|
||||
oldBehaviorData.useLegacyTrajectory !==
|
||||
newBehaviorData.useLegacyTrajectory
|
||||
) {
|
||||
this._useLegacyTrajectory = newBehaviorData.useLegacyTrajectory;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -235,6 +271,14 @@ namespace gdjs {
|
||||
this._checkTransitionOnFloorOrFalling();
|
||||
}
|
||||
|
||||
this._wasLeftKeyPressed = this._leftKey;
|
||||
this._wasRightKeyPressed = this._rightKey;
|
||||
this._wasLadderKeyPressed = this._ladderKey;
|
||||
this._wasUpKeyPressed = this._releaseLadderKey;
|
||||
this._wasDownKeyPressed = this._upKey;
|
||||
this._wasJumpKeyPressed = this._downKey;
|
||||
this._wasReleasePlatformKeyPressed = this._releasePlatformKey;
|
||||
this._wasReleaseLadderKeyPressed = this._jumpKey;
|
||||
//4) Do not forget to reset pressed keys
|
||||
this._leftKey = false;
|
||||
this._rightKey = false;
|
||||
@@ -255,6 +299,7 @@ namespace gdjs {
|
||||
doStepPostEvents(runtimeScene: gdjs.RuntimeScene) {}
|
||||
|
||||
private _updateSpeed(timeDelta: float): float {
|
||||
const previousSpeed = this._currentSpeed;
|
||||
//Change the speed according to the player's input.
|
||||
// @ts-ignore
|
||||
if (this._leftKey) {
|
||||
@@ -284,7 +329,8 @@ namespace gdjs {
|
||||
if (this._currentSpeed < -this._maxSpeed) {
|
||||
this._currentSpeed = -this._maxSpeed;
|
||||
}
|
||||
return this._currentSpeed * timeDelta;
|
||||
// Use Verlet integration.
|
||||
return ((this._currentSpeed + previousSpeed) * timeDelta) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -463,8 +509,11 @@ namespace gdjs {
|
||||
);
|
||||
collidingPlatforms.length = 0;
|
||||
for (const platform of this._potentialCollidingObjects) {
|
||||
if (this._isCollidingWith(platform) && this._canGrab(platform)) {
|
||||
collidingPlatforms.push(platform);
|
||||
if (
|
||||
this._isCollidingWith(platform.behavior) &&
|
||||
this._canGrab(platform.behavior)
|
||||
) {
|
||||
collidingPlatforms.push(platform.behavior);
|
||||
}
|
||||
}
|
||||
object.setX(oldX);
|
||||
@@ -528,15 +577,18 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
_fall(timeDelta: float) {
|
||||
const previousFallSpeed = this._currentFallSpeed;
|
||||
this._currentFallSpeed += this._gravity * timeDelta;
|
||||
if (this._currentFallSpeed > this._maxFallingSpeed) {
|
||||
this._currentFallSpeed = this._maxFallingSpeed;
|
||||
}
|
||||
this._requestedDeltaY += this._currentFallSpeed * timeDelta;
|
||||
this._requestedDeltaY = Math.min(
|
||||
this._requestedDeltaY,
|
||||
this._maxFallingSpeed * timeDelta
|
||||
);
|
||||
if (this._useLegacyTrajectory) {
|
||||
this._requestedDeltaY += this._currentFallSpeed * timeDelta;
|
||||
} else {
|
||||
// Use Verlet integration.
|
||||
this._requestedDeltaY +=
|
||||
((this._currentFallSpeed + previousFallSpeed) / 2) * timeDelta;
|
||||
}
|
||||
}
|
||||
|
||||
//Scene change is not supported
|
||||
@@ -558,10 +610,13 @@ namespace gdjs {
|
||||
const y1 = this.owner.getY() + this._yGrabOffset - this._lastDeltaY;
|
||||
const y2 = this.owner.getY() + this._yGrabOffset;
|
||||
const platformY = platform.owner.getY() + platform.getYGrabOffset();
|
||||
// This must be inclusive for at least one position.
|
||||
// Otherwise, if the character is at the exact position,
|
||||
// it could not be able to grab the platform at any frame.
|
||||
return (
|
||||
platform.canBeGrabbed() &&
|
||||
((y1 < platformY && platformY < y2) ||
|
||||
(y2 < platformY && platformY < y1))
|
||||
((y1 < platformY && platformY <= y2) ||
|
||||
(y2 <= platformY && platformY < y1))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -590,7 +645,7 @@ namespace gdjs {
|
||||
* @returns true if the object was moved
|
||||
*/
|
||||
private _separateFromPlatforms(
|
||||
candidates: gdjs.PlatformRuntimeBehavior[],
|
||||
candidates: gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>[],
|
||||
excludeJumpThrus: boolean
|
||||
) {
|
||||
excludeJumpThrus = !!excludeJumpThrus;
|
||||
@@ -601,17 +656,19 @@ namespace gdjs {
|
||||
for (let i = 0; i < candidates.length; ++i) {
|
||||
const platform = candidates[i];
|
||||
if (
|
||||
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER
|
||||
platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.LADDER
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
excludeJumpThrus &&
|
||||
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.JUMPTHRU
|
||||
platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.JUMPTHRU
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
objects.push(platform.owner);
|
||||
objects.push(platform.behavior.owner);
|
||||
}
|
||||
return this.owner.separateFromObjects(objects, this._ignoreTouchingEdges);
|
||||
}
|
||||
@@ -625,31 +682,33 @@ namespace gdjs {
|
||||
* @returns true if the object collides any platform
|
||||
*/
|
||||
_isCollidingWithOneOf(
|
||||
candidates: gdjs.PlatformRuntimeBehavior[],
|
||||
candidates: gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>[],
|
||||
exceptThisOne?: number | null,
|
||||
excludeJumpThrus?: boolean
|
||||
) {
|
||||
excludeJumpThrus = !!excludeJumpThrus;
|
||||
for (let i = 0; i < candidates.length; ++i) {
|
||||
const platform = candidates[i];
|
||||
if (platform.owner.id === exceptThisOne) {
|
||||
if (platform.behavior.owner.id === exceptThisOne) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER
|
||||
platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.LADDER
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
excludeJumpThrus &&
|
||||
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.JUMPTHRU
|
||||
platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.JUMPTHRU
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
gdjs.RuntimeObject.collisionTest(
|
||||
this.owner,
|
||||
platform.owner,
|
||||
platform.behavior.owner,
|
||||
this._ignoreTouchingEdges
|
||||
)
|
||||
) {
|
||||
@@ -670,7 +729,7 @@ namespace gdjs {
|
||||
* @returns the platform where to walk or if an obstacle was found
|
||||
*/
|
||||
_findHighestFloorAndMoveOnTop(
|
||||
candidates: gdjs.PlatformRuntimeBehavior[],
|
||||
candidates: gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>[],
|
||||
upwardDeltaY: float,
|
||||
downwardDeltaY: float
|
||||
): PlatformSearchResult {
|
||||
@@ -682,19 +741,23 @@ namespace gdjs {
|
||||
let isCollidingAnyPlatform = false;
|
||||
for (const platform of candidates) {
|
||||
if (
|
||||
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER ||
|
||||
platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.LADDER ||
|
||||
// Jump through platforms are obstacles only when the character comes from the top.
|
||||
(platform.getPlatformType() ===
|
||||
(platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.JUMPTHRU &&
|
||||
// When following the floor, jumpthrus that are higher than the character are ignored.
|
||||
// If we only look above the character bottom, every jumpthrus can be discarded
|
||||
// without doing any collision check.
|
||||
((this._state === this._onFloor &&
|
||||
platform !== this._onFloor.getFloorPlatform() &&
|
||||
platform.behavior !== this._onFloor.getFloorPlatform() &&
|
||||
downwardDeltaY < 0) ||
|
||||
// When trying to land on a platform, exclude jumpthrus that were already overlapped.
|
||||
(this._state !== this._onFloor &&
|
||||
this._isIn(this._overlappedJumpThru, platform.owner.id))))
|
||||
this._isIn(
|
||||
this._overlappedJumpThru,
|
||||
platform.behavior.owner.id
|
||||
))))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -706,8 +769,8 @@ namespace gdjs {
|
||||
if (
|
||||
// When following the floor, ignore jumpthrus that are higher than the character bottom.
|
||||
this._state === this._onFloor &&
|
||||
platform !== this._onFloor.getFloorPlatform() &&
|
||||
platform.getPlatformType() ===
|
||||
platform.behavior !== this._onFloor.getFloorPlatform() &&
|
||||
platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.JUMPTHRU &&
|
||||
highestRelativeY < 0
|
||||
) {
|
||||
@@ -733,7 +796,7 @@ namespace gdjs {
|
||||
highestRelativeY < totalHighestY
|
||||
) {
|
||||
totalHighestY = highestRelativeY;
|
||||
highestGround = platform;
|
||||
highestGround = platform.behavior;
|
||||
}
|
||||
}
|
||||
if (highestGround) {
|
||||
@@ -755,10 +818,10 @@ namespace gdjs {
|
||||
* @return the search context
|
||||
*/
|
||||
private _findPlatformHighestRelativeYUnderObject(
|
||||
platform: gdjs.PlatformRuntimeBehavior,
|
||||
platform: gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>,
|
||||
context: FollowConstraintContext
|
||||
): FollowConstraintContext {
|
||||
const platformObject = platform.owner;
|
||||
const platformObject = platform.behavior.owner;
|
||||
const platformAABB = platformObject.getAABB();
|
||||
if (
|
||||
platformAABB.max[0] <= context.ownerMinX ||
|
||||
@@ -861,23 +924,27 @@ namespace gdjs {
|
||||
* @param exceptTheseOnes The platforms to be excluded from the test
|
||||
*/
|
||||
private _isCollidingWithOneOfExcluding(
|
||||
candidates: gdjs.PlatformRuntimeBehavior[],
|
||||
exceptTheseOnes: gdjs.PlatformRuntimeBehavior[]
|
||||
candidates: gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>[],
|
||||
exceptTheseOnes: gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>[]
|
||||
) {
|
||||
for (let i = 0; i < candidates.length; ++i) {
|
||||
const platform = candidates[i];
|
||||
if (exceptTheseOnes && this._isIn(exceptTheseOnes, platform.owner.id)) {
|
||||
if (
|
||||
exceptTheseOnes &&
|
||||
this._isIn(exceptTheseOnes, platform.behavior.owner.id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER
|
||||
platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.LADDER
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
gdjs.RuntimeObject.collisionTest(
|
||||
this.owner,
|
||||
platform.owner,
|
||||
platform.behavior.owner,
|
||||
this._ignoreTouchingEdges
|
||||
)
|
||||
) {
|
||||
@@ -914,11 +981,11 @@ namespace gdjs {
|
||||
for (let i = 0; i < this._potentialCollidingObjects.length; ++i) {
|
||||
const platform = this._potentialCollidingObjects[i];
|
||||
if (
|
||||
platform.getPlatformType() ===
|
||||
platform.behavior.getPlatformType() ===
|
||||
gdjs.PlatformRuntimeBehavior.JUMPTHRU &&
|
||||
gdjs.RuntimeObject.collisionTest(
|
||||
this.owner,
|
||||
platform.owner,
|
||||
platform.behavior.owner,
|
||||
this._ignoreTouchingEdges
|
||||
)
|
||||
) {
|
||||
@@ -935,14 +1002,15 @@ namespace gdjs {
|
||||
for (let i = 0; i < this._potentialCollidingObjects.length; ++i) {
|
||||
const platform = this._potentialCollidingObjects[i];
|
||||
if (
|
||||
platform.getPlatformType() !== gdjs.PlatformRuntimeBehavior.LADDER
|
||||
platform.behavior.getPlatformType() !==
|
||||
gdjs.PlatformRuntimeBehavior.LADDER
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
gdjs.RuntimeObject.collisionTest(
|
||||
this.owner,
|
||||
platform.owner,
|
||||
platform.behavior.owner,
|
||||
this._ignoreTouchingEdges
|
||||
)
|
||||
) {
|
||||
@@ -952,9 +1020,12 @@ namespace gdjs {
|
||||
return false;
|
||||
}
|
||||
|
||||
_isIn(platformArray: gdjs.PlatformRuntimeBehavior[], id: integer) {
|
||||
_isIn(
|
||||
platformArray: gdjs.BehaviorRBushAABB<gdjs.PlatformRuntimeBehavior>[],
|
||||
id: integer
|
||||
) {
|
||||
for (let i = 0; i < platformArray.length; ++i) {
|
||||
if (platformArray[i].owner.id === id) {
|
||||
if (platformArray[i].behavior.owner.id === id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -965,8 +1036,10 @@ namespace gdjs {
|
||||
* Update _potentialCollidingObjects member with platforms near the object.
|
||||
*/
|
||||
private _updatePotentialCollidingObjects(maxMovementLength: float) {
|
||||
const object = this.owner;
|
||||
|
||||
this._manager.getAllPlatformsAround(
|
||||
this.owner,
|
||||
object,
|
||||
maxMovementLength,
|
||||
this._potentialCollidingObjects
|
||||
);
|
||||
@@ -975,7 +1048,7 @@ namespace gdjs {
|
||||
// is not considered as colliding with itself, in the case that it also has the
|
||||
// platform behavior.
|
||||
for (let i = 0; i < this._potentialCollidingObjects.length; ) {
|
||||
if (this._potentialCollidingObjects[i].owner === this.owner) {
|
||||
if (this._potentialCollidingObjects[i].behavior.owner === object) {
|
||||
this._potentialCollidingObjects.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
@@ -1007,6 +1080,38 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**.
|
||||
* @param input The control to be tested [Left,Right,Up,Down,Ladder,Jump,Release,Release Ladder].
|
||||
* @returns true if the key was used since the last `doStepPreEvents` call.
|
||||
*/
|
||||
isUsingControl(input: string): boolean {
|
||||
if (input === 'Left') {
|
||||
return this._wasLeftKeyPressed;
|
||||
}
|
||||
if (input === 'Right') {
|
||||
return this._wasRightKeyPressed;
|
||||
}
|
||||
if (input === 'Up') {
|
||||
return this._wasUpKeyPressed;
|
||||
}
|
||||
if (input === 'Down') {
|
||||
return this._wasDownKeyPressed;
|
||||
}
|
||||
if (input === 'Ladder') {
|
||||
return this._wasLadderKeyPressed;
|
||||
}
|
||||
if (input === 'Jump') {
|
||||
return this._wasJumpKeyPressed;
|
||||
}
|
||||
if (input === 'Release') {
|
||||
return this._wasReleasePlatformKeyPressed;
|
||||
}
|
||||
if (input === 'Release Ladder') {
|
||||
return this._wasReleaseLadderKeyPressed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the gravity of the Platformer Object.
|
||||
* @returns The current gravity.
|
||||
@@ -1790,20 +1895,14 @@ namespace gdjs {
|
||||
beforeMovingY(timeDelta: float, oldX: float) {
|
||||
const behavior = this._behavior;
|
||||
|
||||
//Fall
|
||||
if (!this._jumpingFirstDelta) {
|
||||
behavior._fall(timeDelta);
|
||||
}
|
||||
this._jumpingFirstDelta = false;
|
||||
|
||||
// Check if the jump key is continuously held since
|
||||
// the beginning of the jump.
|
||||
if (!behavior._jumpKey) {
|
||||
this._jumpKeyHeldSinceJumpStart = false;
|
||||
}
|
||||
this._timeSinceCurrentJumpStart += timeDelta;
|
||||
behavior._requestedDeltaY -= this._currentJumpSpeed * timeDelta;
|
||||
|
||||
const previousJumpSpeed = this._currentJumpSpeed;
|
||||
// Decrease jump speed after the (optional) jump sustain time is over.
|
||||
const sustainJumpSpeed =
|
||||
this._jumpKeyHeldSinceJumpStart &&
|
||||
@@ -1811,6 +1910,27 @@ namespace gdjs {
|
||||
if (!sustainJumpSpeed) {
|
||||
this._currentJumpSpeed -= behavior._gravity * timeDelta;
|
||||
}
|
||||
|
||||
if (this._behavior._useLegacyTrajectory) {
|
||||
behavior._requestedDeltaY -= previousJumpSpeed * timeDelta;
|
||||
|
||||
// Fall
|
||||
// The condition is a legacy thing.
|
||||
// There is no actual reason not to fall at 1st frame.
|
||||
// Before a refactoring, it used to not be this obvious.
|
||||
if (!this._jumpingFirstDelta) {
|
||||
behavior._fall(timeDelta);
|
||||
}
|
||||
} else {
|
||||
// Use Verlet integration.
|
||||
behavior._requestedDeltaY +=
|
||||
((-previousJumpSpeed - this._currentJumpSpeed) / 2) * timeDelta;
|
||||
|
||||
// Fall
|
||||
behavior._fall(timeDelta);
|
||||
}
|
||||
this._jumpingFirstDelta = false;
|
||||
|
||||
if (this._currentJumpSpeed < 0) {
|
||||
behavior._setFalling();
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ Copyright (c) 2013-2016 Florian Rival (Florian.Rival@gmail.com)
|
||||
*/
|
||||
namespace gdjs {
|
||||
declare var rbush: any;
|
||||
type SearchArea = { minX: float; minY: float; maxX: float; maxY: float };
|
||||
|
||||
/**
|
||||
* Manages the common objects shared by objects having a
|
||||
@@ -18,12 +19,7 @@ namespace gdjs {
|
||||
* @param object The object
|
||||
*/
|
||||
constructor(runtimeScene: gdjs.RuntimeScene) {
|
||||
this._platformRBush = new rbush(9, [
|
||||
'.owner.getAABB().min[0]',
|
||||
'.owner.getAABB().min[1]',
|
||||
'.owner.getAABB().max[0]',
|
||||
'.owner.getAABB().max[1]',
|
||||
]);
|
||||
this._platformRBush = new rbush();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +42,13 @@ namespace gdjs {
|
||||
* Add a platform to the list of existing platforms.
|
||||
*/
|
||||
addPlatform(platformBehavior: gdjs.PlatformRuntimeBehavior) {
|
||||
this._platformRBush.insert(platformBehavior);
|
||||
if (platformBehavior.currentRBushAABB)
|
||||
platformBehavior.currentRBushAABB.updateAABBFromOwner();
|
||||
else
|
||||
platformBehavior.currentRBushAABB = new gdjs.BehaviorRBushAABB(
|
||||
platformBehavior
|
||||
);
|
||||
this._platformRBush.insert(platformBehavior.currentRBushAABB);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +56,7 @@ namespace gdjs {
|
||||
* added before.
|
||||
*/
|
||||
removePlatform(platformBehavior: gdjs.PlatformRuntimeBehavior) {
|
||||
this._platformRBush.remove(platformBehavior);
|
||||
this._platformRBush.remove(platformBehavior.currentRBushAABB);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +67,7 @@ namespace gdjs {
|
||||
getAllPlatformsAround(
|
||||
object: gdjs.RuntimeObject,
|
||||
maxMovementLength: number,
|
||||
result: gdjs.PlatformRuntimeBehavior[]
|
||||
result: gdjs.BehaviorRBushAABB<PlatformRuntimeBehavior>[]
|
||||
): any {
|
||||
// TODO: This would better be done using the object AABB (getAABB), as (`getCenterX`;`getCenterY`) point
|
||||
// is not necessarily in the middle of the object (for sprites for example).
|
||||
@@ -73,20 +75,37 @@ namespace gdjs {
|
||||
const oh = object.getHeight();
|
||||
const x = object.getDrawableX() + object.getCenterX();
|
||||
const y = object.getDrawableY() + object.getCenterY();
|
||||
const searchArea = gdjs.staticObject(
|
||||
const searchArea: SearchArea = gdjs.staticObject(
|
||||
PlatformObjectsManager.prototype.getAllPlatformsAround
|
||||
);
|
||||
// @ts-ignore
|
||||
) as SearchArea;
|
||||
searchArea.minX = x - ow / 2 - maxMovementLength;
|
||||
// @ts-ignore
|
||||
searchArea.minY = y - oh / 2 - maxMovementLength;
|
||||
// @ts-ignore
|
||||
searchArea.maxX = x + ow / 2 + maxMovementLength;
|
||||
// @ts-ignore
|
||||
searchArea.maxY = y + oh / 2 + maxMovementLength;
|
||||
const nearbyPlatforms = this._platformRBush.search(searchArea);
|
||||
const nearbyPlatforms: gdjs.BehaviorRBushAABB<
|
||||
PlatformRuntimeBehavior
|
||||
>[] = this._platformRBush.search(searchArea);
|
||||
|
||||
result.length = 0;
|
||||
result.push.apply(result, nearbyPlatforms);
|
||||
|
||||
// Extra check on the platform owner AABB
|
||||
// TODO: PR https://github.com/4ian/GDevelop/pull/2602 should remove the need
|
||||
// for this extra check once merged.
|
||||
for (let i = 0; i < nearbyPlatforms.length; i++) {
|
||||
const platform = nearbyPlatforms[i];
|
||||
const platformAABB = platform.behavior.owner.getAABB();
|
||||
const platformIsStillAround =
|
||||
platformAABB.min[0] <= searchArea.maxX &&
|
||||
platformAABB.min[1] <= searchArea.maxY &&
|
||||
platformAABB.max[0] >= searchArea.minX &&
|
||||
platformAABB.max[1] >= searchArea.minY;
|
||||
// Filter platforms that are not in the searched area anymore.
|
||||
// This can happen because platforms are not updated in the RBush before that
|
||||
// characters movement are being processed.
|
||||
if (platformIsStillAround) {
|
||||
result.push(platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +125,9 @@ namespace gdjs {
|
||||
_oldWidth: float = 0;
|
||||
_oldHeight: float = 0;
|
||||
_oldAngle: float = 0;
|
||||
currentRBushAABB: gdjs.BehaviorRBushAABB<
|
||||
PlatformRuntimeBehavior
|
||||
> | null = null;
|
||||
_manager: gdjs.PlatformObjectsManager;
|
||||
_registeredInManager: boolean = false;
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
const epsilon = 1 / (2 << 16);
|
||||
[0, 60].forEach((slopeMaxAngle) => {
|
||||
describe(`(walk on flat floors, slopeMaxAngle: ${slopeMaxAngle}°)`, function () {
|
||||
let runtimeScene;
|
||||
@@ -383,7 +384,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
});
|
||||
|
||||
it('can track object height changes', function () {
|
||||
//Put the object near the right ledge of the platform.
|
||||
// Put the character near the right ledge of the platform.
|
||||
object.setPosition(
|
||||
platform.getX() + 10,
|
||||
platform.getY() - object.getHeight() + 1
|
||||
@@ -398,16 +399,19 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
false
|
||||
);
|
||||
expect(object.getX()).to.be(10);
|
||||
expect(object.getY()).to.be.within(-31, -30); // -30 = -10 (platform y) + -20 (object height)
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
|
||||
// Make the platform under the character feet smaller.
|
||||
object.setCustomWidthAndHeight(object.getWidth(), 9);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
// The character follows it.
|
||||
expect(object.getY()).to.be(-19); // -19 = -10 (platform y) + -9 (object height)
|
||||
|
||||
// The character walks on the platform.
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
@@ -417,10 +421,12 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
);
|
||||
}
|
||||
expect(object.getY()).to.be(-19);
|
||||
expect(object.getX()).to.be.within(17.638, 17.639);
|
||||
expect(object.getX()).to.be.above(16);
|
||||
|
||||
// Make the platform under the character feet bigger.
|
||||
object.setCustomWidthAndHeight(object.getWidth(), 20);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
// The character follows it.
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
});
|
||||
|
||||
@@ -692,4 +698,86 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
expect(object.getX()).to.be(6);
|
||||
});
|
||||
});
|
||||
|
||||
[20, 30, 60, 120].forEach((framesPerSecond) => {
|
||||
describe(`(FPS independent trajectory: ${framesPerSecond} fps)`, function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
|
||||
beforeEach(function () {
|
||||
runtimeScene = makePlatformerTestRuntimeScene(1000 / framesPerSecond);
|
||||
|
||||
// Put a platformer object on a platform
|
||||
object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
behaviors: [
|
||||
{
|
||||
type: 'PlatformBehavior::PlatformerObjectBehavior',
|
||||
name: 'auto1',
|
||||
gravity: 1500,
|
||||
maxFallingSpeed: 1500,
|
||||
acceleration: 500,
|
||||
deceleration: 1500,
|
||||
maxSpeed: 500,
|
||||
jumpSpeed: 900,
|
||||
canGrabPlatforms: true,
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
});
|
||||
object.setCustomWidthAndHeight(10, 20);
|
||||
runtimeScene.addObject(object);
|
||||
});
|
||||
|
||||
const fallOnPlatform = (maxFrameCount) => {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < maxFrameCount; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
//Check the object is on the platform
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
};
|
||||
|
||||
it('can walk', function () {
|
||||
const platform = addPlatformObject(runtimeScene);
|
||||
platform.setPosition(0, -10);
|
||||
platform.setCustomWidthAndHeight(600, 32);
|
||||
|
||||
object.setPosition(0, -32);
|
||||
// Ensure the object falls on the platform
|
||||
fallOnPlatform(10);
|
||||
expect(object.getY()).to.be(-30);
|
||||
|
||||
// Accelerate
|
||||
for (let i = 0; i < framesPerSecond; ++i) {
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
|
||||
// Reached the maximum speed
|
||||
expect(object.getX()).to.be.within(250 - epsilon, 250 + epsilon);
|
||||
expect(object.getY()).to.be(platform.getY() - object.getHeight());
|
||||
expect(object.getBehavior('auto1').getCurrentSpeed()).to.be.within(
|
||||
500 - epsilon,
|
||||
500 + epsilon
|
||||
);
|
||||
|
||||
// Decelerate
|
||||
for (let i = 0; i < framesPerSecond / 3; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
|
||||
// Stopped
|
||||
expect(object.getX()).to.be.within(333, 334);
|
||||
expect(object.getY()).to.be(platform.getY() - object.getHeight());
|
||||
expect(object.getBehavior('auto1').getCurrentSpeed()).to.be(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,5 @@
|
||||
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
const epsilon = 1 / (2 << 16);
|
||||
describe('(falling)', function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
@@ -39,16 +40,19 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
});
|
||||
|
||||
it('can fall when in the air', function () {
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
// The character falls.
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
true
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
if (i < 10) expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
if (i < 10)
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
//Check the platform stopped the platformer object.
|
||||
// The platform stopped the character.
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
@@ -56,20 +60,21 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
|
||||
// The character walk out of the platform.
|
||||
for (let i = 0; i < 35; ++i) {
|
||||
//Check that the platformer object can fall.
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object.getX()).to.be.within(87.5, 87.51);
|
||||
expect(object.getY()).to.be(-24.75);
|
||||
expect(object.getX()).to.be.above(84);
|
||||
expect(object.getY()).to.be(-26.875);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
|
||||
|
||||
for (let i = 0; i < 100; ++i) {
|
||||
//Let the speed on X axis go back to 0.
|
||||
// Let the speed on X axis go back to 0.
|
||||
for (let i = 0; i < 50; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object.getBehavior('auto1').getCurrentSpeed()).to.be(0);
|
||||
});
|
||||
|
||||
it('falls when a platform is moved away', function () {
|
||||
@@ -115,6 +120,413 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
});
|
||||
});
|
||||
|
||||
[20, 30, 60, 120].forEach((framesPerSecond) => {
|
||||
describe(`(FPS independent trajectory: ${framesPerSecond} fps)`, function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
let platform;
|
||||
|
||||
beforeEach(function () {
|
||||
runtimeScene = makePlatformerTestRuntimeScene(1000 / framesPerSecond);
|
||||
|
||||
// Put a platformer object on a platform
|
||||
object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
behaviors: [
|
||||
{
|
||||
type: 'PlatformBehavior::PlatformerObjectBehavior',
|
||||
name: 'auto1',
|
||||
gravity: 1500,
|
||||
maxFallingSpeed: 1500,
|
||||
acceleration: 500,
|
||||
deceleration: 1500,
|
||||
maxSpeed: 500,
|
||||
jumpSpeed: 900,
|
||||
canGrabPlatforms: true,
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
});
|
||||
object.setCustomWidthAndHeight(10, 20);
|
||||
runtimeScene.addObject(object);
|
||||
object.setPosition(0, -32);
|
||||
|
||||
// Put a platform.
|
||||
platform = addPlatformObject(runtimeScene);
|
||||
platform.setPosition(0, -10);
|
||||
});
|
||||
|
||||
it('can jump', function () {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < framesPerSecond / 6; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
}
|
||||
|
||||
//Check the object is on the platform
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
|
||||
// Jump with sustaining 1/10 of second
|
||||
// A jump will at least sustain one frame,
|
||||
// because the jump key is pressed.
|
||||
// To have the same sustain time for each fps,
|
||||
// we use their greatest common divisor: 10.
|
||||
for (let i = 0; i < framesPerSecond / 10; ++i) {
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
}
|
||||
expect(object.getY()).to.be.within(-112.5 - epsilon, -112.5 + epsilon);
|
||||
|
||||
// Jump without sustaining
|
||||
for (let i = 0; i < framesPerSecond / 4 - 1; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Check that we reached the maximum height
|
||||
expect(object.getY()).to.be.above(-206.25);
|
||||
// At 30 fps, the maximum value is between 2 frames.
|
||||
if (framesPerSecond !== 30) {
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
expect(object.getY()).to.be.within(
|
||||
-206.25 - epsilon,
|
||||
-206.25 + epsilon
|
||||
);
|
||||
}
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
expect(object.getY()).to.be.above(-206.25);
|
||||
|
||||
// Then let the object fall
|
||||
for (let i = 0; i < framesPerSecond / 3 - 2; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
// The landing happens 1 or 2 frames sooner for some fps.
|
||||
// This is expected as a collision is involved.
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
|
||||
expect(object.getY()).to.be(-30);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// The legacy trajectory calculus uses Euler method instead of Verlet integration.
|
||||
// In this mode, the character is jumping higher at lower frame rates.
|
||||
describe('(FPS dependent trajectory: 120 fps)', function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
let platform;
|
||||
|
||||
beforeEach(function () {
|
||||
runtimeScene = makePlatformerTestRuntimeScene(1000 / 120);
|
||||
|
||||
// Put a platformer object on a platform
|
||||
object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
behaviors: [
|
||||
{
|
||||
type: 'PlatformBehavior::PlatformerObjectBehavior',
|
||||
name: 'auto1',
|
||||
gravity: 1500,
|
||||
maxFallingSpeed: 1500,
|
||||
acceleration: 500,
|
||||
deceleration: 1500,
|
||||
maxSpeed: 500,
|
||||
jumpSpeed: 900,
|
||||
canGrabPlatforms: true,
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: true,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
});
|
||||
object.setCustomWidthAndHeight(10, 20);
|
||||
runtimeScene.addObject(object);
|
||||
object.setPosition(0, -32);
|
||||
|
||||
// Put a platform.
|
||||
platform = addPlatformObject(runtimeScene);
|
||||
platform.setPosition(0, -10);
|
||||
});
|
||||
|
||||
it('can jump', function () {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < 120 / 6; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 120);
|
||||
}
|
||||
|
||||
// Check the object is on the platform
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
|
||||
// Jump with sustaining 1/10 of second
|
||||
for (let i = 0; i < 120 / 10; ++i) {
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 120);
|
||||
}
|
||||
expect(object.getY()).to.be(-113.125);
|
||||
|
||||
// Jump without sustaining
|
||||
for (let i = 0; i < 120 / 4; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 120);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Check that we reached the maximum height
|
||||
expect(object.getY()).to.be(-210);
|
||||
// The maximum is between these 2 frames
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be(-210);
|
||||
|
||||
// Then, let the object fall.
|
||||
for (let i = 0; i < 120 / 3; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 120);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
runtimeScene.renderAndStep(1000 / 120);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
|
||||
expect(object.getY()).to.be(-30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(FPS dependent trajectory: 60 fps)', function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
let platform;
|
||||
|
||||
beforeEach(function () {
|
||||
runtimeScene = makePlatformerTestRuntimeScene(1000 / 60);
|
||||
|
||||
// Put a platformer object on a platform
|
||||
object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
behaviors: [
|
||||
{
|
||||
type: 'PlatformBehavior::PlatformerObjectBehavior',
|
||||
name: 'auto1',
|
||||
gravity: 1500,
|
||||
maxFallingSpeed: 1500,
|
||||
acceleration: 500,
|
||||
deceleration: 1500,
|
||||
maxSpeed: 500,
|
||||
jumpSpeed: 900,
|
||||
canGrabPlatforms: true,
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: true,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
});
|
||||
object.setCustomWidthAndHeight(10, 20);
|
||||
runtimeScene.addObject(object);
|
||||
object.setPosition(0, -32);
|
||||
|
||||
// Put a platform.
|
||||
platform = addPlatformObject(runtimeScene);
|
||||
platform.setPosition(0, -10);
|
||||
});
|
||||
|
||||
it('can jump', function () {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
|
||||
//Check the object is on the platform
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
|
||||
// Jump with sustaining 1/10 of second
|
||||
for (let i = 0; i < 60 / 10; ++i) {
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 120);
|
||||
}
|
||||
expect(object.getY()).to.be(-113.75);
|
||||
|
||||
// Jump without sustaining
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
for (let i = 0; i < 60 / 4; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Check that we reached the maximum height
|
||||
expect(object.getY()).to.be.above(-220);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be(-220);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.above(-220);
|
||||
|
||||
// Then let the object fall
|
||||
for (let i = 0; i < 60 / 3; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
|
||||
expect(object.getY()).to.be(-30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(FPS dependent trajectory: 30 fps)', function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
let platform;
|
||||
|
||||
beforeEach(function () {
|
||||
runtimeScene = makePlatformerTestRuntimeScene(1000 / 30);
|
||||
|
||||
// Put a platformer object on a platform
|
||||
object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
behaviors: [
|
||||
{
|
||||
type: 'PlatformBehavior::PlatformerObjectBehavior',
|
||||
name: 'auto1',
|
||||
gravity: 1500,
|
||||
maxFallingSpeed: 1500,
|
||||
acceleration: 500,
|
||||
deceleration: 1500,
|
||||
maxSpeed: 500,
|
||||
jumpSpeed: 900,
|
||||
canGrabPlatforms: true,
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: true,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
});
|
||||
object.setCustomWidthAndHeight(10, 20);
|
||||
runtimeScene.addObject(object);
|
||||
object.setPosition(0, -32);
|
||||
|
||||
// Put a platform.
|
||||
platform = addPlatformObject(runtimeScene);
|
||||
platform.setPosition(0, -10);
|
||||
});
|
||||
|
||||
it('can jump', function () {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 30);
|
||||
}
|
||||
|
||||
//Check the object is on the platform
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
|
||||
// Jump with sustaining 1/10 of second
|
||||
for (let i = 0; i < 30 / 10; ++i) {
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 120);
|
||||
}
|
||||
expect(object.getY()).to.be(-115);
|
||||
|
||||
// Jump without sustaining
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
for (let i = 0; i < 30 / 4; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 30);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Check that we reached the maximum height
|
||||
expect(object.getY()).to.be(-233 - 1 / 3);
|
||||
// The maximum is between these 2 frames
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be(-233 - 1 / 3);
|
||||
|
||||
// Then let the object fall
|
||||
for (let i = 0; i < 30 / 3; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 30);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
runtimeScene.renderAndStep(1000 / 30);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
|
||||
expect(object.getY()).to.be(-30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(jump and jump sustain)', function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
@@ -154,56 +566,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
platform.setPosition(0, -10);
|
||||
});
|
||||
|
||||
it('can jump', function () {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
|
||||
//Check the object is on the platform
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isMoving()).to.be(false);
|
||||
|
||||
// Jump without sustaining
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
for (let i = 0; i < 18; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Check that we reached the maximum height
|
||||
expect(object.getY()).to.be.within(-180, -179);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be(-180);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-180, -179);
|
||||
|
||||
// Then let the object fall
|
||||
for (let i = 0; i < 17; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
|
||||
expect(object.getY()).to.be(-30);
|
||||
});
|
||||
|
||||
it('can jump, sustaining the jump', function () {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
@@ -231,21 +593,24 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
}
|
||||
|
||||
// Check the height reached
|
||||
expect(object.getY()).to.be(-230);
|
||||
expect(object.getY()).to.be.within(-225 - epsilon, -225 + epsilon);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be(-235);
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
expect(object.getY()).to.be.within(
|
||||
-229.5833333333333 - epsilon,
|
||||
-229.5833333333333 + epsilon
|
||||
);
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
// Verify that pressing the jump key does not change anything
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
|
||||
// Check that we reached the maximum height
|
||||
expect(object.getY()).to.be(-247.5);
|
||||
expect(object.getY()).to.be.above(-240);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be(-247.5);
|
||||
expect(object.getY()).to.be.within(-240 - epsilon, -240 + epsilon);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-247, -246);
|
||||
expect(object.getY()).to.be.above(-240);
|
||||
|
||||
// Then let the object fall
|
||||
for (let i = 0; i < 60; ++i) {
|
||||
@@ -273,7 +638,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object.getY()).to.be.within(-101, -100);
|
||||
expect(object.getY()).to.be.within(-100, -99);
|
||||
|
||||
// Stop holding the jump key
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
@@ -285,23 +650,26 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
}
|
||||
|
||||
// Check that we reached the maximum height
|
||||
expect(object.getY()).to.be.within(-206, -205);
|
||||
expect(object.getY()).to.be.above(-199.7916666666666 + epsilon);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-208, -207);
|
||||
expect(object.getY()).to.be.within(
|
||||
-199.7916666666666 - epsilon,
|
||||
-199.7916666666666 + epsilon
|
||||
);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-208, -207);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-208, -207);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-206, -205);
|
||||
expect(object.getY()).to.be.within(
|
||||
-199.7916666666666 - epsilon,
|
||||
-199.7916666666666 + epsilon
|
||||
);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.above(-199.7916666666666 + epsilon);
|
||||
|
||||
// Then let the object fall
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
|
||||
false
|
||||
);
|
||||
for (let i = 0; i < 60; ++i) {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object.getY()).to.be(-30);
|
||||
@@ -492,7 +860,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
|
||||
// Jump, reach the top and go down
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
for (let i = 0; i < 29; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
}
|
||||
@@ -612,7 +980,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
});
|
||||
|
||||
it('can jump through a jumpthru and land', function () {
|
||||
jumpthru.setPosition(0, -33);
|
||||
jumpthru.setPosition(0, -32);
|
||||
//Check the platform stopped the platformer object.
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
@@ -629,15 +997,15 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-39, -38);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-47, -46);
|
||||
expect(object.getY()).to.be.within(-46, -45);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
// At this step, the object is almost on the jumpthru (-53 + 20 (object height) = -33 (jump thru Y position)),
|
||||
// At this step, the object is almost on the jumpthru (-52 + 20 (object height) = -32 (jump thru Y position)),
|
||||
// but the object should not stop.
|
||||
expect(object.getY()).to.be.within(-54, -53);
|
||||
expect(object.getY()).to.be.within(-53, -52);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-61, -60);
|
||||
expect(object.getY()).to.be.within(-60, -59);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(-67, -66);
|
||||
expect(object.getY()).to.be.within(-66, -65);
|
||||
|
||||
// Verify the object is still jumping
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
@@ -647,7 +1015,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object.getY()).to.be.within(-89, -88);
|
||||
expect(object.getY()).to.be.within(-83, -82);
|
||||
|
||||
// Verify the object is now considered as falling in its jump:
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
@@ -669,9 +1037,9 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
it('can jump right under a jumpthru without landing', function () {
|
||||
// A big one because the object jump to the right.
|
||||
jumpthru.setCustomWidthAndHeight(600, 20);
|
||||
const highestJumpY = -104; // actually -103.6
|
||||
const highestJumpY = -99.41666666666661;
|
||||
// Right above the maximum reach by jumping
|
||||
jumpthru.setPosition(0, highestJumpY + object.getHeight());
|
||||
jumpthru.setPosition(0, Math.floor(highestJumpY) + object.getHeight());
|
||||
|
||||
// The object landed on the platform.
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
@@ -686,14 +1054,18 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
|
||||
// The object jumps.
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
for (let i = 0; i < 17; ++i) {
|
||||
for (let i = 0; i < 16; ++i) {
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
}
|
||||
// The object is at the highest of the jump.
|
||||
expect(object.getY()).to.be.within(highestJumpY, highestJumpY + 1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(
|
||||
highestJumpY - epsilon,
|
||||
highestJumpY + epsilon
|
||||
);
|
||||
|
||||
// The object starts to fall.
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
@@ -715,9 +1087,9 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
it('can jump right above a jumpthru and landing', function () {
|
||||
// A big one because the object jump to the right.
|
||||
jumpthru.setCustomWidthAndHeight(600, 20);
|
||||
const highestJumpY = -104; // actually -103.6
|
||||
const highestJumpY = -99.41666666666661;
|
||||
// Right above the maximum reach by jumping
|
||||
jumpthru.setPosition(0, highestJumpY + 1 + object.getHeight());
|
||||
jumpthru.setPosition(0, Math.ceil(highestJumpY) + object.getHeight());
|
||||
|
||||
// The object landed on the platform.
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
@@ -732,14 +1104,18 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
|
||||
// The object jumps.
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
for (let i = 0; i < 17; ++i) {
|
||||
for (let i = 0; i < 16; ++i) {
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isJumping()).to.be(true);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(false);
|
||||
}
|
||||
// The object is at the highest of the jump.
|
||||
expect(object.getY()).to.be.within(highestJumpY, highestJumpY + 1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getY()).to.be.within(
|
||||
highestJumpY - epsilon,
|
||||
highestJumpY + epsilon
|
||||
);
|
||||
|
||||
// The object landed on the jumpthru.
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
@@ -774,7 +1150,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('and gdjs.PlatformRuntimeBehavior at same time', function () {
|
||||
describe('and gdjs.PlatformRuntimeBehavior at same time - ', function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
let platform;
|
||||
@@ -848,38 +1224,45 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
object2.setPosition(9, -60);
|
||||
});
|
||||
|
||||
it('can jump through the jumpthru', function () {
|
||||
// Check that the second object falls (it's not stopped by itself)
|
||||
it('can move', function () {
|
||||
// The 2nd object falls (it's not stopped by itself).
|
||||
expect(object2.getY()).to.be(-60);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object2.getY()).to.be(-59.75);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object2.getY()).to.be(-59.25);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object2.getY()).to.be(-58.5);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object2.getY()).to.be(-57.5);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const previousY = object2.getY();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object2.getY()).to.be.above(previousY);
|
||||
expect(object2.getBehavior('PlatformerObject').isFalling()).to.be(true);
|
||||
}
|
||||
expect(object2.getY()).to.be(-58);
|
||||
|
||||
//Check the first object stays on the platform.
|
||||
// The 1st object stays on the platform.
|
||||
expect(object.getY()).to.be(-30);
|
||||
|
||||
// Simulate more frames. Check that trying to jump won't do anything.
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
// The 2nd object can't jump on itself.
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
object2.getBehavior('PlatformerObject').simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object2.getY()).to.be(-48.75);
|
||||
expect(object2.getY()).to.be.below(object.getY() - object2.getHeight());
|
||||
expect(object.getX()).to.be(0);
|
||||
expect(object.getY()).to.be(-30);
|
||||
|
||||
// Verify that the first platformer object is moved 1px to the left
|
||||
// as the falling platformer object+platform collides with it
|
||||
// At the 1st frame of collision, the result depends on execution order.
|
||||
// The effect on the 1st object is not tested.
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object2.getY()).to.be(-46.25);
|
||||
expect(object2.getY()).to.be.above(object.getY() - object2.getHeight());
|
||||
|
||||
// 1st the object can be pushed down, when the intersection height < 1.
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
// The falling platformer object+platform collides with the 1st object.
|
||||
// The 1st object moves 1px to the left.
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object2.getY()).to.be.above(
|
||||
object.getY() - object2.getHeight() + 1
|
||||
);
|
||||
expect(object.getX()).to.be(-1);
|
||||
expect(object.getY()).to.be(-30);
|
||||
|
||||
// Simulate more frames so that the object reaches the floor
|
||||
// The 2nd object reaches the floor.
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
@@ -895,9 +1278,9 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object2.getX()).to.be(9);
|
||||
expect(object2.getY()).to.be(-72.5);
|
||||
expect(object2.getY()).to.be.below(-70);
|
||||
expect(object.getX()).to.be(-1);
|
||||
expect(object.getY()).to.be(-72.5);
|
||||
expect(object.getY()).to.be.below(-70);
|
||||
|
||||
// Try to go right for the first object: won't work because the other
|
||||
// object is a platform.
|
||||
@@ -906,9 +1289,9 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object2.getX()).to.be(9);
|
||||
expect(object2.getY()).to.be.within(-94.2, -94.1);
|
||||
expect(object2.getY()).to.be.below(-90);
|
||||
expect(object.getX()).to.be(-1);
|
||||
expect(object.getY()).to.be.within(-94.2, -94.1);
|
||||
expect(object.getY()).to.be.below(-90);
|
||||
|
||||
// Try to go right for the first and second object: can do.
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
@@ -916,18 +1299,18 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
object2.getBehavior('PlatformerObject').simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object2.getX()).to.be.within(9.83, 9.84);
|
||||
expect(object2.getY()).to.be.within(-101.2, -101.1);
|
||||
expect(object.getX()).to.be.within(-0.59, -0.58);
|
||||
expect(object.getY()).to.be.within(-101.2, -101.1);
|
||||
expect(object2.getX()).to.be.above(9.5);
|
||||
expect(object2.getY()).to.be.below(-95);
|
||||
expect(object.getX()).to.be.above(-0.8);
|
||||
expect(object.getY()).to.be.below(-95);
|
||||
|
||||
// Let the object fall back on the floor.
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object2.getX()).to.be.within(9.83, 9.84);
|
||||
expect(object2.getX()).to.be.above(9.5);
|
||||
expect(object2.getY()).to.be(-30);
|
||||
expect(object.getX()).to.be.within(-0.59, -0.58);
|
||||
expect(object.getX()).to.be.above(-0.8);
|
||||
expect(object.getY()).to.be(-30);
|
||||
});
|
||||
});
|
||||
|
@@ -38,7 +38,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
platform.setPosition(0, -10);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
//Put the object near the right ledge of the platform.
|
||||
// Put the character near the right ledge of the platform.
|
||||
object.setPosition(
|
||||
platform.getX() + platform.getWidth() + 2,
|
||||
platform.getY() - 10
|
||||
@@ -49,14 +49,16 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
|
||||
//Check that the object grabbed the platform
|
||||
// The character grabs the platform.
|
||||
expect(object.getX()).to.be.within(
|
||||
platform.getX() + platform.getWidth() + 0,
|
||||
platform.getX() + platform.getWidth() + 1
|
||||
);
|
||||
expect(object.getY()).to.be(platform.getY());
|
||||
|
||||
// The character releases the platform.
|
||||
object.getBehavior('auto1').simulateReleasePlatformKey();
|
||||
// The character falls.
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isFalling()).to.be(true);
|
||||
@@ -64,9 +66,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
//Check that the object is falling
|
||||
expect(object.getY()).to.be(3.75);
|
||||
expect(object.getY()).to.be.above(0);
|
||||
});
|
||||
|
||||
[true, false].forEach((addTopPlatformFirst) => {
|
||||
@@ -298,11 +298,11 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
releaseLadder(10);
|
||||
object.getBehavior('auto1').simulateLadderKey();
|
||||
expect(object.getY()).to.be.within(
|
||||
// gravity is 1500, 10 frames falling ~ 23px
|
||||
objectPositionAfterFirstClimb + 22,
|
||||
objectPositionAfterFirstClimb + 24
|
||||
// gravity is 1500, 10 frames falling ~ 21px
|
||||
objectPositionAfterFirstClimb + 20,
|
||||
objectPositionAfterFirstClimb + 21
|
||||
);
|
||||
climbLadder(24);
|
||||
climbLadder(23);
|
||||
// Check that we reached the maximum height
|
||||
const playerAtLadderTop = ladder.getY() - object.getHeight();
|
||||
expect(object.getY()).to.be.within(
|
||||
@@ -355,7 +355,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
|
||||
// Jump
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
for (let i = 0; i < 19; ++i) {
|
||||
for (let i = 0; i < 18; ++i) {
|
||||
jumpAndAscend(1);
|
||||
}
|
||||
|
||||
@@ -407,10 +407,10 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
// and grab the 2nd one, even if still ascending
|
||||
object.getBehavior('auto1').simulateLadderKey();
|
||||
// still moves a little because of inertia
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
|
||||
}
|
||||
stayOnLadder(1);
|
||||
});
|
||||
|
||||
@@ -431,6 +431,10 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
|
||||
}
|
||||
// Here, if we had pressed Right the character would have been falling.
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
|
||||
// Now, it falls.
|
||||
fall(5);
|
||||
});
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
const epsilon = 1 / (2 << 8);
|
||||
const epsilon = 1 / (2 << 16);
|
||||
|
||||
describe('(moving platforms)', function () {
|
||||
let runtimeScene;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
|
||||
const makePlatformerTestRuntimeScene = () => {
|
||||
const makePlatformerTestRuntimeScene = (timeDelta = 1000 / 60) => {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
resources: {
|
||||
@@ -16,7 +16,7 @@
|
||||
instances: [],
|
||||
});
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return (1 / 60) * 1000;
|
||||
return timeDelta;
|
||||
};
|
||||
return runtimeScene;
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
const epsilon = 1 / (2 << 8);
|
||||
const epsilon = 1 / (2 << 16);
|
||||
|
||||
describe('(walk on slopes)', function () {
|
||||
let runtimeScene;
|
||||
@@ -362,7 +362,13 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
walkRight(30);
|
||||
expect(object.getX()).to.be.above(platform.getX());
|
||||
// Gone downward following the 2nd platform.
|
||||
expect(object.getY()).to.be(platform.getY() - object.getHeight());
|
||||
// The floor detection can't round it to 30
|
||||
// because the character bottom is 50 with rounding error
|
||||
// 29.999999999999996 + 20 = 50
|
||||
expect(object.getY()).to.be.within(
|
||||
platform.getY() - object.getHeight() - epsilon,
|
||||
platform.getY() - object.getHeight() + epsilon
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -95,7 +95,7 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
downwardDeltaY
|
||||
) => {
|
||||
const result = characterBehavior._findHighestFloorAndMoveOnTop(
|
||||
[platformBehavior],
|
||||
[platformBehavior.currentRBushAABB],
|
||||
upwardDeltaY,
|
||||
downwardDeltaY
|
||||
);
|
||||
@@ -110,7 +110,7 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
) => {
|
||||
const oldY = characterBehavior.owner.getY();
|
||||
const result = characterBehavior._findHighestFloorAndMoveOnTop(
|
||||
[platformBehavior],
|
||||
[platformBehavior.currentRBushAABB],
|
||||
upwardDeltaY,
|
||||
downwardDeltaY
|
||||
);
|
||||
@@ -127,7 +127,7 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
) => {
|
||||
const oldY = characterBehavior.owner.getY();
|
||||
const result = characterBehavior._findHighestFloorAndMoveOnTop(
|
||||
[platformBehavior],
|
||||
[platformBehavior.currentRBushAABB],
|
||||
upwardDeltaY,
|
||||
downwardDeltaY
|
||||
);
|
||||
@@ -136,9 +136,6 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
expect(characterBehavior.owner.getY()).to.be(oldY);
|
||||
};
|
||||
|
||||
const noCollision = gdjs.PlatformerObjectRuntimeBehavior._noCollision;
|
||||
const floorIsTooHigh = gdjs.PlatformerObjectRuntimeBehavior._floorIsTooHigh;
|
||||
|
||||
[false, true].forEach((swapVerticesOrder) => {
|
||||
describe(`(swapVertexOrder: ${swapVerticesOrder})`, function () {
|
||||
const collisionMasks = {
|
||||
@@ -304,6 +301,10 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
platform.setCustomWidthAndHeight(300, 300);
|
||||
platform.setPosition(position[0], position[1]);
|
||||
const platformBehavior = platform.getBehavior('Platform');
|
||||
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
|
||||
runtimeScene
|
||||
);
|
||||
platformObstaclesManager.addPlatform(platformBehavior);
|
||||
|
||||
it('can detect a platform away downward', function () {
|
||||
character.setPosition(300, -210.1);
|
||||
@@ -372,6 +373,10 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
platform.setCustomWidthAndHeight(300, 300);
|
||||
platform.setPosition(position[0], position[1]);
|
||||
const platformBehavior = platform.getBehavior('Platform');
|
||||
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
|
||||
runtimeScene
|
||||
);
|
||||
platformObstaclesManager.addPlatform(platformBehavior);
|
||||
|
||||
it('can detect an obstacle overlapping the top', function () {
|
||||
// -10 because the character can follow a platform downward.
|
||||
@@ -398,6 +403,10 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
platform.setCustomWidthAndHeight(200, 200);
|
||||
platform.setPosition(250, -250);
|
||||
const platformBehavior = platform.getBehavior('Platform');
|
||||
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
|
||||
runtimeScene
|
||||
);
|
||||
platformObstaclesManager.addPlatform(platformBehavior);
|
||||
|
||||
it('can detect a tunnel ceiling', function () {
|
||||
character.setPosition(300, -210.1);
|
||||
@@ -443,12 +452,15 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
const runtimeScene = makeTestRuntimeScene();
|
||||
const character = addCharacter(runtimeScene);
|
||||
const behavior = character.getBehavior('auto1');
|
||||
|
||||
const platform = addPlatform(
|
||||
runtimeScene,
|
||||
collisionMasks.verticalTunnel
|
||||
);
|
||||
const platformBehavior = platform.getBehavior('Platform');
|
||||
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
|
||||
runtimeScene
|
||||
);
|
||||
platformObstaclesManager.addPlatform(platformBehavior);
|
||||
|
||||
it('can fell inside a vertical tunnel that fit the character', function () {
|
||||
platform.setCustomWidthAndHeight(200, 200);
|
||||
@@ -482,6 +494,10 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
|
||||
const platform = addPlatform(runtimeScene, collisionMasks.square);
|
||||
const platformBehavior = platform.getBehavior('Platform');
|
||||
const platformObstaclesManager = gdjs.PlatformObjectsManager.getManager(
|
||||
runtimeScene
|
||||
);
|
||||
platformObstaclesManager.addPlatform(platformBehavior);
|
||||
|
||||
it('can detect a platform at its exact position', function () {
|
||||
platform.setCustomWidthAndHeight(100, 100);
|
||||
|
@@ -5,6 +5,7 @@ Copyright (c) 2008-2016 Florian Rival (Florian.Rival@gmail.com)
|
||||
This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#include "GDCore/Extensions/Metadata/MultipleInstructionMetadata.h"
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
#include "ShapePainterObject.h"
|
||||
@@ -40,10 +41,10 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
"res/actions/rectangle.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("Top left side: X position"))
|
||||
.AddParameter("expression", _("Top left side: Y position"))
|
||||
.AddParameter("expression", _("Bottom right side: X position"))
|
||||
.AddParameter("expression", _("Bottom right side: Y position"))
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.SetFunctionName("DrawRectangle")
|
||||
.SetIncludeFile("PrimitiveDrawing/ShapePainterObject.h");
|
||||
|
||||
@@ -131,10 +132,10 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
"res/actions/roundedRectangle.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("Top left side: X position"))
|
||||
.AddParameter("expression", _("Top left side: Y position"))
|
||||
.AddParameter("expression", _("Bottom right side: X position"))
|
||||
.AddParameter("expression", _("Bottom right side: Y position"))
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.AddParameter("expression", _("Radius (in pixels)"))
|
||||
.SetFunctionName("DrawRoundedRectangle")
|
||||
.SetIncludeFile("PrimitiveDrawing/ShapePainterObject.h");
|
||||
@@ -632,5 +633,162 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("AreCoordinatesRelative")
|
||||
.SetIncludeFile("PrimitiveDrawing/ShapePainterObject.h");
|
||||
|
||||
obj.AddAction("Scale",
|
||||
_("Scale"),
|
||||
_("Modify the scale of the specified object."),
|
||||
_("the scale"),
|
||||
_("Size"),
|
||||
"res/actions/scale24.png",
|
||||
"res/actions/scale.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddExpressionAndConditionAndAction("number",
|
||||
"ScaleX",
|
||||
_("Scale on X axis"),
|
||||
_("the width's scale of an object"),
|
||||
_("the width's scale"),
|
||||
_("Size"),
|
||||
"res/actions/scaleWidth24.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddExpressionAndConditionAndAction("number",
|
||||
"ScaleY",
|
||||
_("Scale on Y axis"),
|
||||
_("the height's scale of an object"),
|
||||
_("the height's scale"),
|
||||
_("Size"),
|
||||
"res/actions/scaleHeight24.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("FlipX",
|
||||
_("Flip the object horizontally"),
|
||||
_("Flip the object horizontally"),
|
||||
_("Flip horizontally _PARAM0_: _PARAM1_"),
|
||||
_("Effects"),
|
||||
"res/actions/flipX24.png",
|
||||
"res/actions/flipX.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("yesorno", _("Activate flipping"))
|
||||
.MarkAsSimple();
|
||||
|
||||
obj.AddAction("FlipY",
|
||||
_("Flip the object vertically"),
|
||||
_("Flip the object vertically"),
|
||||
_("Flip vertically _PARAM0_: _PARAM1_"),
|
||||
_("Effects"),
|
||||
"res/actions/flipY24.png",
|
||||
"res/actions/flipY.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("yesorno", _("Activate flipping"))
|
||||
.MarkAsSimple();
|
||||
|
||||
obj.AddCondition("FlippedX",
|
||||
_("Horizontally flipped"),
|
||||
_("Check if the object is horizontally flipped"),
|
||||
_("_PARAM0_ is horizontally flipped"),
|
||||
_("Effects"),
|
||||
"res/actions/flipX24.png",
|
||||
"res/actions/flipX.png")
|
||||
.AddParameter("object", _("Object"), "Drawer");
|
||||
|
||||
obj.AddCondition("FlippedY",
|
||||
_("Vertically flipped"),
|
||||
_("Check if the object is vertically flipped"),
|
||||
_("_PARAM0_ is vertically flipped"),
|
||||
_("Effects"),
|
||||
"res/actions/flipY24.png",
|
||||
"res/actions/flipY.png")
|
||||
.AddParameter("object", _("Object"), "Drawer");
|
||||
|
||||
obj.AddAction("Width",
|
||||
_("Width"),
|
||||
_("Change the width of an object."),
|
||||
_("the width"),
|
||||
_("Size"),
|
||||
"res/actions/scaleWidth24.png",
|
||||
"res/actions/scale.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("Height",
|
||||
_("Height"),
|
||||
_("Change the height of an object."),
|
||||
_("the height"),
|
||||
_("Size"),
|
||||
"res/actions/scaleHeight24.png",
|
||||
"res/actions/scale.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("SetRotationCenter",
|
||||
_("Center of rotation"),
|
||||
_("Change the center of rotation of an object relatively to the object origin."),
|
||||
_("Change the center of rotation of _PARAM0_: _PARAM1_; _PARAM2_"),
|
||||
_("Angle"),
|
||||
"res/actions/position24.png",
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X position"))
|
||||
.AddParameter("expression", _("Y position"))
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("SetRectangularCollisionMask",
|
||||
_("Collision Mask"),
|
||||
_("Change the collision mask of an object to a rectangle relatively to the object origin."),
|
||||
_("Change the collision mask of _PARAM0_ to a rectangle from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_"),
|
||||
_("Position"),
|
||||
"res/actions/position24.png",
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddExpression("ToDrawingX",
|
||||
_("X drawing coordinate of a point from the scene"),
|
||||
_("X drawing coordinate of a point from the scene"),
|
||||
_("Position"),
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X scene position"))
|
||||
.AddParameter("expression", _("Y scene position"));
|
||||
|
||||
obj.AddExpression("ToDrawingY",
|
||||
_("Y drawing coordinate of a point from the scene"),
|
||||
_("Y drawing coordinate of a point from the scene"),
|
||||
_("Position"),
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X scene position"))
|
||||
.AddParameter("expression", _("Y scene position"));
|
||||
|
||||
obj.AddExpression("ToSceneX",
|
||||
_("X scene coordinate of a point from the drawing"),
|
||||
_("X scene coordinate of a point from the drawing"),
|
||||
_("Position"),
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X drawing position"))
|
||||
.AddParameter("expression", _("Y drawing position"));
|
||||
|
||||
obj.AddExpression("ToSceneY",
|
||||
_("Y scene coordinate of a point from the drawing"),
|
||||
_("Y scene coordinate of a point from the drawing"),
|
||||
_("Position"),
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X drawing position"))
|
||||
.AddParameter("expression", _("Y drawing position"));
|
||||
|
||||
#endif
|
||||
}
|
||||
|
@@ -170,6 +170,73 @@ class PrimitiveDrawingJsExtension : public gd::PlatformExtension {
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::AreCoordinatesRelative"]
|
||||
.SetFunctionName("areCoordinatesRelative");
|
||||
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Scale"]
|
||||
.SetFunctionName("setScale")
|
||||
.SetGetter("getScale");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Drawer::SetScaleX"]
|
||||
.SetFunctionName("setScaleX")
|
||||
.SetGetter("getScaleX");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Drawer::SetScaleY"]
|
||||
.SetFunctionName("setScaleY")
|
||||
.SetGetter("getScaleY");
|
||||
GetAllConditionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::ScaleX"]
|
||||
.SetFunctionName("getScaleX");
|
||||
GetAllConditionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::ScaleY"]
|
||||
.SetFunctionName("getScaleY");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::ScaleX"]
|
||||
.SetFunctionName("getScaleX");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::ScaleY"]
|
||||
.SetFunctionName("getScaleY");
|
||||
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::FlipX"]
|
||||
.SetFunctionName("flipX");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::FlipY"]
|
||||
.SetFunctionName("flipY");
|
||||
GetAllConditionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::FlippedX"]
|
||||
.SetFunctionName("isFlippedX");
|
||||
GetAllConditionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::FlippedY"]
|
||||
.SetFunctionName("isFlippedY");
|
||||
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Width"]
|
||||
.SetFunctionName("setWidth")
|
||||
.SetGetter("getWidth");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Height"]
|
||||
.SetFunctionName("setHeight")
|
||||
.SetGetter("getHeight");
|
||||
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::SetRotationCenter"]
|
||||
.SetFunctionName("setRotationCenter");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::SetRectangularCollisionMask"]
|
||||
.SetFunctionName("setRectangularCollisionMask");
|
||||
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["ToDrawingX"]
|
||||
.SetFunctionName("transformToDrawingX");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["ToDrawingY"]
|
||||
.SetFunctionName("transformToDrawingY");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["ToSceneX"]
|
||||
.SetFunctionName("transformToSceneX");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["ToSceneY"]
|
||||
.SetFunctionName("transformToSceneX");
|
||||
|
||||
GD_COMPLETE_EXTENSION_COMPILATION_INFORMATION();
|
||||
};
|
||||
};
|
||||
|
@@ -4,6 +4,26 @@ namespace gdjs {
|
||||
class ShapePainterRuntimeObjectPixiRenderer {
|
||||
_object: gdjs.ShapePainterRuntimeObject;
|
||||
_graphics: PIXI.Graphics;
|
||||
/**
|
||||
* Graphics positions can need updates when shapes are added,
|
||||
* this avoids to do it each time.
|
||||
*/
|
||||
_positionXIsUpToDate = false;
|
||||
/**
|
||||
* Graphics positions can need updates when shapes are added,
|
||||
* this avoids to do it each time.
|
||||
*/
|
||||
_positionYIsUpToDate = false;
|
||||
/**
|
||||
* This allows to use the transformation of the renderer
|
||||
* and compute it only when necessary.
|
||||
*/
|
||||
_transformationIsUpToDate = false;
|
||||
|
||||
private static readonly _positionForTransformation: PIXI.IPointData = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
constructor(
|
||||
runtimeObject: gdjs.ShapePainterRuntimeObject,
|
||||
@@ -23,6 +43,7 @@ namespace gdjs {
|
||||
|
||||
clear() {
|
||||
this._graphics.clear();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawRectangle(x1: float, y1: float, x2: float, y2: float) {
|
||||
@@ -33,6 +54,7 @@ namespace gdjs {
|
||||
);
|
||||
this._graphics.drawRect(x1, y1, x2 - x1, y2 - y1);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawCircle(x: float, y: float, radius: float) {
|
||||
@@ -43,6 +65,7 @@ namespace gdjs {
|
||||
);
|
||||
this._graphics.drawCircle(x, y, radius);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawLine(x1: float, y1: float, x2: float, y2: float, thickness: float) {
|
||||
@@ -68,6 +91,7 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawLineV2(x1: float, y1: float, x2: float, y2: float, thickness: float) {
|
||||
@@ -79,6 +103,7 @@ namespace gdjs {
|
||||
this._graphics.moveTo(x1, y1);
|
||||
this._graphics.lineTo(x2, y2);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawEllipse(x1: float, y1: float, width: float, height: float) {
|
||||
@@ -89,6 +114,7 @@ namespace gdjs {
|
||||
);
|
||||
this._graphics.drawEllipse(x1, y1, width / 2, height / 2);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawRoundedRectangle(
|
||||
@@ -106,6 +132,7 @@ namespace gdjs {
|
||||
this._graphics.drawRoundedRect(x1, y1, x2 - x1, y2 - y1, radius);
|
||||
this._graphics.closePath();
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawStar(
|
||||
@@ -132,6 +159,7 @@ namespace gdjs {
|
||||
);
|
||||
this._graphics.closePath();
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawArc(
|
||||
@@ -164,6 +192,7 @@ namespace gdjs {
|
||||
this._graphics.closePath();
|
||||
}
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawBezierCurve(
|
||||
@@ -184,6 +213,7 @@ namespace gdjs {
|
||||
this._graphics.moveTo(x1, y1);
|
||||
this._graphics.bezierCurveTo(cpX, cpY, cpX2, cpY2, x2, y2);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawQuadraticCurve(
|
||||
@@ -202,6 +232,7 @@ namespace gdjs {
|
||||
this._graphics.moveTo(x1, y1);
|
||||
this._graphics.quadraticCurveTo(cpX, cpY, x2, y2);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
beginFillPath() {
|
||||
@@ -213,6 +244,7 @@ namespace gdjs {
|
||||
|
||||
endFillPath() {
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawPathMoveTo(x1: float, y1: float) {
|
||||
@@ -221,6 +253,7 @@ namespace gdjs {
|
||||
|
||||
drawPathLineTo(x1: float, y1: float) {
|
||||
this._graphics.lineTo(x1, y1);
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawPathBezierCurveTo(
|
||||
@@ -232,6 +265,7 @@ namespace gdjs {
|
||||
toY: float
|
||||
) {
|
||||
this._graphics.bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY);
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawPathArc(
|
||||
@@ -250,14 +284,17 @@ namespace gdjs {
|
||||
gdjs.toRad(endAngle),
|
||||
anticlockwise ? true : false
|
||||
);
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawPathQuadraticCurveTo(cpX: float, cpY: float, toX: float, toY: float) {
|
||||
this._graphics.quadraticCurveTo(cpX, cpY, toX, toY);
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
closePath() {
|
||||
this._graphics.closePath();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
updateOutline(): void {
|
||||
@@ -268,20 +305,185 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
updateXPosition(): void {
|
||||
if (!this._object._absoluteCoordinates) {
|
||||
this._graphics.position.x = this._object.x;
|
||||
} else {
|
||||
invalidateBounds() {
|
||||
this._object.invalidateBounds();
|
||||
this._positionXIsUpToDate = false;
|
||||
this._positionYIsUpToDate = false;
|
||||
}
|
||||
|
||||
updatePreRender(): void {
|
||||
this.updatePositionIfNeeded();
|
||||
}
|
||||
|
||||
updatePositionX(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.pivot.x = 0;
|
||||
this._graphics.position.x = 0;
|
||||
} else {
|
||||
// Make the drawing rotate around the rotation center.
|
||||
this._graphics.pivot.x = this._object.getRotationCenterX();
|
||||
// Multiply by the scale to have the scale anchor
|
||||
// at the object position instead of the center.
|
||||
this._graphics.position.x =
|
||||
this._object.x +
|
||||
this._graphics.pivot.x * Math.abs(this._graphics.scale.x);
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updatePositionY(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.pivot.y = 0;
|
||||
this._graphics.position.y = 0;
|
||||
} else {
|
||||
this._graphics.pivot.y = this._object.getRotationCenterY();
|
||||
this._graphics.position.y =
|
||||
this._object.y +
|
||||
this._graphics.pivot.y * Math.abs(this._graphics.scale.y);
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updatePositionIfNeeded() {
|
||||
if (!this._positionXIsUpToDate) {
|
||||
this.updatePositionX();
|
||||
this._positionXIsUpToDate = true;
|
||||
}
|
||||
if (!this._positionYIsUpToDate) {
|
||||
this.updatePositionY();
|
||||
this._positionYIsUpToDate = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateYPosition(): void {
|
||||
if (!this._object._absoluteCoordinates) {
|
||||
this._graphics.position.y = this._object.y;
|
||||
} else {
|
||||
this._graphics.position.y = 0;
|
||||
updateTransformationIfNeeded() {
|
||||
if (!this._transformationIsUpToDate) {
|
||||
this.updatePositionIfNeeded();
|
||||
this._graphics.updateTransform();
|
||||
}
|
||||
this._transformationIsUpToDate = true;
|
||||
}
|
||||
|
||||
updateRotationCenter(): void {
|
||||
// The pivot and position depends on the rotation center point.
|
||||
this._positionXIsUpToDate = false;
|
||||
this._positionYIsUpToDate = false;
|
||||
// The whole transformation changes based on the rotation center point.
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updateAngle(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.angle = 0;
|
||||
} else {
|
||||
this._graphics.angle = this._object.angle;
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updateScaleX(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.scale.x = 1;
|
||||
} else {
|
||||
this._graphics.scale.x = this._object._scaleX;
|
||||
}
|
||||
// updatePositionX() uses scale.x
|
||||
this._positionXIsUpToDate = false;
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updateScaleY(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.scale.y = 1;
|
||||
} else {
|
||||
this._graphics.scale.y = this._object._scaleY;
|
||||
}
|
||||
// updatePositionY() uses scale.y
|
||||
this._positionYIsUpToDate = false;
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
getDrawableX(): float {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
return this._graphics.getLocalBounds().left;
|
||||
}
|
||||
let localBound = this._graphics.getLocalBounds().left;
|
||||
if (this._object._flippedX) {
|
||||
const rotationCenterX = this._object.getRotationCenterX();
|
||||
localBound = 2 * rotationCenterX - localBound;
|
||||
}
|
||||
// When new shape are drawn, the bounds of the object can extend.
|
||||
// The object position stays the same but (drawableX; drawableY) can change.
|
||||
return (
|
||||
this._object.getX() + localBound * Math.abs(this._graphics.scale.x)
|
||||
);
|
||||
}
|
||||
|
||||
getDrawableY(): float {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
return this._graphics.getLocalBounds().top;
|
||||
}
|
||||
let localBound = this._graphics.getLocalBounds().top;
|
||||
if (this._object._flippedY) {
|
||||
const rotationCenterY = this._object.getRotationCenterY();
|
||||
localBound = 2 * rotationCenterY - localBound;
|
||||
}
|
||||
return (
|
||||
this._object.getY() + localBound * Math.abs(this._graphics.scale.y)
|
||||
);
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this._graphics.width;
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return this._graphics.height;
|
||||
}
|
||||
|
||||
getUnscaledWidth(): float {
|
||||
return this._graphics.getLocalBounds().width;
|
||||
}
|
||||
|
||||
getUnscaledHeight(): float {
|
||||
return this._graphics.getLocalBounds().height;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The drawing origin relatively to the drawable top left corner.
|
||||
*/
|
||||
getFrameRelativeOriginX() {
|
||||
return -this._graphics.getLocalBounds().left;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The drawing origin relatively to the drawable top left corner.
|
||||
*/
|
||||
getFrameRelativeOriginY() {
|
||||
return -this._graphics.getLocalBounds().top;
|
||||
}
|
||||
|
||||
transformToDrawing(point: FloatPoint): FloatPoint {
|
||||
this.updateTransformationIfNeeded();
|
||||
const position =
|
||||
ShapePainterRuntimeObjectPixiRenderer._positionForTransformation;
|
||||
position.x = point[0];
|
||||
position.y = point[1];
|
||||
this._graphics.localTransform.applyInverse(position, position);
|
||||
point[0] = position.x;
|
||||
point[1] = position.y;
|
||||
return point;
|
||||
}
|
||||
|
||||
transformToScene(point: FloatPoint): FloatPoint {
|
||||
this.updateTransformationIfNeeded();
|
||||
const position =
|
||||
ShapePainterRuntimeObjectPixiRenderer._positionForTransformation;
|
||||
position.x = point[0];
|
||||
position.y = point[1];
|
||||
this._graphics.localTransform.apply(position, position);
|
||||
point[0] = position.x;
|
||||
point[1] = position.y;
|
||||
return point;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -37,15 +37,25 @@ namespace gdjs {
|
||||
* The ShapePainterRuntimeObject allows to draw graphics shapes on screen.
|
||||
*/
|
||||
export class ShapePainterRuntimeObject extends gdjs.RuntimeObject {
|
||||
_scaleX: number = 1;
|
||||
_scaleY: number = 1;
|
||||
_blendMode: number = 0;
|
||||
_flippedX: boolean = false;
|
||||
_flippedY: boolean = false;
|
||||
_customCenter: FloatPoint | null = null;
|
||||
_customCollisionMask: Polygon[] | null = null;
|
||||
|
||||
_fillColor: integer;
|
||||
_outlineColor: integer;
|
||||
_fillOpacity: float;
|
||||
_outlineOpacity: float;
|
||||
_outlineSize: float;
|
||||
_absoluteCoordinates: boolean;
|
||||
_useAbsoluteCoordinates: boolean;
|
||||
_clearBetweenFrames: boolean;
|
||||
_renderer: gdjs.ShapePainterRuntimeObjectRenderer;
|
||||
|
||||
private static readonly _pointForTransformation: FloatPoint = [0, 0];
|
||||
|
||||
/**
|
||||
* @param runtimeScene The scene the object belongs to.
|
||||
* @param shapePainterObjectData The initial properties of the object
|
||||
@@ -74,7 +84,7 @@ namespace gdjs {
|
||||
this._fillOpacity = shapePainterObjectData.fillOpacity;
|
||||
this._outlineOpacity = shapePainterObjectData.outlineOpacity;
|
||||
this._outlineSize = shapePainterObjectData.outlineSize;
|
||||
this._absoluteCoordinates = shapePainterObjectData.absoluteCoordinates;
|
||||
this._useAbsoluteCoordinates = shapePainterObjectData.absoluteCoordinates;
|
||||
this._clearBetweenFrames = shapePainterObjectData.clearBetweenFrames;
|
||||
this._renderer = new gdjs.ShapePainterRuntimeObjectRenderer(
|
||||
this,
|
||||
@@ -133,9 +143,12 @@ namespace gdjs {
|
||||
if (
|
||||
oldObjectData.absoluteCoordinates !== newObjectData.absoluteCoordinates
|
||||
) {
|
||||
this._absoluteCoordinates = newObjectData.absoluteCoordinates;
|
||||
this._renderer.updateXPosition();
|
||||
this._renderer.updateYPosition();
|
||||
this._useAbsoluteCoordinates = newObjectData.absoluteCoordinates;
|
||||
this._renderer.updatePositionX();
|
||||
this._renderer.updatePositionY();
|
||||
this._renderer.updateAngle();
|
||||
this._renderer.updateScaleX();
|
||||
this._renderer.updateScaleY();
|
||||
}
|
||||
if (
|
||||
oldObjectData.clearBetweenFrames !== newObjectData.clearBetweenFrames
|
||||
@@ -161,7 +174,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
getVisibilityAABB() {
|
||||
return this._absoluteCoordinates ? null : this.getAABB();
|
||||
return this._useAbsoluteCoordinates ? null : this.getAABB();
|
||||
}
|
||||
|
||||
drawRectangle(x1: float, y1: float, x2: float, y2: float) {
|
||||
@@ -325,11 +338,11 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
setCoordinatesRelative(value: boolean): void {
|
||||
this._absoluteCoordinates = !value;
|
||||
this._useAbsoluteCoordinates = !value;
|
||||
}
|
||||
|
||||
areCoordinatesRelative(): boolean {
|
||||
return !this._absoluteCoordinates;
|
||||
return !this._useAbsoluteCoordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -438,7 +451,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
super.setX(x);
|
||||
this._renderer.updateXPosition();
|
||||
this._renderer.updatePositionX();
|
||||
}
|
||||
|
||||
setY(y: float): void {
|
||||
@@ -446,15 +459,337 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
super.setY(y);
|
||||
this._renderer.updateYPosition();
|
||||
this._renderer.updatePositionY();
|
||||
}
|
||||
|
||||
setAngle(angle: float): void {
|
||||
if (angle === this.angle) {
|
||||
return;
|
||||
}
|
||||
super.setAngle(angle);
|
||||
this._renderer.updateAngle();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The center of rotation is defined relatively
|
||||
* to the drawing origin (the object position).
|
||||
* This avoid the center to move on the drawing
|
||||
* when new shapes push the bounds.
|
||||
*
|
||||
* When no custom center is defined, it will move
|
||||
* to stay at the center of the drawable bounds.
|
||||
*
|
||||
* @param x coordinate of the custom center
|
||||
* @param y coordinate of the custom center
|
||||
*/
|
||||
setRotationCenter(x: float, y: float): void {
|
||||
if (!this._customCenter) {
|
||||
this._customCenter = [0, 0];
|
||||
}
|
||||
this._customCenter[0] = x;
|
||||
this._customCenter[1] = y;
|
||||
this._renderer.updateRotationCenter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The center X relatively to the drawing origin
|
||||
* (whereas `getCenterX()` is relative to the top left drawable bound and scaled).
|
||||
*/
|
||||
getRotationCenterX(): float {
|
||||
return this._customCenter
|
||||
? this._customCenter[0]
|
||||
: this._renderer.getUnscaledWidth() / 2 -
|
||||
this._renderer.getFrameRelativeOriginX();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The center Y relatively to the drawing origin
|
||||
* (whereas `getCenterY()` is relative to the top left drawable bound and scaled).
|
||||
*/
|
||||
getRotationCenterY(): float {
|
||||
return this._customCenter
|
||||
? this._customCenter[1]
|
||||
: this._renderer.getUnscaledHeight() / 2 -
|
||||
this._renderer.getFrameRelativeOriginY();
|
||||
}
|
||||
|
||||
getCenterX(): float {
|
||||
if (!this._customCenter) {
|
||||
return super.getCenterX();
|
||||
}
|
||||
return (
|
||||
this._customCenter[0] * Math.abs(this._scaleX) +
|
||||
this.getX() -
|
||||
this.getDrawableX()
|
||||
);
|
||||
}
|
||||
|
||||
getCenterY(): float {
|
||||
if (!this._customCenter) {
|
||||
return super.getCenterY();
|
||||
}
|
||||
return (
|
||||
this._customCenter[1] * Math.abs(this._scaleY) +
|
||||
this.getY() -
|
||||
this.getDrawableY()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the width of the object. This changes the scale on X axis of the object.
|
||||
*
|
||||
* @param newWidth The new width of the object, in pixels.
|
||||
*/
|
||||
setWidth(newWidth: float): void {
|
||||
const unscaledWidth = this._renderer.getUnscaledWidth();
|
||||
if (unscaledWidth !== 0) {
|
||||
this.setScaleX(newWidth / unscaledWidth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the height of the object. This changes the scale on Y axis of the object.
|
||||
*
|
||||
* @param newHeight The new height of the object, in pixels.
|
||||
*/
|
||||
setHeight(newHeight: float): void {
|
||||
const unscaledHeight = this._renderer.getUnscaledHeight();
|
||||
if (unscaledHeight !== 0) {
|
||||
this.setScaleY(newHeight / unscaledHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X and Y axis of the object.
|
||||
*
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScale(newScale: float): void {
|
||||
this.setScaleX(newScale);
|
||||
this.setScaleY(newScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X axis of the object (changing its width).
|
||||
*
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleX(newScale: float): void {
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
if (newScale === Math.abs(this._scaleX)) {
|
||||
return;
|
||||
}
|
||||
this._scaleX = newScale * (this._flippedX ? -1 : 1);
|
||||
this._renderer.updateScaleX();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on Y axis of the object (changing its width).
|
||||
*
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleY(newScale: float): void {
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
if (newScale === Math.abs(this._scaleY)) {
|
||||
return;
|
||||
}
|
||||
this._scaleY = newScale * (this._flippedY ? -1 : 1);
|
||||
this._renderer.updateScaleY();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
flipX(enable: boolean): void {
|
||||
if (enable !== this._flippedX) {
|
||||
this._scaleX *= -1;
|
||||
this._flippedX = enable;
|
||||
this._renderer.updateScaleX();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
flipY(enable: boolean): void {
|
||||
if (enable !== this._flippedY) {
|
||||
this._scaleY *= -1;
|
||||
this._flippedY = enable;
|
||||
this._renderer.updateScaleY();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
isFlippedX(): boolean {
|
||||
return this._flippedX;
|
||||
}
|
||||
|
||||
isFlippedY(): boolean {
|
||||
return this._flippedY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*
|
||||
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*/
|
||||
getScale(): number {
|
||||
const scaleX = Math.abs(this._scaleX);
|
||||
const scaleY = Math.abs(this._scaleY);
|
||||
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object on Y axis.
|
||||
*
|
||||
* @return the scale of the object on Y axis
|
||||
*/
|
||||
getScaleY(): float {
|
||||
return Math.abs(this._scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object on X axis.
|
||||
*
|
||||
* @return the scale of the object on X axis
|
||||
*/
|
||||
getScaleX(): float {
|
||||
return Math.abs(this._scaleX);
|
||||
}
|
||||
|
||||
invalidateBounds() {
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
getDrawableX(): float {
|
||||
return this._renderer.getDrawableX();
|
||||
}
|
||||
|
||||
getDrawableY(): float {
|
||||
return this._renderer.getDrawableY();
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return 32;
|
||||
return this._renderer.getWidth();
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return 32;
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
updatePreRender(runtimeScene: gdjs.RuntimeScene): void {
|
||||
this._renderer.updatePreRender();
|
||||
}
|
||||
|
||||
transformToDrawing(x: float, y: float) {
|
||||
const point = ShapePainterRuntimeObject._pointForTransformation;
|
||||
point[0] = x;
|
||||
point[1] = y;
|
||||
return this._renderer.transformToDrawing(point);
|
||||
}
|
||||
|
||||
transformToScene(x: float, y: float) {
|
||||
const point = ShapePainterRuntimeObject._pointForTransformation;
|
||||
point[0] = x;
|
||||
point[1] = y;
|
||||
return this._renderer.transformToScene(point);
|
||||
}
|
||||
|
||||
transformToDrawingX(x: float, y: float) {
|
||||
return this.transformToDrawing(x, y)[0];
|
||||
}
|
||||
|
||||
transformToDrawingY(x: float, y: float) {
|
||||
return this.transformToDrawing(x, y)[1];
|
||||
}
|
||||
|
||||
transformToSceneX(x: float, y: float) {
|
||||
return this.transformToScene(x, y)[0];
|
||||
}
|
||||
|
||||
transformToSceneY(x: float, y: float) {
|
||||
return this.transformToScene(x, y)[1];
|
||||
}
|
||||
|
||||
setRectangularCollisionMask(
|
||||
left: float,
|
||||
top: float,
|
||||
right: float,
|
||||
bottom: float
|
||||
) {
|
||||
if (!this._customCollisionMask) {
|
||||
const rectangle = new gdjs.Polygon();
|
||||
rectangle.vertices.push([0, 0]);
|
||||
rectangle.vertices.push([0, 0]);
|
||||
rectangle.vertices.push([0, 0]);
|
||||
rectangle.vertices.push([0, 0]);
|
||||
this._customCollisionMask = [rectangle];
|
||||
}
|
||||
const rectangle = this._customCollisionMask[0].vertices;
|
||||
|
||||
rectangle[0][0] = left;
|
||||
rectangle[0][1] = top;
|
||||
|
||||
rectangle[1][0] = right;
|
||||
rectangle[1][1] = top;
|
||||
|
||||
rectangle[2][0] = right;
|
||||
rectangle[2][1] = bottom;
|
||||
|
||||
rectangle[3][0] = left;
|
||||
rectangle[3][1] = bottom;
|
||||
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
updateHitBoxes(): void {
|
||||
this.hitBoxes = this._defaultHitBoxes;
|
||||
const width = this.getWidth();
|
||||
const height = this.getHeight();
|
||||
const centerX = this.getCenterX();
|
||||
const centerY = this.getCenterY();
|
||||
const vertices = this.hitBoxes[0].vertices;
|
||||
if (this._customCollisionMask) {
|
||||
const customCollisionMaskVertices = this._customCollisionMask[0]
|
||||
.vertices;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const point = this.transformToScene(
|
||||
customCollisionMaskVertices[i][0],
|
||||
customCollisionMaskVertices[i][1]
|
||||
);
|
||||
vertices[i][0] = point[0];
|
||||
vertices[i][1] = point[1];
|
||||
}
|
||||
} else {
|
||||
if (centerX === width / 2 && centerY === height / 2) {
|
||||
vertices[0][0] = -centerX;
|
||||
vertices[0][1] = -centerY;
|
||||
vertices[1][0] = +centerX;
|
||||
vertices[1][1] = -centerY;
|
||||
vertices[2][0] = +centerX;
|
||||
vertices[2][1] = +centerY;
|
||||
vertices[3][0] = -centerX;
|
||||
vertices[3][1] = +centerY;
|
||||
} else {
|
||||
vertices[0][0] = 0 - centerX;
|
||||
vertices[0][1] = 0 - centerY;
|
||||
vertices[1][0] = width - centerX;
|
||||
vertices[1][1] = 0 - centerY;
|
||||
vertices[2][0] = width - centerX;
|
||||
vertices[2][1] = height - centerY;
|
||||
vertices[3][0] = 0 - centerX;
|
||||
vertices[3][1] = height - centerY;
|
||||
}
|
||||
if (!this._useAbsoluteCoordinates) {
|
||||
this.hitBoxes[0].rotate(gdjs.toRad(this.getAngle()));
|
||||
}
|
||||
this.hitBoxes[0].move(
|
||||
this.getDrawableX() + centerX,
|
||||
this.getDrawableY() + centerY
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
gdjs.registerObject(
|
||||
|
@@ -0,0 +1,205 @@
|
||||
// @ts-check
|
||||
|
||||
describe('gdjs.ShapePainterRuntimeObject (using a PIXI RuntimeGame with assets)', function () {
|
||||
/**
|
||||
* @param {gdjs.RuntimeScene} runtimeScene
|
||||
*/
|
||||
const makeSpriteRuntimeObjectWithCustomHitBox = (runtimeScene) =>
|
||||
new gdjs.ShapePainterRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: 'PrimitiveDrawing::Drawer',
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
fillColor: { r: 0, g: 0, b: 0 },
|
||||
outlineColor: { r: 0, g: 0, b: 0 },
|
||||
fillOpacity: 255,
|
||||
outlineOpacity: 255,
|
||||
outlineSize: 1,
|
||||
absoluteCoordinates: false,
|
||||
clearBetweenFrames: false,
|
||||
});
|
||||
|
||||
/** @param {gdjs.RuntimeScene} runtimeScene */
|
||||
const loadScene = (runtimeScene) => {
|
||||
runtimeScene.loadFromScene({
|
||||
layers: [
|
||||
{
|
||||
name: '',
|
||||
visibility: true,
|
||||
effects: [],
|
||||
cameras: [],
|
||||
ambientLightColorR: 0,
|
||||
ambientLightColorG: 0,
|
||||
ambientLightColorB: 0,
|
||||
isLightingLayer: false,
|
||||
followBaseLayerCamera: true,
|
||||
},
|
||||
],
|
||||
r: 0,
|
||||
v: 0,
|
||||
b: 0,
|
||||
mangledName: 'Scene1',
|
||||
name: 'Scene1',
|
||||
stopSoundsOnStartup: false,
|
||||
title: '',
|
||||
behaviorsSharedData: [],
|
||||
objects: [],
|
||||
instances: [],
|
||||
variables: [],
|
||||
});
|
||||
};
|
||||
|
||||
it('properly computes bounds of the object (basics)', async () => {
|
||||
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
loadScene(runtimeScene);
|
||||
|
||||
const object = makeSpriteRuntimeObjectWithCustomHitBox(runtimeScene);
|
||||
|
||||
expect(object.getWidth()).to.be(0);
|
||||
expect(object.getHeight()).to.be(0);
|
||||
|
||||
object.drawLineV2(10, 10, 20, 30, 3);
|
||||
|
||||
// Check the automatically computed bounds:
|
||||
expect(object.getDrawableX()).to.be(8.5);
|
||||
expect(object.getDrawableY()).to.be(8.5);
|
||||
expect(object.getWidth()).to.be(13);
|
||||
expect(object.getHeight()).to.be(23);
|
||||
|
||||
// Check the automatic center positioning:
|
||||
expect(object.getCenterXInScene()).to.be(15);
|
||||
expect(object.getCenterYInScene()).to.be(20);
|
||||
expect(object.getCenterX()).to.be(15 - 8.5);
|
||||
expect(object.getCenterY()).to.be(20 - 8.5);
|
||||
|
||||
// Check hit boxes:
|
||||
expect(object.getAABB()).to.eql({
|
||||
min: [8.5, 8.5],
|
||||
max: [8.5 + 13, 8.5 + 23],
|
||||
});
|
||||
|
||||
// Check after scaling (scaling is done from the origin):
|
||||
object.setScale(2);
|
||||
expect(object.getDrawableX()).to.be(17);
|
||||
expect(object.getDrawableY()).to.be(17);
|
||||
expect(object.getWidth()).to.be(13 * 2);
|
||||
expect(object.getHeight()).to.be(23 * 2);
|
||||
expect(object.getAABB()).to.eql({ min: [17, 17], max: [43, 63] });
|
||||
|
||||
// Check after rotating (rotating is done from the center):
|
||||
object.setAngle(45);
|
||||
expect(object.getDrawableX()).to.be(17); // Drawable X/Y is not impacted...
|
||||
expect(object.getDrawableY()).to.be(17);
|
||||
expect(object.getWidth()).to.be(13 * 2); // ...Neither is the size
|
||||
expect(object.getHeight()).to.be(23 * 2);
|
||||
expect(object.getAABB()).to.eql({
|
||||
// The hit boxes/AABB are rotated:
|
||||
min: [4.54415587728429, 14.54415587728429],
|
||||
max: [55.45584412271571, 65.45584412271572],
|
||||
});
|
||||
});
|
||||
|
||||
it('properly computes bounds of the object (custom center)', async () => {
|
||||
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
loadScene(runtimeScene);
|
||||
|
||||
const object = makeSpriteRuntimeObjectWithCustomHitBox(runtimeScene);
|
||||
|
||||
expect(object.getWidth()).to.be(0);
|
||||
expect(object.getHeight()).to.be(0);
|
||||
|
||||
object.drawLineV2(10, 10, 20, 30, 3);
|
||||
object.setRotationCenter(10, 9);
|
||||
|
||||
// Check the automatically computed bounds (not impacted by the center):
|
||||
expect(object.getDrawableX()).to.be(8.5);
|
||||
expect(object.getDrawableY()).to.be(8.5);
|
||||
expect(object.getWidth()).to.be(13);
|
||||
expect(object.getHeight()).to.be(23);
|
||||
|
||||
// Check the center positioning:
|
||||
expect(object.getCenterXInScene()).to.be(10);
|
||||
expect(object.getCenterYInScene()).to.be(9);
|
||||
expect(object.getCenterX()).to.be(10 - 8.5);
|
||||
expect(object.getCenterY()).to.be(9 - 8.5);
|
||||
|
||||
// Check hit boxes (not impacted by the center, as no rotation is made):
|
||||
expect(object.getAABB()).to.eql({
|
||||
min: [8.5, 8.5],
|
||||
max: [8.5 + 13, 8.5 + 23],
|
||||
});
|
||||
|
||||
// Check after scaling (scaling is done from the origin):
|
||||
object.setScale(2);
|
||||
expect(object.getDrawableX()).to.be(17);
|
||||
expect(object.getDrawableY()).to.be(17);
|
||||
expect(object.getWidth()).to.be(13 * 2);
|
||||
expect(object.getHeight()).to.be(23 * 2);
|
||||
expect(object.getAABB()).to.eql({ min: [17, 17], max: [43, 63] });
|
||||
|
||||
// Check after rotating (rotating is done from the center):
|
||||
object.setAngle(45);
|
||||
expect(object.getAABB()).to.eql({
|
||||
// The hit boxes/AABB are rotated:
|
||||
min: [-13.941125496954278, 15.17157287525381],
|
||||
max: [36.970562748477136, 66.08326112068524],
|
||||
});
|
||||
|
||||
// Draw outside of the current bounds.
|
||||
const oldMinX = object.getAABB().min[0];
|
||||
const oldMinY = object.getAABB().min[1];
|
||||
const oldMaxX = object.getAABB().max[0];
|
||||
const oldMaxY = object.getAABB().max[1];
|
||||
const oldCenterX = object.getCenterXInScene();
|
||||
const oldCenterY = object.getCenterYInScene();
|
||||
object.drawLineV2(-10, -10, 21, 31, 3);
|
||||
|
||||
// Check that the center stays the same.
|
||||
expect(object.getCenterXInScene()).to.be(oldCenterX);
|
||||
expect(object.getCenterYInScene()).to.be(oldCenterY);
|
||||
|
||||
// Check that the AABB expands.
|
||||
const newAABB = object.getAABB();
|
||||
expect(newAABB.min[0]).below(oldMinX);
|
||||
expect(newAABB.min[1]).below(oldMinY);
|
||||
expect(newAABB.max[0]).above(oldMaxX);
|
||||
expect(newAABB.max[1]).above(oldMaxY);
|
||||
});
|
||||
|
||||
it('can transform points', async () => {
|
||||
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
loadScene(runtimeScene);
|
||||
|
||||
const object = makeSpriteRuntimeObjectWithCustomHitBox(runtimeScene);
|
||||
|
||||
object.drawLineV2(0, 0, 10, 10, 2);
|
||||
expect(object.getWidth()).to.be(12);
|
||||
expect(object.getHeight()).to.be(12);
|
||||
|
||||
// Check changes in position/scale are taken into account:
|
||||
object.setPosition(50, 100);
|
||||
expect(object.transformToScene(10, 20)).to.eql([60, 120]);
|
||||
|
||||
object.setScale(2);
|
||||
expect(object.transformToScene(10, 20)).to.eql([70, 140]);
|
||||
|
||||
// Check rotation with the default center:
|
||||
expect(object.getCenterXInScene()).to.be(60);
|
||||
expect(object.getCenterYInScene()).to.be(110);
|
||||
expect(object.transformToScene(5, 5)).to.eql([60, 110]);
|
||||
expect(object.transformToScene(10, 20)).to.eql([70, 140]);
|
||||
|
||||
object.setAngle(90);
|
||||
expect(object.transformToScene(5, 5)).to.eql([60, 110]);
|
||||
expect(object.transformToScene(10, 20)).to.eql([30, 120]);
|
||||
|
||||
// Check rotation with a custom center:
|
||||
object.setRotationCenter(20, 9);
|
||||
expect(object.transformToScene(10, 20)).to.eql([68, 98]);
|
||||
expect(object.transformToDrawing(68, 98)).to.eql([10, 20]);
|
||||
});
|
||||
});
|
@@ -1,17 +1,21 @@
|
||||
namespace gdjs {
|
||||
export namespace evtTools {
|
||||
export namespace spatialSound {
|
||||
const logger = new gdjs.Logger('Spatial Sound');
|
||||
export const setSoundPosition = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
x: float,
|
||||
y: float,
|
||||
z: float
|
||||
) =>
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.getSoundOnChannel(channel)
|
||||
.setSpatialPosition(x, y, z);
|
||||
) => {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
if (sound) sound.setSpatialPosition(x, y, z);
|
||||
else
|
||||
logger.error(
|
||||
`Cannot set the spatial position of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -45,23 +45,24 @@ TopDownMovementBehavior::GetProperties(
|
||||
const gd::SerializerElement& behaviorContent) const {
|
||||
std::map<gd::String, gd::PropertyDescriptor> properties;
|
||||
|
||||
properties[_("Allows diagonals")]
|
||||
properties[_("Allows diagonals")].SetGroup(_("Movement"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("allowDiagonals") ? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Acceleration")].SetValue(
|
||||
properties[_("Acceleration")].SetGroup(_("Movement")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("acceleration")));
|
||||
properties[_("Deceleration")].SetValue(
|
||||
properties[_("Deceleration")].SetGroup(_("Movement")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("deceleration")));
|
||||
properties[_("Max. speed")].SetValue(
|
||||
properties[_("Max. speed")].SetGroup(_("Movement")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxSpeed")));
|
||||
properties[_("Rotate speed")].SetValue(
|
||||
properties[_("Rotate speed")].SetGroup(_("Rotation")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angularMaxSpeed")));
|
||||
properties[_("Rotate object")]
|
||||
.SetGroup(_("Rotation"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("rotateObject") ? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Angle offset")].SetValue(
|
||||
properties[_("Angle offset")].SetGroup(_("Rotation")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angleOffset")));
|
||||
properties[_("Default controls")]
|
||||
.SetValue(behaviorContent.GetBoolAttribute("ignoreDefaultControls")
|
||||
@@ -80,6 +81,7 @@ TopDownMovementBehavior::GetProperties(
|
||||
else if (viewpoint == "CustomIsometry")
|
||||
viewpointStr = _("Custom Isometry");
|
||||
properties[_("Viewpoint")]
|
||||
.SetGroup(_("Viewpoint"))
|
||||
.SetValue(viewpointStr)
|
||||
.SetType("Choice")
|
||||
.AddExtraInfo(_("Top-Down"))
|
||||
@@ -87,11 +89,13 @@ TopDownMovementBehavior::GetProperties(
|
||||
.AddExtraInfo(_("True Isometry (30°)"))
|
||||
.AddExtraInfo(_("Custom Isometry"));
|
||||
properties[_("Custom isometry angle")]
|
||||
.SetGroup(_("Viewpoint"))
|
||||
.SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("customIsometryAngle")))
|
||||
.SetDescription(_("If you choose \"Custom Isometry\", this allows to "
|
||||
"specify the angle of your isometry projection."));
|
||||
properties[_("Movement angle offset")]
|
||||
.SetGroup(_("Viewpoint"))
|
||||
.SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("movementAngleOffset")))
|
||||
.SetDescription(_(
|
||||
|
@@ -1,8 +1,9 @@
|
||||
// @ts-check
|
||||
describe('gdjs.TopDownMovementRuntimeBehavior', function () {
|
||||
const epsilon = 1 / (2 << 8);
|
||||
const topDownName = 'auto1';
|
||||
|
||||
const createScene = () => {
|
||||
const createScene = (timeDelta = 1000 / 60) => {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
// @ts-ignore - missing properties.
|
||||
@@ -38,7 +39,7 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
|
||||
instances: [],
|
||||
});
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return (1 / 60) * 1000;
|
||||
return timeDelta;
|
||||
};
|
||||
return runtimeScene;
|
||||
};
|
||||
@@ -427,4 +428,28 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
[20, 30, 60, 120].forEach((framesPerSecond) => {
|
||||
describe(`(frames per second: ${framesPerSecond})`, function () {
|
||||
let runtimeScene;
|
||||
let player;
|
||||
beforeEach(function () {
|
||||
runtimeScene = createScene(1000 / framesPerSecond);
|
||||
player = addPlayer(runtimeScene, true);
|
||||
});
|
||||
|
||||
it('moves the same distance', function () {
|
||||
player.setPosition(200, 300);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
// It takes 0,5 second to reach the maximum speed because
|
||||
// the acceleration is 400 and maxSpeed is 200.
|
||||
for (let i = 0; i < framesPerSecond / 2; i++) {
|
||||
player.getBehavior(topDownName).simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
}
|
||||
expect(player.getX()).to.be.within(250 - epsilon, 250 + epsilon);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -324,7 +324,10 @@ namespace gdjs {
|
||||
const timeDelta = this.owner.getElapsedTime(runtimeScene) / 1000;
|
||||
let directionInRad = 0;
|
||||
let directionInDeg = 0;
|
||||
//Update the speed of the object
|
||||
const previousVelocityX = this._xVelocity;
|
||||
const previousVelocityY = this._yVelocity;
|
||||
|
||||
// Update the speed of the object:
|
||||
if (direction !== -1) {
|
||||
directionInRad =
|
||||
((direction + this._movementAngleOffset / 45) * Math.PI) / 4.0;
|
||||
@@ -344,52 +347,57 @@ namespace gdjs {
|
||||
this._yVelocity += norm * Math.sin(directionInRad);
|
||||
|
||||
this._stickForce = 0;
|
||||
} else {
|
||||
} else if (this._yVelocity !== 0 || this._xVelocity !== 0) {
|
||||
directionInRad = Math.atan2(this._yVelocity, this._xVelocity);
|
||||
directionInDeg =
|
||||
(Math.atan2(this._yVelocity, this._xVelocity) * 180.0) / Math.PI;
|
||||
directionInDeg = (directionInRad * 180.0) / Math.PI;
|
||||
const xVelocityWasPositive = this._xVelocity >= 0;
|
||||
const yVelocityWasPositive = this._yVelocity >= 0;
|
||||
this._xVelocity -=
|
||||
this._deceleration * timeDelta * Math.cos(directionInRad);
|
||||
this._yVelocity -=
|
||||
this._deceleration * timeDelta * Math.sin(directionInRad);
|
||||
// @ts-ignore
|
||||
if ((this._xVelocity > 0) ^ xVelocityWasPositive) {
|
||||
if (this._xVelocity > 0 !== xVelocityWasPositive) {
|
||||
this._xVelocity = 0;
|
||||
}
|
||||
// @ts-ignore
|
||||
if ((this._yVelocity > 0) ^ yVelocityWasPositive) {
|
||||
if (this._yVelocity > 0 !== yVelocityWasPositive) {
|
||||
this._yVelocity = 0;
|
||||
}
|
||||
}
|
||||
const speed = Math.sqrt(
|
||||
this._xVelocity * this._xVelocity + this._yVelocity * this._yVelocity
|
||||
);
|
||||
if (speed > this._maxSpeed) {
|
||||
const squaredSpeed =
|
||||
this._xVelocity * this._xVelocity + this._yVelocity * this._yVelocity;
|
||||
if (squaredSpeed > this._maxSpeed * this._maxSpeed) {
|
||||
this._xVelocity = this._maxSpeed * Math.cos(directionInRad);
|
||||
this._yVelocity = this._maxSpeed * Math.sin(directionInRad);
|
||||
}
|
||||
|
||||
// No acceleration for angular speed for now.
|
||||
this._angularSpeed = this._angularMaxSpeed;
|
||||
|
||||
//No acceleration for angular speed for now
|
||||
|
||||
//Position object
|
||||
// Position object.
|
||||
// This is a Verlet integration considering the acceleration as constant.
|
||||
// If you expand deltaX or deltaY, it gives, thanks to the usage of both
|
||||
// the old and the new velocity:
|
||||
// "velocity * timeDelta + acceleration * timeDelta^2 / 2".
|
||||
//
|
||||
// The acceleration is not actually always constant, particularly with a gamepad,
|
||||
// but the error is multiplied by timDelta^3. So, it shouldn't matter much.
|
||||
const deltaX = ((previousVelocityX + this._xVelocity) / 2) * timeDelta;
|
||||
const deltaY = ((previousVelocityY + this._yVelocity) / 2) * timeDelta;
|
||||
if (this._basisTransformation === null) {
|
||||
// Top-down viewpoint
|
||||
object.setX(object.getX() + this._xVelocity * timeDelta);
|
||||
object.setY(object.getY() + this._yVelocity * timeDelta);
|
||||
object.setX(object.getX() + deltaX);
|
||||
object.setY(object.getY() + deltaY);
|
||||
} else {
|
||||
// Isometry viewpoint
|
||||
const point = this._temporaryPointForTransformations;
|
||||
point[0] = this._xVelocity * timeDelta;
|
||||
point[1] = this._yVelocity * timeDelta;
|
||||
point[0] = deltaX;
|
||||
point[1] = deltaY;
|
||||
this._basisTransformation.toScreen(point, point);
|
||||
object.setX(object.getX() + point[0]);
|
||||
object.setY(object.getY() + point[1]);
|
||||
}
|
||||
|
||||
//Also update angle if needed
|
||||
// Also update angle if needed.
|
||||
if (this._xVelocity !== 0 || this._yVelocity !== 0) {
|
||||
this._angle = directionInDeg;
|
||||
if (this._rotateObject) {
|
||||
|
@@ -278,7 +278,7 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
|
||||
outputCode +=
|
||||
codeGenerator.GenerateBooleanFullName(
|
||||
"condition" + gd::String::From(i) + "IsTrue", context) +
|
||||
".val = true;\n";
|
||||
".val = false;\n";
|
||||
}
|
||||
|
||||
for (unsigned int cId = 0; cId < conditions.size(); ++cId) {
|
||||
|
@@ -68,18 +68,18 @@ namespace gdjs {
|
||||
RAlt: 2018,
|
||||
LSystem: 1091,
|
||||
RSystem: 2091,
|
||||
/*"Menu": sf::Keyboard::Menu ,
|
||||
"LBracket": sf::Keyboard::LBracket ,
|
||||
"RBracket": sf::Keyboard::RBracket ,
|
||||
"SemiColon": sf::Keyboard::SemiColon ,
|
||||
"Comma": sf::Keyboard::Comma ,
|
||||
"Period": sf::Keyboard::Period ,
|
||||
"Quote": sf::Keyboard::Quote ,
|
||||
"Slash": sf::Keyboard::Slash ,
|
||||
"BackSlash": sf::Keyboard::BackSlash ,
|
||||
"Tilde": sf::Keyboard::Tilde ,
|
||||
"Equal": sf::Keyboard::Equal ,
|
||||
"Dash": sf::Keyboard::Dash,*/
|
||||
SemiColon: 186,
|
||||
Comma: 188,
|
||||
Period: 190,
|
||||
Quote: 222,
|
||||
Slash: 191,
|
||||
BackSlash: 220,
|
||||
Equal: 187,
|
||||
Dash: 189,
|
||||
Menu: 93,
|
||||
LBracket: 219,
|
||||
RBracket: 221,
|
||||
Tilde: 192,
|
||||
Space: 32,
|
||||
Back: 8,
|
||||
Tab: 9,
|
||||
|
@@ -6,20 +6,24 @@
|
||||
namespace gdjs {
|
||||
export namespace evtTools {
|
||||
export namespace sound {
|
||||
const logger = new gdjs.Logger('Audio events');
|
||||
|
||||
export const getGlobalVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
): float {
|
||||
return runtimeScene.getSoundManager().getGlobalVolume();
|
||||
};
|
||||
|
||||
export const setGlobalVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
globalVolume: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene.getSoundManager().setGlobalVolume(globalVolume);
|
||||
};
|
||||
|
||||
export const unloadAllAudio = function (runtimeScene: gdjs.RuntimeScene) {
|
||||
export const unloadAllAudio = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): void {
|
||||
runtimeScene.getSoundManager().unloadAll();
|
||||
};
|
||||
|
||||
@@ -30,7 +34,7 @@ namespace gdjs {
|
||||
loop: boolean,
|
||||
volume: float,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.playSound(soundFile, loop, volume, pitch);
|
||||
@@ -43,7 +47,7 @@ namespace gdjs {
|
||||
loop: boolean,
|
||||
volume: float,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.playSoundOnChannel(soundFile, channel, loop, volume, pitch);
|
||||
@@ -52,33 +56,45 @@ namespace gdjs {
|
||||
export const stopSoundOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.stop();
|
||||
if (sound) sound.stop();
|
||||
else {
|
||||
logger.error(`Cannot stop non-existing sound on channel ${channel}.`);
|
||||
}
|
||||
};
|
||||
|
||||
export const pauseSoundOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.pause();
|
||||
if (sound) sound.pause();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot pause non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const continueSoundOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
if (sound && !sound.playing()) {
|
||||
sound.play();
|
||||
if (sound) {
|
||||
if (!sound.playing()) sound.play();
|
||||
} else {
|
||||
logger.error(
|
||||
`Cannot continue playing non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const isSoundOnChannelPlaying = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.playing() : false;
|
||||
};
|
||||
@@ -86,68 +102,113 @@ namespace gdjs {
|
||||
export const isSoundOnChannelPaused = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.paused() : false;
|
||||
if (sound) return sound.paused();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing sound on channel ${channel} is paused.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isSoundOnChannelStopped = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.stopped() : true;
|
||||
if (sound) return sound.stopped();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing sound on channel ${channel} is stopped.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSoundOnChannelVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.getVolume() * 100 : 100;
|
||||
if (sound) return sound.getVolume() * 100;
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the volume of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
|
||||
export const setSoundOnChannelVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
volume: float
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.setVolume(volume / 100);
|
||||
if (sound) sound.setVolume(volume / 100);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the volume of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSoundOnChannelPlayingOffset = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.getSeek() : 0;
|
||||
if (sound) return sound.getSeek();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the playing offset of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const setSoundOnChannelPlayingOffset = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
playingOffset: float
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.setSeek(playingOffset);
|
||||
if (sound) sound.setSeek(playingOffset);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the playing offset of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSoundOnChannelPitch = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.getRate() : 1;
|
||||
if (sound) return sound.getRate();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
export const setSoundOnChannelPitch = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.setRate(pitch);
|
||||
if (sound) sound.setRate(pitch);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const preloadSound = (
|
||||
@@ -173,7 +234,7 @@ namespace gdjs {
|
||||
loop: boolean,
|
||||
volume: float,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.playMusic(soundFile, loop, volume, pitch);
|
||||
@@ -186,7 +247,7 @@ namespace gdjs {
|
||||
loop: boolean,
|
||||
volume: float,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.playMusicOnChannel(soundFile, channel, loop, volume, pitch);
|
||||
@@ -195,33 +256,47 @@ namespace gdjs {
|
||||
export const stopMusicOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.stop();
|
||||
if (music) music.stop();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot stop a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const pauseMusicOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.pause();
|
||||
if (music) music.pause();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot pause a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const continueMusicOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
if (music && !music.playing()) {
|
||||
music.play();
|
||||
if (music) {
|
||||
if (!music.playing()) music.play();
|
||||
} else {
|
||||
logger.error(
|
||||
`Cannot stop a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const isMusicOnChannelPlaying = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.playing() : false;
|
||||
};
|
||||
@@ -229,68 +304,113 @@ namespace gdjs {
|
||||
export const isMusicOnChannelPaused = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.paused() : false;
|
||||
if (music) return music.paused();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing music on channel ${channel} is paused.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isMusicOnChannelStopped = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.stopped() : true;
|
||||
if (music) return music.stopped();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing music on channel ${channel} is stopped.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const getMusicOnChannelVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.getVolume() * 100 : 100;
|
||||
if (music) return music.getVolume() * 100;
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the volume of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
|
||||
export const setMusicOnChannelVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
volume: float
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.setVolume(volume / 100);
|
||||
if (music) music.setVolume(volume / 100);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the volume of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMusicOnChannelPlayingOffset = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.getSeek() : 0;
|
||||
if (music) return music.getSeek();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the playing offset of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const setMusicOnChannelPlayingOffset = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
playingOffset: float
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.setSeek(playingOffset);
|
||||
if (music) music.setSeek(playingOffset);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the playing offset of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMusicOnChannelPitch = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.getRate() : 1;
|
||||
if (music) return music.getRate();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
export const setMusicOnChannelPitch = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.setRate(pitch);
|
||||
if (music) music.setRate(pitch);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const preloadMusic = (
|
||||
|
@@ -654,8 +654,8 @@ namespace gdjs {
|
||||
sound.play();
|
||||
}
|
||||
|
||||
getSoundOnChannel(channel: integer): HowlerSound {
|
||||
return this._sounds[channel];
|
||||
getSoundOnChannel(channel: integer): HowlerSound | null {
|
||||
return this._sounds[channel] || null;
|
||||
}
|
||||
|
||||
playMusic(soundName: string, loop: boolean, volume: float, pitch: float) {
|
||||
@@ -702,8 +702,8 @@ namespace gdjs {
|
||||
music.play();
|
||||
}
|
||||
|
||||
getMusicOnChannel(channel: integer): HowlerSound {
|
||||
return this._musics[channel];
|
||||
getMusicOnChannel(channel: integer): HowlerSound | null {
|
||||
return this._musics[channel] || null;
|
||||
}
|
||||
|
||||
setGlobalVolume(volume: float): void {
|
||||
|
@@ -230,11 +230,9 @@ namespace gdjs {
|
||||
debugDraw.fill.alpha = 0.3;
|
||||
|
||||
// Draw Center point
|
||||
const centerPointX = object.getDrawableX() + object.getCenterX();
|
||||
const centerPointY = object.getDrawableY() + object.getCenterY();
|
||||
const centerPoint = layer.convertInverseCoords(
|
||||
centerPointX,
|
||||
centerPointY
|
||||
object.getCenterXInScene(),
|
||||
object.getCenterYInScene()
|
||||
);
|
||||
|
||||
renderObjectPoint(
|
||||
@@ -245,26 +243,44 @@ namespace gdjs {
|
||||
centerPoint[1]
|
||||
);
|
||||
|
||||
// Draw Origin point
|
||||
let originPoint = [object.getDrawableX(), object.getDrawableY()];
|
||||
if (object instanceof gdjs.SpriteRuntimeObject) {
|
||||
// For Sprite objects get the position of the origin point.
|
||||
originPoint = object.getPointPosition('origin');
|
||||
}
|
||||
|
||||
originPoint = layer.convertInverseCoords(
|
||||
originPoint[0],
|
||||
originPoint[1]
|
||||
// Draw position point
|
||||
const positionPoint = layer.convertInverseCoords(
|
||||
object.getX(),
|
||||
object.getY()
|
||||
);
|
||||
|
||||
renderObjectPoint(
|
||||
renderedObjectPoints.points,
|
||||
'Origin',
|
||||
'Position',
|
||||
0xff0000,
|
||||
originPoint[0],
|
||||
originPoint[1]
|
||||
positionPoint[0],
|
||||
positionPoint[1]
|
||||
);
|
||||
|
||||
// Draw Origin point
|
||||
if (object instanceof gdjs.SpriteRuntimeObject) {
|
||||
let originPoint = object.getPointPosition('origin');
|
||||
// When there is neither rotation nor flipping,
|
||||
// the origin point is over the position point.
|
||||
if (
|
||||
Math.abs(originPoint[0] - positionPoint[0]) >= 1 ||
|
||||
Math.abs(originPoint[1] - positionPoint[1]) >= 1
|
||||
) {
|
||||
originPoint = layer.convertInverseCoords(
|
||||
originPoint[0],
|
||||
originPoint[1]
|
||||
);
|
||||
|
||||
renderObjectPoint(
|
||||
renderedObjectPoints.points,
|
||||
'Origin',
|
||||
0xff0000,
|
||||
originPoint[0],
|
||||
originPoint[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw custom point
|
||||
if (showCustomPoints && object instanceof gdjs.SpriteRuntimeObject) {
|
||||
if (!object._animationFrame) continue;
|
||||
|
@@ -1159,9 +1159,9 @@ namespace gdjs {
|
||||
/**
|
||||
* Change the width of the object. This changes the scale on X axis of the object.
|
||||
*
|
||||
* @param width The new width of the object, in pixels.
|
||||
* @param newWidth The new width of the object, in pixels.
|
||||
*/
|
||||
setWidth(newWidth): void {
|
||||
setWidth(newWidth: float): void {
|
||||
if (this._animationFrameDirty) {
|
||||
this._updateAnimationFrame();
|
||||
}
|
||||
@@ -1174,9 +1174,9 @@ namespace gdjs {
|
||||
/**
|
||||
* Change the height of the object. This changes the scale on Y axis of the object.
|
||||
*
|
||||
* @param height The new height of the object, in pixels.
|
||||
* @param newHeight The new height of the object, in pixels.
|
||||
*/
|
||||
setHeight(newHeight): void {
|
||||
setHeight(newHeight: float): void {
|
||||
if (this._animationFrameDirty) {
|
||||
this._updateAnimationFrame();
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 642 B |
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,8 @@ module.exports = function (config) {
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightobstacleruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/PathfindingBehavior/pathfindingobstacleruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/PathfindingBehavior/pathfindingruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/PrimitiveDrawing/shapepainterruntimeobject.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/PrimitiveDrawing/shapepainterruntimeobject-pixi-renderer.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TopDownMovementBehavior/topdownmovementruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TopDownMovementBehavior/topdownobstacleruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Firebase/A_firebasejs/*.js',
|
||||
|
@@ -743,6 +743,8 @@ interface PropertyDescriptor {
|
||||
[Const, Ref] DOMString GetLabel();
|
||||
[Ref] PropertyDescriptor SetDescription([Const] DOMString label);
|
||||
[Const, Ref] DOMString GetDescription();
|
||||
[Ref] PropertyDescriptor SetGroup([Const] DOMString label);
|
||||
[Const, Ref] DOMString GetGroup();
|
||||
[Ref] PropertyDescriptor AddExtraInfo([Const] DOMString type);
|
||||
[Ref] PropertyDescriptor SetExtraInfo([Const, Ref] VectorString info);
|
||||
[Ref] VectorString GetExtraInfo();
|
||||
@@ -1847,6 +1849,8 @@ interface EventsRemover {
|
||||
|
||||
interface EventsListUnfolder {
|
||||
void STATIC_UnfoldWhenContaining([Ref] EventsList list, [Const, Ref] BaseEvent eventToContain);
|
||||
void STATIC_FoldAll([Ref] EventsList list);
|
||||
void STATIC_UnfoldToLevel([Ref] EventsList list, [Const] unsigned long maxLevel, [Const] optional unsigned long currentLevel = 0);
|
||||
};
|
||||
|
||||
interface EventsSearchResult {
|
||||
@@ -2200,6 +2204,10 @@ interface EventsFunctionsExtension {
|
||||
[Const, Ref] DOMString GetIconUrl();
|
||||
[Ref] EventsFunctionsExtension SetHelpPath([Const] DOMString helpPath);
|
||||
[Const, Ref] DOMString GetHelpPath();
|
||||
void SetOrigin([Const] DOMString originName, [Const] DOMString originIdentifier);
|
||||
[Const, Ref] DOMString GetOriginName();
|
||||
[Const, Ref] DOMString GetOriginIdentifier();
|
||||
|
||||
|
||||
[Ref] DependencyMetadata AddDependency();
|
||||
void RemoveDependencyAt(unsigned long index);
|
||||
|
@@ -571,6 +571,8 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
#define STATIC_FindAllObjectVariables FindAllObjectVariables
|
||||
#define STATIC_SearchInEvents SearchInEvents
|
||||
#define STATIC_UnfoldWhenContaining UnfoldWhenContaining
|
||||
#define STATIC_FoldAll FoldAll
|
||||
#define STATIC_UnfoldToLevel UnfoldToLevel
|
||||
|
||||
#define STATIC_FreeEventsFunctionToObjectsContainer FreeEventsFunctionToObjectsContainer
|
||||
#define STATIC_BehaviorEventsFunctionToObjectsContainer BehaviorEventsFunctionToObjectsContainer
|
||||
|
@@ -23,6 +23,9 @@ declare class gdEventsFunctionsExtension extends gdEventsFunctionsContainer {
|
||||
getIconUrl(): string;
|
||||
setHelpPath(helpPath: string): gdEventsFunctionsExtension;
|
||||
getHelpPath(): string;
|
||||
setOrigin(originName: string, originIdentifier: string): void;
|
||||
getOriginName(): string;
|
||||
getOriginIdentifier(): string;
|
||||
addDependency(): gdDependencyMetadata;
|
||||
removeDependencyAt(index: number): void;
|
||||
getAllDependencies(): gdVectorDependencyMetadata;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdEventsListUnfolder {
|
||||
static unfoldWhenContaining(list: gdEventsList, eventToContain: gdBaseEvent): void;
|
||||
static foldAll(list: gdEventsList): void;
|
||||
static unfoldToLevel(list: gdEventsList, maxLevel: number, currentLevel?: number): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -9,6 +9,8 @@ declare class gdPropertyDescriptor {
|
||||
getLabel(): string;
|
||||
setDescription(label: string): gdPropertyDescriptor;
|
||||
getDescription(): string;
|
||||
setGroup(label: string): gdPropertyDescriptor;
|
||||
getGroup(): string;
|
||||
addExtraInfo(type: string): gdPropertyDescriptor;
|
||||
setExtraInfo(info: gdVectorString): gdPropertyDescriptor;
|
||||
getExtraInfo(): gdVectorString;
|
||||
|
@@ -50,14 +50,15 @@ install:
|
||||
|
||||
cd ..\..
|
||||
|
||||
# Package the app for Windows.
|
||||
# Package the app for Windows (and sign it with the certificate set in environment variables).
|
||||
# Don't sign the appx (it will be signed by the Microsoft Store).
|
||||
build_script:
|
||||
- cmd: >-
|
||||
- ps: >-
|
||||
cd newIDE\electron-app
|
||||
|
||||
node --max-old-space-size=3072 scripts/build.js --win appx --publish=never
|
||||
node --max-old-space-size=3072 scripts/build.js --win nsis --publish=never
|
||||
|
||||
node scripts/build.js --skip-app-build --win nsis --publish=never
|
||||
Remove-Item -Path Env:CSC_LINK ; Remove-Item -Path Env:CSC_KEY_PASSWORD ; node scripts/build.js --skip-app-build --win appx --publish=never
|
||||
|
||||
cd ..\..
|
||||
|
||||
|
9
newIDE/app/package-lock.json
generated
9
newIDE/app/package-lock.json
generated
@@ -36759,7 +36759,8 @@
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "3.1.3",
|
||||
@@ -54888,6 +54889,7 @@
|
||||
},
|
||||
"@lingui/react": {
|
||||
"version": "git+ssh://git@github.com/4ian/lingui-react.git#dc6b1e013470d952cf85f96cc4affdd28e29634a",
|
||||
"integrity": "sha512-eoYJ8TI+8IolPh4fue9aIwX2OVp0YrPnV86QBZLfGhxknodVeNmx+4Ic4ym7rI5/davbk9AUZHcssiH+YZWVxw==",
|
||||
"from": "@lingui/react@git://github.com/4ian/lingui-react.git#master",
|
||||
"requires": {
|
||||
"@lingui/core": "2.7.3",
|
||||
@@ -79049,7 +79051,8 @@
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.1.3",
|
||||
@@ -80327,6 +80330,7 @@
|
||||
},
|
||||
"pixi-simple-gesture": {
|
||||
"version": "git+ssh://git@github.com/4ian/pixi-simple-gesture.git#c84e0cc3c62edeca019e708d9897ef6b97a0d18a",
|
||||
"integrity": "sha512-DG1BxP8SK2iPMYWMOPGz5gKDXFmA8JPUpcyyNyIH55fpQraenuLYlosYFFMTRXEy0RZViTUu11H3VrYlfG2CgA==",
|
||||
"from": "pixi-simple-gesture@git://github.com/4ian/pixi-simple-gesture#v0.3.3"
|
||||
},
|
||||
"pixi.js": {
|
||||
@@ -83045,6 +83049,7 @@
|
||||
},
|
||||
"react-mosaic-component": {
|
||||
"version": "git+ssh://git@github.com/4ian/react-mosaic.git#d5ef155119d786c08c7c72e34997dcef2f01f98b",
|
||||
"integrity": "sha512-Izfw/EkG1g39nrZbOqzY52rqFkVFA1SUSv1TLwk7soS1Wy7iHm6zrUgzJdfwKRC2GaDn9WAfSe5ZQ2vIJ/mu5A==",
|
||||
"from": "react-mosaic-component@git://github.com/4ian/react-mosaic#v3.1.0",
|
||||
"requires": {
|
||||
"classnames": "^2.2.6",
|
||||
|
@@ -118,6 +118,14 @@ export const AssetDetails = ({
|
||||
[loadAsset]
|
||||
);
|
||||
|
||||
const canAddAsset = canInstall && !isBeingInstalled && !!asset;
|
||||
const onAddAsset = React.useCallback(
|
||||
() => {
|
||||
if (canAddAsset) onAdd();
|
||||
},
|
||||
[onAdd, canAddAsset]
|
||||
);
|
||||
|
||||
const assetAuthors: ?Array<Author> =
|
||||
asset && authors
|
||||
? asset.authors
|
||||
@@ -146,11 +154,12 @@ export const AssetDetails = ({
|
||||
primary
|
||||
icon={<Add />}
|
||||
label={<Trans>Add to the game</Trans>}
|
||||
onClick={onAdd}
|
||||
disabled={!canInstall || isBeingInstalled || !asset}
|
||||
onClick={onAddAsset}
|
||||
disabled={!canAddAsset}
|
||||
/>
|
||||
</LeftLoader>,
|
||||
]}
|
||||
onApply={onAddAsset}
|
||||
>
|
||||
<Column expand noMargin>
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
|
@@ -76,6 +76,14 @@ export function ExampleDialog({
|
||||
);
|
||||
const hasIcon = exampleShortHeader.previewImageUrls.length > 0;
|
||||
|
||||
const canOpenExample = !isOpening && isCompatible;
|
||||
const onOpenExample = React.useCallback(
|
||||
() => {
|
||||
if (canOpenExample) onOpen();
|
||||
},
|
||||
[onOpen, canOpenExample]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
actions={[
|
||||
@@ -96,8 +104,8 @@ export function ExampleDialog({
|
||||
)
|
||||
}
|
||||
primary
|
||||
onClick={onOpen}
|
||||
disabled={isOpening || !isCompatible}
|
||||
onClick={onOpenExample}
|
||||
disabled={!canOpenExample}
|
||||
buildMenuTemplate={i18n => [
|
||||
{
|
||||
label: electron
|
||||
@@ -115,6 +123,7 @@ export function ExampleDialog({
|
||||
cannotBeDismissed={false}
|
||||
open
|
||||
onRequestClose={onClose}
|
||||
onApply={onOpenExample}
|
||||
>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{!isCompatible && (
|
||||
|
@@ -1,17 +1,12 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import SearchBar from '../../UI/SearchBar';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import Background from '../../UI/Background';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example';
|
||||
import { FiltersChooser } from '../../UI/Search/FiltersChooser';
|
||||
import { ExampleStoreContext } from './ExampleStoreContext';
|
||||
import { ListSearchResults } from '../../UI/Search/ListSearchResults';
|
||||
import { ExampleListItem } from './ExampleListItem';
|
||||
import { ResponsiveWindowMeasurer } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import Subheader from '../../UI/Subheader';
|
||||
import { ExampleDialog } from './ExampleDialog';
|
||||
|
||||
const styles = {
|
||||
@@ -61,6 +56,12 @@ export const ExampleStore = ({ isOpening, onOpen }: Props) => {
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
style={styles.searchBar}
|
||||
tagsHandler={{
|
||||
add: filtersState.addFilter,
|
||||
remove: filtersState.removeFilter,
|
||||
chosenTags: Array.from(filtersState.chosenFilters),
|
||||
}}
|
||||
tags={filters && filters.defaultTags}
|
||||
/>
|
||||
<Line
|
||||
expand
|
||||
@@ -68,22 +69,6 @@ export const ExampleStore = ({ isOpening, onOpen }: Props) => {
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
<Background
|
||||
noFullHeight
|
||||
noExpand
|
||||
width={windowWidth === 'small' ? 150 : 250}
|
||||
>
|
||||
<ScrollView>
|
||||
<Subheader>
|
||||
<Trans>Filters</Trans>
|
||||
</Subheader>
|
||||
<FiltersChooser
|
||||
allFilters={filters}
|
||||
filtersState={filtersState}
|
||||
error={error}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Background>
|
||||
<ListSearchResults
|
||||
onRetry={fetchExamplesAndFilters}
|
||||
error={error}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import {
|
||||
@@ -21,18 +21,7 @@ import { Divider } from '@material-ui/core';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
import { IconContainer } from '../../UI/IconContainer';
|
||||
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
|
||||
|
||||
type Props = {|
|
||||
extensionShortHeader: ExtensionShortHeader,
|
||||
isInstalling: boolean,
|
||||
onClose: () => void,
|
||||
onInstall: () => void,
|
||||
alreadyInstalled: boolean,
|
||||
|};
|
||||
type State = {|
|
||||
extensionHeader: ?ExtensionHeader,
|
||||
error: ?Error,
|
||||
|};
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
|
||||
const getTransformedDescription = (extensionHeader: ExtensionHeader) => {
|
||||
if (
|
||||
@@ -49,134 +38,144 @@ const getTransformedDescription = (extensionHeader: ExtensionHeader) => {
|
||||
return extensionHeader.description;
|
||||
};
|
||||
|
||||
export default class ExtensionInstallDialog extends Component<Props, State> {
|
||||
state = {
|
||||
extensionHeader: null,
|
||||
error: null,
|
||||
};
|
||||
type Props = {|
|
||||
extensionShortHeader: ExtensionShortHeader,
|
||||
isInstalling: boolean,
|
||||
onClose: () => void,
|
||||
onInstall: () => Promise<void>,
|
||||
alreadyInstalled: boolean,
|
||||
|};
|
||||
|
||||
componentDidMount() {
|
||||
this._loadExtensionheader();
|
||||
}
|
||||
const ExtensionInstallDialog = ({
|
||||
extensionShortHeader,
|
||||
isInstalling,
|
||||
onClose,
|
||||
onInstall,
|
||||
alreadyInstalled,
|
||||
}: Props) => {
|
||||
const [error, setError] = React.useState<?Error>(null);
|
||||
const [
|
||||
extensionHeader,
|
||||
setExtensionHeader,
|
||||
] = React.useState<?ExtensionHeader>(null);
|
||||
|
||||
_loadExtensionheader = () => {
|
||||
this.setState({
|
||||
error: null,
|
||||
});
|
||||
getExtensionHeader(this.props.extensionShortHeader).then(
|
||||
extensionHeader => {
|
||||
this.setState({
|
||||
extensionHeader,
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this.setState({
|
||||
error,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
const loadExtensionheader = React.useCallback(
|
||||
() => {
|
||||
setError(null);
|
||||
getExtensionHeader(extensionShortHeader).then(
|
||||
extensionHeader => {
|
||||
setExtensionHeader(extensionHeader);
|
||||
},
|
||||
error => {
|
||||
setError(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
[extensionShortHeader]
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
isInstalling,
|
||||
extensionShortHeader,
|
||||
onClose,
|
||||
onInstall,
|
||||
alreadyInstalled,
|
||||
} = this.props;
|
||||
const { extensionHeader, error } = this.state;
|
||||
React.useEffect(() => loadExtensionheader(), [loadExtensionheader]);
|
||||
|
||||
const isCompatible = isCompatibleWithExtension(
|
||||
getIDEVersion(),
|
||||
extensionShortHeader
|
||||
);
|
||||
const isCompatible = isCompatibleWithExtension(
|
||||
getIDEVersion(),
|
||||
extensionShortHeader
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Back</Trans>}
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
disabled={isInstalling}
|
||||
/>,
|
||||
<LeftLoader isLoading={isInstalling} key="install">
|
||||
<FlatButton
|
||||
label={
|
||||
!isCompatible ? (
|
||||
<Trans>Not compatible</Trans>
|
||||
) : alreadyInstalled ? (
|
||||
<Trans>Re-install/update</Trans>
|
||||
) : (
|
||||
<Trans>Install in project</Trans>
|
||||
)
|
||||
}
|
||||
primary
|
||||
onClick={onInstall}
|
||||
disabled={isInstalling || !isCompatible}
|
||||
/>
|
||||
</LeftLoader>,
|
||||
]}
|
||||
cannotBeDismissed={false}
|
||||
open
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{!isCompatible && (
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
Unfortunately, this extension requires a newer version of
|
||||
GDevelop to work. Update GDevelop to be able to use this
|
||||
extension in your project.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
<Line alignItems="center" noMargin>
|
||||
<IconContainer
|
||||
alt={extensionShortHeader.fullName}
|
||||
src={extensionShortHeader.previewIconUrl}
|
||||
size={64}
|
||||
/>
|
||||
<Column expand>
|
||||
<Text noMargin size="title">
|
||||
{extensionShortHeader.fullName}
|
||||
</Text>
|
||||
<Text noMargin size="body2">
|
||||
<Trans>Version {' ' + extensionShortHeader.version}</Trans>
|
||||
</Text>
|
||||
<Line>
|
||||
{extensionShortHeader.authors &&
|
||||
extensionShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip
|
||||
user={author}
|
||||
key={author.id}
|
||||
isClickable
|
||||
/>
|
||||
))}
|
||||
</Line>
|
||||
</Column>
|
||||
</Line>
|
||||
<Text noMargin>{extensionShortHeader.shortDescription}</Text>
|
||||
<Divider />
|
||||
{extensionHeader && (
|
||||
<MarkdownText
|
||||
source={getTransformedDescription(extensionHeader)}
|
||||
isStandaloneText
|
||||
/>
|
||||
)}
|
||||
{!extensionHeader && !error && <PlaceholderLoader />}
|
||||
{!extensionHeader && error && (
|
||||
<PlaceholderError onRetry={this._loadExtensionheader}>
|
||||
<Trans>
|
||||
Can't load the extension registry. Verify your internet
|
||||
connection or try again later.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
const canInstallExtension = !isInstalling && isCompatible;
|
||||
const onInstallExtension = React.useCallback(
|
||||
() => {
|
||||
if (canInstallExtension) onInstall();
|
||||
},
|
||||
[onInstall, canInstallExtension]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Back</Trans>}
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
disabled={isInstalling}
|
||||
/>,
|
||||
<LeftLoader isLoading={isInstalling} key="install">
|
||||
<RaisedButton
|
||||
label={
|
||||
!isCompatible ? (
|
||||
<Trans>Not compatible</Trans>
|
||||
) : alreadyInstalled ? (
|
||||
<Trans>Re-install/update</Trans>
|
||||
) : (
|
||||
<Trans>Install in project</Trans>
|
||||
)
|
||||
}
|
||||
primary
|
||||
onClick={onInstallExtension}
|
||||
disabled={!canInstallExtension}
|
||||
/>
|
||||
</LeftLoader>,
|
||||
]}
|
||||
cannotBeDismissed={false}
|
||||
open
|
||||
onRequestClose={onClose}
|
||||
onApply={onInstallExtension}
|
||||
>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{!isCompatible && (
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
Unfortunately, this extension requires a newer version of GDevelop
|
||||
to work. Update GDevelop to be able to use this extension in your
|
||||
project.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
<Line alignItems="center" noMargin>
|
||||
<IconContainer
|
||||
alt={extensionShortHeader.fullName}
|
||||
src={extensionShortHeader.previewIconUrl}
|
||||
size={64}
|
||||
/>
|
||||
<Column expand>
|
||||
<Text noMargin size="title">
|
||||
{extensionShortHeader.fullName}
|
||||
</Text>
|
||||
<Text noMargin size="body2">
|
||||
<Trans>Version {' ' + extensionShortHeader.version}</Trans>
|
||||
</Text>
|
||||
<Line>
|
||||
{extensionShortHeader.authors &&
|
||||
extensionShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip
|
||||
user={author}
|
||||
key={author.id}
|
||||
isClickable
|
||||
/>
|
||||
))}
|
||||
</Line>
|
||||
</Column>
|
||||
</Line>
|
||||
<Text noMargin>{extensionShortHeader.shortDescription}</Text>
|
||||
<Divider />
|
||||
{extensionHeader && (
|
||||
<MarkdownText
|
||||
source={getTransformedDescription(extensionHeader)}
|
||||
isStandaloneText
|
||||
/>
|
||||
)}
|
||||
{!extensionHeader && !error && <PlaceholderLoader />}
|
||||
{!extensionHeader && error && (
|
||||
<PlaceholderError onRetry={loadExtensionheader}>
|
||||
<Trans>
|
||||
Can't load the extension registry. Verify your internet connection
|
||||
or try again later.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExtensionInstallDialog;
|
||||
|
@@ -9,11 +9,13 @@ import { IconContainer } from '../../UI/IconContainer';
|
||||
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
|
||||
|
||||
const styles = {
|
||||
button: { width: '100%' },
|
||||
container: {
|
||||
display: 'flex',
|
||||
textAlign: 'left',
|
||||
overflow: 'hidden',
|
||||
padding: 8,
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,7 +44,7 @@ export const ExtensionListItem = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<ButtonBase onClick={onChoose} focusRipple>
|
||||
<ButtonBase onClick={onChoose} focusRipple style={styles.button}>
|
||||
<div style={styles.container} ref={containerRef}>
|
||||
<Line>
|
||||
<IconContainer
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import * as React from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
@@ -13,7 +14,6 @@ import DismissableInfoBar from '../../UI/Messages/DismissableInfoBar';
|
||||
import { type ExtensionShortHeader } from '../../Utils/GDevelopServices/Extension';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
ACHIEVEMENT_FEATURE_FLAG,
|
||||
addCreateBadgePreHookIfNotClaimed,
|
||||
TRIVIAL_FIRST_EXTENSION,
|
||||
} from '../../Utils/GDevelopServices/Badge';
|
||||
@@ -22,6 +22,7 @@ type Props = {|
|
||||
project: gdProject,
|
||||
onClose: () => void,
|
||||
onInstallExtension: ExtensionShortHeader => void,
|
||||
onExtensionInstalled?: (extensionShortHeader?: ExtensionShortHeader) => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,7 @@ export default function ExtensionsSearchDialog({
|
||||
project,
|
||||
onClose,
|
||||
onInstallExtension,
|
||||
onExtensionInstalled,
|
||||
}: Props) {
|
||||
const [isInstalling, setIsInstalling] = React.useState(false);
|
||||
const [extensionWasInstalled, setExtensionWasInstalled] = React.useState(
|
||||
@@ -41,13 +43,46 @@ export default function ExtensionsSearchDialog({
|
||||
);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
|
||||
const installDisplayedExtension = ACHIEVEMENT_FEATURE_FLAG
|
||||
? addCreateBadgePreHookIfNotClaimed(
|
||||
authenticatedUser,
|
||||
TRIVIAL_FIRST_EXTENSION,
|
||||
installExtension
|
||||
)
|
||||
: installExtension;
|
||||
const installDisplayedExtension = addCreateBadgePreHookIfNotClaimed(
|
||||
authenticatedUser,
|
||||
TRIVIAL_FIRST_EXTENSION,
|
||||
installExtension
|
||||
);
|
||||
|
||||
const installOrImportExtension = async (
|
||||
i18n: I18nType,
|
||||
extensionShortHeader?: ExtensionShortHeader
|
||||
) => {
|
||||
setIsInstalling(true);
|
||||
try {
|
||||
let wasExtensionInstalledOrImported;
|
||||
if (!!extensionShortHeader) {
|
||||
onInstallExtension(extensionShortHeader);
|
||||
wasExtensionInstalledOrImported = await installDisplayedExtension(
|
||||
i18n,
|
||||
project,
|
||||
eventsFunctionsExtensionsState,
|
||||
extensionShortHeader
|
||||
);
|
||||
} else {
|
||||
wasExtensionInstalledOrImported = await importExtension(
|
||||
i18n,
|
||||
eventsFunctionsExtensionsState,
|
||||
project
|
||||
);
|
||||
}
|
||||
|
||||
if (wasExtensionInstalledOrImported) {
|
||||
setExtensionWasInstalled(true);
|
||||
if (onExtensionInstalled) onExtensionInstalled();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const eventsFunctionsExtensionOpener = eventsFunctionsExtensionsState.getEventsFunctionsExtensionOpener();
|
||||
|
||||
@@ -55,6 +90,7 @@ export default function ExtensionsSearchDialog({
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Dialog
|
||||
fullHeight
|
||||
title={<Trans>Search for New Extensions</Trans>}
|
||||
actions={[
|
||||
<FlatButton
|
||||
@@ -73,16 +109,7 @@ export default function ExtensionsSearchDialog({
|
||||
key="import"
|
||||
label={<Trans>Import extension</Trans>}
|
||||
onClick={() => {
|
||||
(async () => {
|
||||
setIsInstalling(true);
|
||||
const wasExtensionImported = await importExtension(
|
||||
i18n,
|
||||
eventsFunctionsExtensionsState,
|
||||
project
|
||||
);
|
||||
setExtensionWasInstalled(wasExtensionImported);
|
||||
setIsInstalling(false);
|
||||
})();
|
||||
installOrImportExtension(i18n);
|
||||
}}
|
||||
disabled={isInstalling}
|
||||
/>
|
||||
@@ -96,19 +123,9 @@ export default function ExtensionsSearchDialog({
|
||||
>
|
||||
<ExtensionStore
|
||||
isInstalling={isInstalling}
|
||||
onInstall={async extensionShortHeader => {
|
||||
setIsInstalling(true);
|
||||
onInstallExtension(extensionShortHeader);
|
||||
const wasExtensionInstalled = await installDisplayedExtension(
|
||||
i18n,
|
||||
project,
|
||||
eventsFunctionsExtensionsState,
|
||||
extensionShortHeader
|
||||
);
|
||||
|
||||
setExtensionWasInstalled(wasExtensionInstalled);
|
||||
setIsInstalling(false);
|
||||
}}
|
||||
onInstall={async extensionShortHeader =>
|
||||
installOrImportExtension(i18n, extensionShortHeader)
|
||||
}
|
||||
project={project}
|
||||
showOnlyWithBehaviors={false}
|
||||
/>
|
||||
|
@@ -71,7 +71,8 @@ export const importExtension = async (
|
||||
await addSerializedExtensionsToProject(
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
[serializedExtension]
|
||||
[serializedExtension],
|
||||
false
|
||||
);
|
||||
return true;
|
||||
} catch (rawError) {
|
||||
|
@@ -1,18 +1,13 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import SearchBar from '../../UI/SearchBar';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import Background from '../../UI/Background';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { Column } from '../../UI/Grid';
|
||||
import { type ExtensionShortHeader } from '../../Utils/GDevelopServices/Extension';
|
||||
import { FiltersChooser } from '../../UI/Search/FiltersChooser';
|
||||
import { ExtensionStoreContext } from './ExtensionStoreContext';
|
||||
import { ListSearchResults } from '../../UI/Search/ListSearchResults';
|
||||
import { ExtensionListItem } from './ExtensionListItem';
|
||||
import { ResponsiveWindowMeasurer } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import ExtensionInstallDialog from './ExtensionInstallDialog';
|
||||
import Subheader from '../../UI/Subheader';
|
||||
|
||||
const styles = {
|
||||
searchBar: {
|
||||
@@ -24,7 +19,7 @@ const styles = {
|
||||
type Props = {|
|
||||
isInstalling: boolean,
|
||||
project: gdProject,
|
||||
onInstall: ExtensionShortHeader => Promise<void>,
|
||||
onInstall: ExtensionShortHeader => Promise<boolean>,
|
||||
showOnlyWithBehaviors: boolean,
|
||||
|};
|
||||
|
||||
@@ -76,47 +71,30 @@ export const ExtensionStore = ({
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
style={styles.searchBar}
|
||||
tagsHandler={{
|
||||
add: filtersState.addFilter,
|
||||
remove: filtersState.removeFilter,
|
||||
chosenTags: Array.from(filtersState.chosenFilters),
|
||||
}}
|
||||
tags={filters && filters.allTags}
|
||||
/>
|
||||
<ListSearchResults
|
||||
onRetry={fetchExtensionsAndFilters}
|
||||
error={error}
|
||||
searchItems={filteredSearchResults}
|
||||
getSearchItemUniqueId={getExtensionName}
|
||||
renderSearchItem={(extensionShortHeader, onHeightComputed) => (
|
||||
<ExtensionListItem
|
||||
key={extensionShortHeader.name}
|
||||
project={project}
|
||||
onHeightComputed={onHeightComputed}
|
||||
extensionShortHeader={extensionShortHeader}
|
||||
onChoose={() => {
|
||||
setSelectedExtensionShortHeader(extensionShortHeader);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Line
|
||||
expand
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
<Background
|
||||
noFullHeight
|
||||
noExpand
|
||||
width={windowWidth === 'small' ? 150 : 250}
|
||||
>
|
||||
<ScrollView>
|
||||
<Subheader>
|
||||
<Trans>Filters</Trans>
|
||||
</Subheader>
|
||||
<FiltersChooser
|
||||
allFilters={filters}
|
||||
filtersState={filtersState}
|
||||
error={error}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Background>
|
||||
<ListSearchResults
|
||||
onRetry={fetchExtensionsAndFilters}
|
||||
error={error}
|
||||
searchItems={filteredSearchResults}
|
||||
getSearchItemUniqueId={getExtensionName}
|
||||
renderSearchItem={(extensionShortHeader, onHeightComputed) => (
|
||||
<ExtensionListItem
|
||||
key={extensionShortHeader.name}
|
||||
project={project}
|
||||
onHeightComputed={onHeightComputed}
|
||||
extensionShortHeader={extensionShortHeader}
|
||||
onChoose={() => {
|
||||
setSelectedExtensionShortHeader(extensionShortHeader);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
</ResponsiveWindowMeasurer>
|
||||
@@ -127,8 +105,9 @@ export const ExtensionStore = ({
|
||||
alreadyInstalled={project.hasEventsFunctionsExtensionNamed(
|
||||
selectedExtensionShortHeader.name
|
||||
)}
|
||||
onInstall={() => {
|
||||
onInstall(selectedExtensionShortHeader);
|
||||
onInstall={async () => {
|
||||
const wasInstalled = await onInstall(selectedExtensionShortHeader);
|
||||
if (wasInstalled) setSelectedExtensionShortHeader(null);
|
||||
}}
|
||||
onClose={() => setSelectedExtensionShortHeader(null)}
|
||||
/>
|
||||
|
@@ -400,7 +400,8 @@ export const downloadExtensions = async (
|
||||
export const addSerializedExtensionsToProject = (
|
||||
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
|
||||
project: gdProject,
|
||||
serializedExtensions: Array<SerializedExtension>
|
||||
serializedExtensions: Array<SerializedExtension>,
|
||||
fromExtensionStore: boolean = true
|
||||
): Promise<void> => {
|
||||
serializedExtensions.forEach(serializedExtension => {
|
||||
const { name } = serializedExtension;
|
||||
@@ -419,6 +420,10 @@ export const addSerializedExtensionsToProject = (
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
|
||||
if (fromExtensionStore) {
|
||||
newEventsFunctionsExtension.setOrigin('gdevelop-extension-store', name);
|
||||
}
|
||||
});
|
||||
|
||||
return eventsFunctionsExtensionsState.loadProjectEventsFunctionsExtensions(
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import {
|
||||
addAssetToProject,
|
||||
addSerializedExtensionsToProject,
|
||||
getRequiredBehaviorsFromAsset,
|
||||
filterMissingBehaviors,
|
||||
downloadExtensions,
|
||||
@@ -485,6 +486,71 @@ describe('InstallAsset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSerializedExtensionsToProject', () => {
|
||||
const mockEventsFunctionsExtensionsState: EventsFunctionsExtensionsState = {
|
||||
eventsFunctionsExtensionsError: null,
|
||||
loadProjectEventsFunctionsExtensions: () => Promise.resolve(),
|
||||
unloadProjectEventsFunctionsExtensions: () => {},
|
||||
reloadProjectEventsFunctionsExtensions: () => Promise.resolve(),
|
||||
unloadProjectEventsFunctionsExtension: () => {},
|
||||
getEventsFunctionsExtensionWriter: () => null,
|
||||
getEventsFunctionsExtensionOpener: () => null,
|
||||
ensureLoadFinished: () => Promise.resolve(),
|
||||
getIncludeFileHashs: () => ({}),
|
||||
};
|
||||
|
||||
const serializedExtension = { name: 'ExtensionName' };
|
||||
|
||||
it('adds an extension with origin set if it comes from the store', () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
addSerializedExtensionsToProject(
|
||||
mockEventsFunctionsExtensionsState,
|
||||
project,
|
||||
[serializedExtension]
|
||||
);
|
||||
|
||||
expect(
|
||||
project.hasEventsFunctionsExtensionNamed(serializedExtension.name)
|
||||
).toBe(true);
|
||||
expect(
|
||||
project
|
||||
.getEventsFunctionsExtension(serializedExtension.name)
|
||||
.getOriginName()
|
||||
).toEqual('gdevelop-extension-store');
|
||||
expect(
|
||||
project
|
||||
.getEventsFunctionsExtension(serializedExtension.name)
|
||||
.getOriginIdentifier()
|
||||
).toEqual(serializedExtension.name);
|
||||
});
|
||||
|
||||
it("adds an extension with origin not set if it doesn't come from the store", () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
addSerializedExtensionsToProject(
|
||||
mockEventsFunctionsExtensionsState,
|
||||
project,
|
||||
[serializedExtension],
|
||||
false
|
||||
);
|
||||
|
||||
expect(
|
||||
project.hasEventsFunctionsExtensionNamed(serializedExtension.name)
|
||||
).toBe(true);
|
||||
expect(
|
||||
project
|
||||
.getEventsFunctionsExtension(serializedExtension.name)
|
||||
.getOriginName()
|
||||
).toEqual('');
|
||||
expect(
|
||||
project
|
||||
.getEventsFunctionsExtension(serializedExtension.name)
|
||||
.getOriginIdentifier()
|
||||
).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('installAsset', () => {
|
||||
beforeEach(() => {
|
||||
mockFn(getAsset).mockReset();
|
||||
|
@@ -209,6 +209,7 @@ export default function NewObjectDialog({
|
||||
</Tabs>
|
||||
{currentTab === 'asset-store' && (
|
||||
<AssetStore
|
||||
focusOnMount
|
||||
project={project}
|
||||
objectsContainer={objectsContainer}
|
||||
events={events}
|
||||
|
@@ -1,12 +1,13 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import SearchBar from '../UI/SearchBar';
|
||||
import SearchBar, { useShouldAutofocusSearchbar } from '../UI/SearchBar';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import Background from '../UI/Background';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import { BoxSearchResults } from '../UI/Search/BoxSearchResults';
|
||||
import { type SearchBarInterface } from '../UI/SearchBar';
|
||||
import { FiltersChooser } from '../UI/Search/FiltersChooser';
|
||||
import { AssetStoreContext } from './AssetStoreContext';
|
||||
import { AssetCard } from './AssetCard';
|
||||
@@ -26,6 +27,7 @@ type Props = {
|
||||
objectsContainer: gdObjectsContainer,
|
||||
events: gdEventsList,
|
||||
onOpenDetails: AssetShortHeader => void,
|
||||
focusOnMount?: boolean,
|
||||
};
|
||||
|
||||
export const AssetStore = ({
|
||||
@@ -33,6 +35,7 @@ export const AssetStore = ({
|
||||
objectsContainer,
|
||||
events,
|
||||
onOpenDetails,
|
||||
focusOnMount,
|
||||
}: Props) => {
|
||||
const {
|
||||
filters,
|
||||
@@ -51,6 +54,23 @@ export const AssetStore = ({
|
||||
[fetchAssetsAndFilters]
|
||||
);
|
||||
|
||||
const searchBar = React.useRef<?SearchBarInterface>(null);
|
||||
const shouldAutofocusSearchbar = useShouldAutofocusSearchbar();
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (
|
||||
focusOnMount &&
|
||||
shouldAutofocusSearchbar &&
|
||||
searchBar &&
|
||||
searchBar.current
|
||||
) {
|
||||
searchBar.current.focus();
|
||||
}
|
||||
},
|
||||
[shouldAutofocusSearchbar, focusOnMount]
|
||||
);
|
||||
|
||||
return (
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
@@ -60,6 +80,7 @@ export const AssetStore = ({
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
style={styles.searchBar}
|
||||
ref={searchBar}
|
||||
/>
|
||||
<Line
|
||||
expand
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
enumerateBehaviorsMetadata,
|
||||
filterEnumeratedBehaviorMetadata,
|
||||
} from './EnumerateBehaviorsMetadata';
|
||||
import SearchBar, { useShouldAutofocusSearchbar } from '../UI/SearchBar';
|
||||
import SearchBar, {
|
||||
useShouldAutofocusSearchbar,
|
||||
type SearchBarInterface,
|
||||
} from '../UI/SearchBar';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
import { ExtensionStore } from '../AssetStore/ExtensionStore';
|
||||
import Window from '../Utils/Window';
|
||||
@@ -31,17 +34,28 @@ import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader
|
||||
import { installExtension } from '../AssetStore/ExtensionStore/InstallExtension';
|
||||
import DismissableInfoBar from '../UI/Messages/DismissableInfoBar';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
addCreateBadgePreHookIfNotClaimed,
|
||||
TRIVIAL_FIRST_BEHAVIOR,
|
||||
TRIVIAL_FIRST_EXTENSION,
|
||||
} from '../Utils/GDevelopServices/Badge';
|
||||
import { type ExtensionShortHeader } from '../Utils/GDevelopServices/Extension';
|
||||
|
||||
const styles = {
|
||||
disabledItem: { opacity: 0.6 },
|
||||
};
|
||||
|
||||
const BehaviorListItem = ({
|
||||
i18n,
|
||||
behaviorMetadata,
|
||||
alreadyInstalled,
|
||||
onClick,
|
||||
disabled,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
behaviorMetadata: EnumeratedBehaviorMetadata,
|
||||
alreadyInstalled: boolean,
|
||||
onClick: () => void,
|
||||
disabled: boolean,
|
||||
|}) => (
|
||||
@@ -54,7 +68,9 @@ const BehaviorListItem = ({
|
||||
/>
|
||||
}
|
||||
key={behaviorMetadata.type}
|
||||
primaryText={behaviorMetadata.fullName}
|
||||
primaryText={`${behaviorMetadata.fullName} ${
|
||||
alreadyInstalled ? i18n._(t`(already added to this object)`) : ''
|
||||
}`}
|
||||
secondaryText={behaviorMetadata.description}
|
||||
secondaryTextLines={2}
|
||||
onClick={onClick}
|
||||
@@ -66,6 +82,7 @@ const BehaviorListItem = ({
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onChoose: (type: string, defaultName: string) => void,
|
||||
@@ -77,11 +94,14 @@ export default function NewBehaviorDialog({
|
||||
onClose,
|
||||
onChoose,
|
||||
objectType,
|
||||
objectBehaviorsTypes,
|
||||
}: Props) {
|
||||
const [showDeprecated, setShowDeprecated] = React.useState(false);
|
||||
const [searchText, setSearchText] = React.useState('');
|
||||
const [currentTab, setCurrentTab] = React.useState('installed');
|
||||
const searchBar = React.useRef<?SearchBar>(null);
|
||||
const [currentTab, setCurrentTab] = React.useState<'installed' | 'search'>(
|
||||
'installed'
|
||||
);
|
||||
const searchBar = React.useRef<?SearchBarInterface>(null);
|
||||
const scrollView = React.useRef((null: ?ScrollViewInterface));
|
||||
|
||||
const [isInstalling, setIsInstalling] = React.useState(false);
|
||||
@@ -89,6 +109,13 @@ export default function NewBehaviorDialog({
|
||||
const eventsFunctionsExtensionsState = React.useContext(
|
||||
EventsFunctionsExtensionsContext
|
||||
);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
|
||||
const installDisplayedExtension = addCreateBadgePreHookIfNotClaimed(
|
||||
authenticatedUser,
|
||||
TRIVIAL_FIRST_EXTENSION,
|
||||
installExtension
|
||||
);
|
||||
|
||||
const platform = project.getCurrentPlatform();
|
||||
const behaviorMetadata: Array<EnumeratedBehaviorMetadata> = React.useMemo(
|
||||
@@ -126,7 +153,7 @@ export default function NewBehaviorDialog({
|
||||
({ type }) => !!deprecatedBehaviorsInformation[type]
|
||||
);
|
||||
|
||||
const chooseBehavior = (
|
||||
const _chooseBehavior = (
|
||||
i18n: I18nType,
|
||||
{ type, defaultName }: EnumeratedBehaviorMetadata
|
||||
) => {
|
||||
@@ -136,18 +163,55 @@ export default function NewBehaviorDialog({
|
||||
|
||||
return onChoose(type, defaultName);
|
||||
};
|
||||
const chooseBehavior = addCreateBadgePreHookIfNotClaimed(
|
||||
authenticatedUser,
|
||||
TRIVIAL_FIRST_BEHAVIOR,
|
||||
_chooseBehavior
|
||||
);
|
||||
|
||||
const isAmongObjectBehaviors = (
|
||||
behaviorMetadata: EnumeratedBehaviorMetadata
|
||||
) => objectBehaviorsTypes.includes(behaviorMetadata.type);
|
||||
|
||||
const canBehaviorBeUsed = (behaviorMetadata: EnumeratedBehaviorMetadata) => {
|
||||
// An empty object type means the base object, i.e: any object.
|
||||
return (
|
||||
behaviorMetadata.objectType === '' ||
|
||||
behaviorMetadata.objectType === objectType
|
||||
(behaviorMetadata.objectType === '' ||
|
||||
behaviorMetadata.objectType === objectType) &&
|
||||
!isAmongObjectBehaviors(behaviorMetadata)
|
||||
);
|
||||
};
|
||||
|
||||
const hasSearchNoResult =
|
||||
!!searchText && !behaviors.length && !deprecatedBehaviors.length;
|
||||
|
||||
const onInstallExtension = async (
|
||||
i18n: I18nType,
|
||||
extensionShortHeader: ExtensionShortHeader
|
||||
) => {
|
||||
setIsInstalling(true);
|
||||
try {
|
||||
const wasExtensionInstalled = await installDisplayedExtension(
|
||||
i18n,
|
||||
project,
|
||||
eventsFunctionsExtensionsState,
|
||||
extensionShortHeader
|
||||
);
|
||||
|
||||
if (wasExtensionInstalled) {
|
||||
// Setting the extension install time will force a reload of
|
||||
// the behavior metadata, and so the list of behaviors.
|
||||
setExtensionInstallTime(Date.now());
|
||||
setCurrentTab('installed');
|
||||
if (scrollView.current) scrollView.current.scrollToBottom();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -161,11 +225,14 @@ export default function NewBehaviorDialog({
|
||||
onClick={onClose}
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={<HelpButton helpPagePath="/behaviors" />}
|
||||
secondaryActions={[
|
||||
<HelpButton helpPagePath="/behaviors" key="help" />,
|
||||
]}
|
||||
open
|
||||
cannotBeDismissed={false}
|
||||
flexBody
|
||||
noMargin
|
||||
fullHeight
|
||||
>
|
||||
<Column expand noMargin>
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
@@ -201,8 +268,12 @@ export default function NewBehaviorDialog({
|
||||
<List>
|
||||
{behaviors.map((behaviorMetadata, index) => (
|
||||
<BehaviorListItem
|
||||
i18n={i18n}
|
||||
key={index}
|
||||
behaviorMetadata={behaviorMetadata}
|
||||
alreadyInstalled={isAmongObjectBehaviors(
|
||||
behaviorMetadata
|
||||
)}
|
||||
onClick={() => chooseBehavior(i18n, behaviorMetadata)}
|
||||
disabled={!canBehaviorBeUsed(behaviorMetadata)}
|
||||
/>
|
||||
@@ -215,8 +286,12 @@ export default function NewBehaviorDialog({
|
||||
{showDeprecated &&
|
||||
deprecatedBehaviors.map((behaviorMetadata, index) => (
|
||||
<BehaviorListItem
|
||||
i18n={i18n}
|
||||
key={index}
|
||||
behaviorMetadata={behaviorMetadata}
|
||||
alreadyInstalled={isAmongObjectBehaviors(
|
||||
behaviorMetadata
|
||||
)}
|
||||
onClick={() => chooseBehavior(i18n, behaviorMetadata)}
|
||||
disabled={!canBehaviorBeUsed(behaviorMetadata)}
|
||||
/>
|
||||
@@ -261,27 +336,12 @@ export default function NewBehaviorDialog({
|
||||
</React.Fragment>
|
||||
)}
|
||||
{currentTab === 'search' && (
|
||||
<ExtensionStore // TODO
|
||||
<ExtensionStore
|
||||
project={project}
|
||||
isInstalling={isInstalling}
|
||||
onInstall={async extensionShortHeader => {
|
||||
setIsInstalling(true);
|
||||
const wasExtensionInstalled = await installExtension(
|
||||
i18n,
|
||||
project,
|
||||
eventsFunctionsExtensionsState,
|
||||
extensionShortHeader
|
||||
);
|
||||
|
||||
if (wasExtensionInstalled) {
|
||||
// Setting the extension install time will force a reload of
|
||||
// the behavior metadata, and so the list of behaviors.
|
||||
setExtensionInstallTime(Date.now());
|
||||
setCurrentTab('installed');
|
||||
if (scrollView.current) scrollView.current.scrollToBottom();
|
||||
}
|
||||
setIsInstalling(false);
|
||||
}}
|
||||
onInstall={async extensionShortHeader =>
|
||||
onInstallExtension(i18n, extensionShortHeader)
|
||||
}
|
||||
showOnlyWithBehaviors
|
||||
/>
|
||||
)}
|
||||
|
@@ -9,7 +9,6 @@ import IconButton from '../UI/IconButton';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
import { MiniToolbarText } from '../UI/MiniToolbar';
|
||||
import HelpIcon from '../UI/HelpIcon';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import NewBehaviorDialog from './NewBehaviorDialog';
|
||||
import BehaviorsEditorService from './BehaviorsEditorService';
|
||||
import Window from '../Utils/Window';
|
||||
@@ -28,13 +27,11 @@ import EmptyBehaviorsPlaceholder from './EmptyBehaviorsPlaceholder';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import { IconContainer } from '../UI/IconContainer';
|
||||
import {
|
||||
ACHIEVEMENT_FEATURE_FLAG,
|
||||
addCreateBadgePreHookIfNotClaimed,
|
||||
TRIVIAL_FIRST_BEHAVIOR,
|
||||
} from '../Utils/GDevelopServices/Badge';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { getBehaviorTutorialIds } from '../Utils/GDevelopServices/Tutorial';
|
||||
import {
|
||||
addBehaviorToObject,
|
||||
listObjectBehaviorsTypes,
|
||||
} from '../Utils/Behavior';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -52,7 +49,6 @@ const BehaviorsEditor = (props: Props) => {
|
||||
const [newBehaviorDialogOpen, setNewBehaviorDialogOpen] = React.useState(
|
||||
false
|
||||
);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
|
||||
const { object, project } = props;
|
||||
const allBehaviorNames = object.getAllBehaviorNames().toJSArray();
|
||||
@@ -60,47 +56,21 @@ const BehaviorsEditor = (props: Props) => {
|
||||
|
||||
const { values } = React.useContext(PreferencesContext);
|
||||
|
||||
const hasBehaviorWithType = (type: string) => {
|
||||
return allBehaviorNames
|
||||
.map(behaviorName => object.getBehavior(behaviorName))
|
||||
.map(behavior => behavior.getTypeName())
|
||||
.filter(behaviorType => behaviorType === type).length;
|
||||
};
|
||||
|
||||
const _addBehavior = (type: string, defaultName: string) => {
|
||||
setNewBehaviorDialogOpen(false);
|
||||
|
||||
if (hasBehaviorWithType(type)) {
|
||||
const answer = Window.showConfirmDialog(
|
||||
"There is already a behavior of this type attached to the object. It's possible to add this behavior again, but it's unusual and may not be always supported properly. Are you sure you want to add this behavior again?"
|
||||
);
|
||||
|
||||
if (!answer) return;
|
||||
}
|
||||
|
||||
const name = newNameGenerator(defaultName, name =>
|
||||
object.hasBehaviorNamed(name)
|
||||
);
|
||||
gd.WholeProjectRefactorer.addBehaviorAndRequiredBehaviors(
|
||||
const addBehavior = (type: string, defaultName: string) => {
|
||||
const wasBehaviorAdded = addBehaviorToObject(
|
||||
project,
|
||||
object,
|
||||
type,
|
||||
name
|
||||
defaultName
|
||||
);
|
||||
|
||||
if (wasBehaviorAdded) setNewBehaviorDialogOpen(false);
|
||||
|
||||
forceUpdate();
|
||||
if (props.onSizeUpdated) props.onSizeUpdated();
|
||||
props.onUpdateBehaviorsSharedData();
|
||||
};
|
||||
|
||||
const addBehavior = ACHIEVEMENT_FEATURE_FLAG
|
||||
? addCreateBadgePreHookIfNotClaimed(
|
||||
authenticatedUser,
|
||||
TRIVIAL_FIRST_BEHAVIOR,
|
||||
_addBehavior
|
||||
)
|
||||
: _addBehavior;
|
||||
|
||||
const onChangeBehaviorName = (
|
||||
behaviorContent: gdBehaviorContent,
|
||||
newName: string
|
||||
@@ -294,6 +264,7 @@ const BehaviorsEditor = (props: Props) => {
|
||||
<NewBehaviorDialog
|
||||
open={newBehaviorDialogOpen}
|
||||
objectType={object.getType()}
|
||||
objectBehaviorsTypes={listObjectBehaviorsTypes(object)}
|
||||
onClose={() => setNewBehaviorDialogOpen(false)}
|
||||
onChoose={addBehavior}
|
||||
project={project}
|
||||
|
@@ -324,6 +324,7 @@ export const DebuggerConsole = ({
|
||||
onClick={() => setEditingHiddenGroups(false)}
|
||||
/>,
|
||||
]}
|
||||
onApply={() => setEditingHiddenGroups(false)}
|
||||
>
|
||||
<Column>
|
||||
{(() => {
|
||||
|
@@ -35,7 +35,6 @@ import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEd
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import { EmptyEffectsPlaceholder } from './EmptyEffectsPlaceholder';
|
||||
import {
|
||||
ACHIEVEMENT_FEATURE_FLAG,
|
||||
addCreateBadgePreHookIfNotClaimed,
|
||||
TRIVIAL_FIRST_EFFECT,
|
||||
} from '../Utils/GDevelopServices/Badge';
|
||||
@@ -93,13 +92,11 @@ export default function EffectsList(props: Props) {
|
||||
onEffectsUpdated();
|
||||
};
|
||||
|
||||
const addEffect = ACHIEVEMENT_FEATURE_FLAG
|
||||
? addCreateBadgePreHookIfNotClaimed(
|
||||
authenticatedUser,
|
||||
TRIVIAL_FIRST_EFFECT,
|
||||
_addEffect
|
||||
)
|
||||
: _addEffect;
|
||||
const addEffect = addCreateBadgePreHookIfNotClaimed(
|
||||
authenticatedUser,
|
||||
TRIVIAL_FIRST_EFFECT,
|
||||
_addEffect
|
||||
);
|
||||
|
||||
const removeEffect = (name: string) => {
|
||||
effectsContainer.removeEffect(name);
|
||||
|
@@ -25,6 +25,7 @@ import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
import StringArrayEditor from '../StringArrayEditor';
|
||||
import ColorField from '../UI/ColorField';
|
||||
import BehaviorTypeSelector from '../BehaviorTypeSelector';
|
||||
import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -131,6 +132,19 @@ export default class EventsBasedBehaviorPropertiesEditor extends React.Component
|
||||
};
|
||||
};
|
||||
|
||||
_getPropertyGroupNames = (): Array<string> => {
|
||||
const { eventsBasedBehavior } = this.props;
|
||||
const properties = eventsBasedBehavior.getPropertyDescriptors();
|
||||
|
||||
const groupNames = new Set<string>();
|
||||
for (let i = 0; i < properties.size(); i++) {
|
||||
const property = properties.at(i);
|
||||
const group = property.getGroup() || '';
|
||||
groupNames.add(group);
|
||||
}
|
||||
return [...groupNames].sort((a, b) => a.localeCompare(b));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { eventsBasedBehavior } = this.props;
|
||||
|
||||
@@ -340,6 +354,24 @@ export default class EventsBasedBehaviorPropertiesEditor extends React.Component
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SemiControlledAutoComplete
|
||||
floatingLabelText={<Trans>Group name</Trans>}
|
||||
hintText={t`Leave it empty to use the default group.`}
|
||||
fullWidth
|
||||
value={property.getGroup()}
|
||||
onChange={text => {
|
||||
property.setGroup(text);
|
||||
this.forceUpdate();
|
||||
this.props.onPropertiesUpdated();
|
||||
}}
|
||||
dataSource={this._getPropertyGroupNames().map(
|
||||
name => ({
|
||||
text: name,
|
||||
value: name,
|
||||
})
|
||||
)}
|
||||
openOnFocus={true}
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={
|
||||
|
@@ -6,6 +6,7 @@ type EnumeratedProperty = {|
|
||||
name: string,
|
||||
type: string,
|
||||
description: string,
|
||||
group: string,
|
||||
label: string,
|
||||
value: string,
|
||||
extraInfo: Array<string>,
|
||||
@@ -25,6 +26,7 @@ export const enumerateNamedPropertyDescriptorsList = (
|
||||
name: namedProperty.getName(),
|
||||
type: namedProperty.getType(),
|
||||
description: namedProperty.getDescription(),
|
||||
group: namedProperty.getGroup(),
|
||||
label: namedProperty.getLabel(),
|
||||
value: namedProperty.getValue(),
|
||||
extraInfo: namedProperty.getExtraInfo().toJSArray(),
|
||||
@@ -40,6 +42,7 @@ export const toGdPropertyDescriptor = (
|
||||
propertyDescriptor
|
||||
.setType(enumeratedProperty.type)
|
||||
.setDescription(enumeratedProperty.description)
|
||||
.setGroup(enumeratedProperty.group)
|
||||
.setLabel(enumeratedProperty.label)
|
||||
.setValue(enumeratedProperty.value)
|
||||
.setHidden(enumeratedProperty.isHidden);
|
||||
|
@@ -138,9 +138,12 @@ export default class EventsFunctionExtractorDialog extends React.Component<
|
||||
<Dialog
|
||||
onApply={onApply}
|
||||
title={<Trans>Extract the events in a function</Trans>}
|
||||
secondaryActions={
|
||||
<HelpButton helpPagePath="/events/functions/extract-events" />
|
||||
}
|
||||
secondaryActions={[
|
||||
<HelpButton
|
||||
helpPagePath="/events/functions/extract-events"
|
||||
key="help"
|
||||
/>,
|
||||
]}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
|
@@ -308,6 +308,16 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
|
||||
this.forceEventsUpdate();
|
||||
}
|
||||
|
||||
foldAll() {
|
||||
gd.EventsListUnfolder.foldAll(this.props.events);
|
||||
this.forceEventsUpdate();
|
||||
}
|
||||
|
||||
unfoldToLevel(level: number) {
|
||||
gd.EventsListUnfolder.unfoldToLevel(this.props.events, level);
|
||||
this.forceEventsUpdate();
|
||||
}
|
||||
|
||||
_getEventRow(searchedEvent: gdBaseEvent) {
|
||||
// TODO: flatData could be replaced by a hashmap of events to row index
|
||||
return findIndex(
|
||||
|
@@ -2,8 +2,12 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Add from '@material-ui/icons/Add';
|
||||
import { List, type ListItemRefType } from '../../../UI/List';
|
||||
import SearchBar, { useShouldAutofocusSearchbar } from '../../../UI/SearchBar';
|
||||
import SearchBar, {
|
||||
useShouldAutofocusSearchbar,
|
||||
type SearchBarInterface,
|
||||
} from '../../../UI/SearchBar';
|
||||
import { type EnumeratedInstructionOrExpressionMetadata } from '../../../InstructionOrExpression/EnumeratedInstructionOrExpressionMetadata.js';
|
||||
import {
|
||||
type TreeNode,
|
||||
@@ -16,7 +20,9 @@ import { renderInstructionOrExpressionTree } from '../SelectorListItems/Selector
|
||||
import EmptyMessage from '../../../UI/EmptyMessage';
|
||||
import ScrollView, { type ScrollViewInterface } from '../../../UI/ScrollView';
|
||||
import { Line } from '../../../UI/Grid';
|
||||
import RaisedButton from '../../../UI/RaisedButton';
|
||||
import { getInstructionListItemValue } from '../SelectorListItems/Keys';
|
||||
import { ResponsiveLineStackLayout } from '../../../UI/Layout';
|
||||
|
||||
const styles = {
|
||||
searchBar: {
|
||||
@@ -38,6 +44,7 @@ type Props<T> = {|
|
||||
searchPlaceholderIsCondition?: ?boolean,
|
||||
helpPagePath?: ?string,
|
||||
style?: Object,
|
||||
onClickMore?: () => void,
|
||||
|};
|
||||
type State<T> = {|
|
||||
searchText: string,
|
||||
@@ -51,7 +58,7 @@ export default class InstructionOrExpressionSelector<
|
||||
searchText: '',
|
||||
searchResults: [],
|
||||
};
|
||||
_searchBar: ?SearchBar;
|
||||
_searchBar: ?SearchBarInterface;
|
||||
_scrollView = React.createRef<ScrollViewInterface>();
|
||||
_selectedItem = React.createRef<ListItemRefType>();
|
||||
|
||||
@@ -88,6 +95,7 @@ export default class InstructionOrExpressionSelector<
|
||||
useSubheaders,
|
||||
helpPagePath,
|
||||
style,
|
||||
onClickMore,
|
||||
} = this.props;
|
||||
const { searchText } = this.state;
|
||||
const displayedInstructionsList: Array<T> = searchText
|
||||
@@ -137,23 +145,25 @@ export default class InstructionOrExpressionSelector<
|
||||
>
|
||||
{hasResults && (
|
||||
<List>
|
||||
{searchText
|
||||
? displayedInstructionsList.map(
|
||||
enumeratedInstructionOrExpressionMetadata =>
|
||||
renderInstructionOrExpressionListItem({
|
||||
instructionOrExpressionMetadata: enumeratedInstructionOrExpressionMetadata,
|
||||
iconSize: iconSize,
|
||||
onClick: () =>
|
||||
onChoose(
|
||||
enumeratedInstructionOrExpressionMetadata.type,
|
||||
enumeratedInstructionOrExpressionMetadata
|
||||
),
|
||||
selectedValue: getInstructionListItemValue(
|
||||
selectedType
|
||||
{searchText ? (
|
||||
displayedInstructionsList.map(
|
||||
enumeratedInstructionOrExpressionMetadata =>
|
||||
renderInstructionOrExpressionListItem({
|
||||
instructionOrExpressionMetadata: enumeratedInstructionOrExpressionMetadata,
|
||||
iconSize: iconSize,
|
||||
onClick: () =>
|
||||
onChoose(
|
||||
enumeratedInstructionOrExpressionMetadata.type,
|
||||
enumeratedInstructionOrExpressionMetadata
|
||||
),
|
||||
})
|
||||
)
|
||||
: renderInstructionOrExpressionTree({
|
||||
selectedValue: getInstructionListItemValue(
|
||||
selectedType
|
||||
),
|
||||
})
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{renderInstructionOrExpressionTree({
|
||||
instructionTreeNode: instructionsInfoTree,
|
||||
iconSize,
|
||||
onChoose,
|
||||
@@ -164,6 +174,20 @@ export default class InstructionOrExpressionSelector<
|
||||
initiallyOpenedPath: this.initialInstructionTypePath,
|
||||
selectedItemRef: this._selectedItem,
|
||||
})}
|
||||
{onClickMore && (
|
||||
<ResponsiveLineStackLayout justifyContent="center">
|
||||
<RaisedButton
|
||||
primary
|
||||
icon={<Add />}
|
||||
onClick={onClickMore}
|
||||
label={
|
||||
<Trans>Add a new behavior to the object</Trans>
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
{!hasResults && (
|
||||
|
@@ -5,6 +5,8 @@ import { type I18n as I18nType } from '@lingui/core';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import Add from '@material-ui/icons/Add';
|
||||
import { Chip } from '@material-ui/core';
|
||||
import {
|
||||
createTree,
|
||||
type InstructionOrExpressionTreeNode,
|
||||
@@ -21,7 +23,10 @@ import {
|
||||
filterEnumeratedInstructionOrExpressionMetadataByScope,
|
||||
} from '../../InstructionOrExpression/EnumeratedInstructionOrExpressionMetadata.js';
|
||||
import { List, type ListItemRefType, ListItem } from '../../UI/List';
|
||||
import SearchBar, { useShouldAutofocusSearchbar } from '../../UI/SearchBar';
|
||||
import SearchBar, {
|
||||
useShouldAutofocusSearchbar,
|
||||
type SearchBarInterface,
|
||||
} from '../../UI/SearchBar';
|
||||
import ThemeConsumer from '../../UI/Theme/ThemeConsumer';
|
||||
import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView';
|
||||
import { Tabs, Tab } from '../../UI/Tabs';
|
||||
@@ -33,6 +38,8 @@ import {
|
||||
enumerateObjects,
|
||||
} from '../../ObjectsList/EnumerateObjects';
|
||||
import TagChips from '../../UI/TagChips';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import { renderGroupObjectsListItem } from './SelectorListItems/SelectorGroupObjectsListItem';
|
||||
import { renderObjectListItem } from './SelectorListItems/SelectorObjectListItem';
|
||||
import { renderInstructionOrExpressionListItem } from './SelectorListItems/SelectorInstructionOrExpressionListItem';
|
||||
@@ -78,6 +85,7 @@ type Props = {|
|
||||
onChooseObject: (objectName: string) => void,
|
||||
onSearchStartOrReset?: () => void,
|
||||
style?: Object,
|
||||
onClickMore?: () => void,
|
||||
|};
|
||||
|
||||
const iconSize = 24;
|
||||
@@ -87,7 +95,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
State
|
||||
> {
|
||||
state = { searchText: '', selectedObjectTags: [] };
|
||||
_searchBar = React.createRef<SearchBar>();
|
||||
_searchBar = React.createRef<SearchBarInterface>();
|
||||
_scrollView = React.createRef<ScrollViewInterface>();
|
||||
_selectedItem = React.createRef<ListItemRefType>();
|
||||
|
||||
@@ -104,6 +112,15 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
this.props.chosenInstructionType
|
||||
);
|
||||
|
||||
reEnumerateInstructions = () => {
|
||||
this.freeInstructionsInfo = filterEnumeratedInstructionOrExpressionMetadataByScope(
|
||||
enumerateFreeInstructions(this.props.isCondition),
|
||||
this.props.scope
|
||||
);
|
||||
this.freeInstructionsInfoTree = createTree(this.freeInstructionsInfo);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
// All the instructions, to be used when searching, so that the search is done
|
||||
// across all the instructions (including object and behaviors instructions).
|
||||
allInstructionsInfo: Array<EnumeratedInstructionMetadata> = filterEnumeratedInstructionOrExpressionMetadataByScope(
|
||||
@@ -165,6 +182,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
currentTab,
|
||||
onChangeTab,
|
||||
onSearchStartOrReset,
|
||||
onClickMore,
|
||||
} = this.props;
|
||||
const { searchText, selectedObjectTags } = this.state;
|
||||
|
||||
@@ -198,6 +216,10 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
? deduplicateInstructionsList(filteredInstructionsList.slice(0, 20))
|
||||
: filteredInstructionsList;
|
||||
|
||||
const displayedTags = isSearching
|
||||
? this._getAllObjectTags().filter(tag => tag.includes(searchText))
|
||||
: [];
|
||||
|
||||
const remainingResultsCount = isSearching
|
||||
? Math.max(filteredInstructionsList.length - 20, 0)
|
||||
: 0;
|
||||
@@ -206,7 +228,8 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
!isSearching ||
|
||||
!!displayedObjectsList.length ||
|
||||
!!displayedObjectGroupsList.length ||
|
||||
!!displayedInstructionsList.length;
|
||||
!!displayedInstructionsList.length ||
|
||||
!!displayedTags.length;
|
||||
|
||||
const onSubmitSearch = () => {
|
||||
if (!isSearching) return;
|
||||
@@ -252,7 +275,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
}
|
||||
}}
|
||||
onRequestSearch={onSubmitSearch}
|
||||
buildTagsMenuTemplate={() =>
|
||||
buildMenuTemplate={() =>
|
||||
this._buildObjectTagsMenuTemplate(i18n)
|
||||
}
|
||||
style={styles.searchBar}
|
||||
@@ -330,6 +353,31 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{isSearching &&
|
||||
currentTab === 'objects' &&
|
||||
displayedTags.length > 0 && (
|
||||
<Subheader>
|
||||
<Trans>Object tags</Trans>
|
||||
</Subheader>
|
||||
)}
|
||||
{currentTab === 'objects' &&
|
||||
displayedTags.map(tag => (
|
||||
<ListItem
|
||||
key={tag}
|
||||
primaryText={<Chip label={tag} />}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
selectedObjectTags: [
|
||||
...this.state.selectedObjectTags,
|
||||
tag,
|
||||
],
|
||||
searchText: '',
|
||||
});
|
||||
this._searchBar.current &&
|
||||
this._searchBar.current.focus();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{isSearching && displayedInstructionsList.length > 0 && (
|
||||
<Subheader>
|
||||
{isCondition ? (
|
||||
@@ -356,18 +404,43 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
: undefined,
|
||||
})
|
||||
)}
|
||||
{!isSearching &&
|
||||
currentTab === 'free-instructions' &&
|
||||
renderInstructionOrExpressionTree({
|
||||
instructionTreeNode: this.freeInstructionsInfoTree,
|
||||
onChoose: onChooseInstruction,
|
||||
iconSize,
|
||||
selectedValue: chosenInstructionType
|
||||
? getInstructionListItemValue(chosenInstructionType)
|
||||
: undefined,
|
||||
initiallyOpenedPath: this.initialInstructionTypePath,
|
||||
selectedItemRef: this._selectedItem,
|
||||
})}
|
||||
{!isSearching && currentTab === 'free-instructions' && (
|
||||
<>
|
||||
{renderInstructionOrExpressionTree({
|
||||
instructionTreeNode: this.freeInstructionsInfoTree,
|
||||
onChoose: onChooseInstruction,
|
||||
iconSize,
|
||||
selectedValue: chosenInstructionType
|
||||
? getInstructionListItemValue(
|
||||
chosenInstructionType
|
||||
)
|
||||
: undefined,
|
||||
initiallyOpenedPath: this
|
||||
.initialInstructionTypePath,
|
||||
selectedItemRef: this._selectedItem,
|
||||
})}
|
||||
{onClickMore && (
|
||||
<ResponsiveLineStackLayout justifyContent="center">
|
||||
<RaisedButton
|
||||
primary
|
||||
icon={<Add />}
|
||||
onClick={onClickMore}
|
||||
label={
|
||||
isCondition ? (
|
||||
<Trans>
|
||||
Search for new conditions in extensions
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Search for new actions in extensions
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{remainingResultsCount > 0 && (
|
||||
<ListItem
|
||||
primaryText={
|
||||
|
@@ -18,14 +18,21 @@ import HelpButton from '../../UI/HelpButton';
|
||||
import { type EventsScope } from '../../InstructionOrExpression/EventsScope.flow';
|
||||
import { SelectColumns } from '../../UI/Reponsive/SelectColumns';
|
||||
import {
|
||||
ResponsiveWindowMeasurer,
|
||||
useResponsiveWindowWidth,
|
||||
type WidthType,
|
||||
} from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import {
|
||||
useNewInstructionEditor,
|
||||
getInstructionMetadata,
|
||||
} from './NewInstructionEditor';
|
||||
import NewBehaviorDialog from '../../BehaviorsEditor/NewBehaviorDialog';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import getObjectByName from '../../Utils/GetObjectByName';
|
||||
import {
|
||||
addBehaviorToObject,
|
||||
listObjectBehaviorsTypes,
|
||||
} from '../../Utils/Behavior';
|
||||
import ExtensionsSearchDialog from '../../AssetStore/ExtensionStore/ExtensionsSearchDialog';
|
||||
|
||||
const styles = {
|
||||
fullHeightSelector: {
|
||||
@@ -122,6 +129,12 @@ export default function NewInstructionEditorDialog({
|
||||
} = newInstructionEditorSetters;
|
||||
const hasObjectChosen =
|
||||
!!chosenObjectInstructionsInfo && !!chosenObjectInstructionsInfoTree;
|
||||
const chosenObject = chosenObjectName
|
||||
? getObjectByName(project, scope.layout, chosenObjectName)
|
||||
: null;
|
||||
const freeInstructionComponentRef = React.useRef<?InstructionOrObjectSelector>(
|
||||
null
|
||||
);
|
||||
const [step, setStep] = React.useState(() =>
|
||||
getInitialStepName(isNewInstruction)
|
||||
);
|
||||
@@ -129,7 +142,16 @@ export default function NewInstructionEditorDialog({
|
||||
currentInstructionOrObjectSelectorTab,
|
||||
setCurrentInstructionOrObjectSelectorTab,
|
||||
] = React.useState(() => getInitialTab(isNewInstruction, hasObjectChosen));
|
||||
const windowWidth = useResponsiveWindowWidth();
|
||||
const instructionType: string = instruction.getType();
|
||||
const [
|
||||
newBehaviorDialogOpen,
|
||||
setNewBehaviorDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const [
|
||||
newExtensionDialogOpen,
|
||||
setNewExtensionDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
|
||||
// Handle the back button
|
||||
const stepBackFrom = (origin: StepName, windowWidth: WidthType) => {
|
||||
@@ -145,6 +167,30 @@ export default function NewInstructionEditorDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const addBehavior = (type: string, defaultName: string) => {
|
||||
if (!chosenObject) return;
|
||||
|
||||
const wasBehaviorAdded = addBehaviorToObject(
|
||||
project,
|
||||
chosenObject,
|
||||
type,
|
||||
defaultName
|
||||
);
|
||||
|
||||
if (wasBehaviorAdded) setNewBehaviorDialogOpen(false);
|
||||
|
||||
// Re-choose the same object to force recomputation of chosenObjectInstructionsInfoTree
|
||||
// This is not done automatically because a change in the object behaviors
|
||||
// is not detected by React at this level.
|
||||
chooseObject(chosenObject.getName());
|
||||
};
|
||||
|
||||
const onExtensionInstalled = () => {
|
||||
setNewExtensionDialogOpen(false);
|
||||
freeInstructionComponentRef.current &&
|
||||
freeInstructionComponentRef.current.reEnumerateInstructions();
|
||||
};
|
||||
|
||||
// Focus the parameters when showing them
|
||||
const instructionParametersEditor = React.useRef(
|
||||
(null: ?InstructionParametersEditor)
|
||||
@@ -175,6 +221,7 @@ export default function NewInstructionEditorDialog({
|
||||
style={styles.fullHeightSelector}
|
||||
project={project}
|
||||
scope={scope}
|
||||
ref={freeInstructionComponentRef}
|
||||
currentTab={currentInstructionOrObjectSelectorTab}
|
||||
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
@@ -192,6 +239,7 @@ export default function NewInstructionEditorDialog({
|
||||
}}
|
||||
focusOnMount={!instructionType}
|
||||
onSearchStartOrReset={forceUpdate}
|
||||
onClickMore={() => setNewExtensionDialogOpen(true)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -232,106 +280,122 @@ export default function NewInstructionEditorDialog({
|
||||
focusOnMount={!instructionType}
|
||||
searchPlaceholderObjectName={chosenObjectName}
|
||||
searchPlaceholderIsCondition={isCondition}
|
||||
onClickMore={() => setNewBehaviorDialogOpen(true)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
<Dialog
|
||||
onApply={instructionType ? onSubmit : null}
|
||||
actions={[
|
||||
<>
|
||||
<Dialog
|
||||
onApply={instructionType ? onSubmit : null}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Cancel</Trans>}
|
||||
primary={false}
|
||||
onClick={onCancel}
|
||||
key="cancel"
|
||||
/>,
|
||||
<FlatButton
|
||||
label={<Trans>Ok</Trans>}
|
||||
primary={true}
|
||||
keyboardFocused={false}
|
||||
disabled={!instructionType}
|
||||
onClick={onSubmit}
|
||||
key="ok"
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
windowWidth !== 'large' && step !== 'object-or-free-instructions' ? (
|
||||
<FlatButton
|
||||
label={<Trans>Cancel</Trans>}
|
||||
label={<Trans>Back</Trans>}
|
||||
primary={false}
|
||||
onClick={onCancel}
|
||||
key="cancel"
|
||||
/>,
|
||||
<FlatButton
|
||||
label={<Trans>Ok</Trans>}
|
||||
primary={true}
|
||||
keyboardFocused={false}
|
||||
disabled={!instructionType}
|
||||
onClick={onSubmit}
|
||||
key="ok"
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
windowWidth !== 'large' &&
|
||||
step !== 'object-or-free-instructions' ? (
|
||||
<FlatButton
|
||||
label={<Trans>Back</Trans>}
|
||||
primary={false}
|
||||
onClick={() => stepBackFrom(step, windowWidth)}
|
||||
key="back"
|
||||
/>
|
||||
) : null,
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath={instructionHelpPage || '/events'}
|
||||
label={
|
||||
!instructionHelpPage ||
|
||||
(windowWidth === 'small' ||
|
||||
step === 'object-or-free-instructions') ? (
|
||||
<Trans>Help</Trans>
|
||||
) : isCondition ? (
|
||||
<Trans>Help for this condition</Trans>
|
||||
) : (
|
||||
<Trans>Help for this action</Trans>
|
||||
)
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
open={open}
|
||||
onRequestClose={onCancel}
|
||||
cannotBeDismissed={true}
|
||||
maxWidth={false}
|
||||
noMargin
|
||||
flexRowBody
|
||||
fullHeight={
|
||||
true /* Always use full height to avoid a very small dialog when there are not a lot of objects. */
|
||||
}
|
||||
>
|
||||
<SelectColumns
|
||||
columnsRenderer={{
|
||||
'instruction-or-object-selector': renderInstructionOrObjectSelector,
|
||||
'object-instruction-selector': renderObjectInstructionSelector,
|
||||
parameters: renderParameters,
|
||||
}}
|
||||
getColumns={() => {
|
||||
if (windowWidth === 'large') {
|
||||
if (chosenObjectName) {
|
||||
return [
|
||||
'instruction-or-object-selector',
|
||||
'object-instruction-selector',
|
||||
'parameters',
|
||||
];
|
||||
} else {
|
||||
return ['instruction-or-object-selector', 'parameters'];
|
||||
}
|
||||
} else if (windowWidth === 'medium') {
|
||||
if (step === 'object-or-free-instructions') {
|
||||
return ['instruction-or-object-selector'];
|
||||
} else {
|
||||
if (chosenObjectName) {
|
||||
return ['object-instruction-selector', 'parameters'];
|
||||
} else {
|
||||
return ['parameters'];
|
||||
}
|
||||
}
|
||||
onClick={() => stepBackFrom(step, windowWidth)}
|
||||
key="back"
|
||||
/>
|
||||
) : null,
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath={instructionHelpPage || '/events'}
|
||||
label={
|
||||
!instructionHelpPage ||
|
||||
(windowWidth === 'small' ||
|
||||
step === 'object-or-free-instructions') ? (
|
||||
<Trans>Help</Trans>
|
||||
) : isCondition ? (
|
||||
<Trans>Help for this condition</Trans>
|
||||
) : (
|
||||
<Trans>Help for this action</Trans>
|
||||
)
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
open={open}
|
||||
onRequestClose={onCancel}
|
||||
cannotBeDismissed={true}
|
||||
maxWidth={false}
|
||||
noMargin
|
||||
flexRowBody
|
||||
fullHeight={
|
||||
true /* Always use full height to avoid a very small dialog when there are not a lot of objects. */
|
||||
}
|
||||
>
|
||||
<SelectColumns
|
||||
columnsRenderer={{
|
||||
'instruction-or-object-selector': renderInstructionOrObjectSelector,
|
||||
'object-instruction-selector': renderObjectInstructionSelector,
|
||||
parameters: renderParameters,
|
||||
}}
|
||||
getColumns={() => {
|
||||
if (windowWidth === 'large') {
|
||||
if (chosenObjectName) {
|
||||
return [
|
||||
'instruction-or-object-selector',
|
||||
'object-instruction-selector',
|
||||
'parameters',
|
||||
];
|
||||
} else {
|
||||
if (step === 'object-or-free-instructions') {
|
||||
return ['instruction-or-object-selector'];
|
||||
} else if (step === 'object-instructions') {
|
||||
return ['object-instruction-selector'];
|
||||
return ['instruction-or-object-selector', 'parameters'];
|
||||
}
|
||||
} else if (windowWidth === 'medium') {
|
||||
if (step === 'object-or-free-instructions') {
|
||||
return ['instruction-or-object-selector'];
|
||||
} else {
|
||||
if (chosenObjectName) {
|
||||
return ['object-instruction-selector', 'parameters'];
|
||||
} else {
|
||||
return ['parameters'];
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
} else {
|
||||
if (step === 'object-or-free-instructions') {
|
||||
return ['instruction-or-object-selector'];
|
||||
} else if (step === 'object-instructions') {
|
||||
return ['object-instruction-selector'];
|
||||
} else {
|
||||
return ['parameters'];
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
{newBehaviorDialogOpen && chosenObject && (
|
||||
<NewBehaviorDialog
|
||||
project={project}
|
||||
open={newBehaviorDialogOpen}
|
||||
objectType={chosenObject.getType()}
|
||||
objectBehaviorsTypes={listObjectBehaviorsTypes(chosenObject)}
|
||||
onClose={() => setNewBehaviorDialogOpen(false)}
|
||||
onChoose={addBehavior}
|
||||
/>
|
||||
)}
|
||||
</ResponsiveWindowMeasurer>
|
||||
{newExtensionDialogOpen && (
|
||||
<ExtensionsSearchDialog
|
||||
project={project}
|
||||
onClose={() => setNewExtensionDialogOpen(false)}
|
||||
onInstallExtension={() => {}}
|
||||
onExtensionInstalled={onExtensionInstalled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import { mapVector } from '../../Utils/MapFor';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
export const enumerateParametersUsableInExpressions = (
|
||||
eventsFunction: gdEventsFunction
|
||||
): Array<gdParameterMetadata> => {
|
||||
return mapVector(eventsFunction.getParameters(), parameterMetadata =>
|
||||
parameterMetadata.isCodeOnly() ||
|
||||
gd.ParameterMetadata.isObject(parameterMetadata.getType()) ||
|
||||
gd.ParameterMetadata.isBehavior(parameterMetadata.getType())
|
||||
? null
|
||||
: parameterMetadata
|
||||
).filter(Boolean);
|
||||
};
|
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import GenericExpressionField from './GenericExpressionField';
|
||||
import { type ParameterFieldProps } from './ParameterFieldCommons';
|
||||
import { type ExpressionAutocompletion } from '../../ExpressionAutocompletion';
|
||||
import { enumerateParametersUsableInExpressions } from './EnumerateFunctionParameters';
|
||||
|
||||
export default class FunctionParameterNameField extends Component<
|
||||
ParameterFieldProps,
|
||||
void
|
||||
> {
|
||||
_field: ?GenericExpressionField;
|
||||
|
||||
focus() {
|
||||
if (this._field) this._field.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const parameterNames: Array<ExpressionAutocompletion> = this.props.scope
|
||||
.eventsFunction
|
||||
? enumerateParametersUsableInExpressions(
|
||||
this.props.scope.eventsFunction
|
||||
).map(parameterMetadata => ({
|
||||
kind: 'Text',
|
||||
completion: `"${parameterMetadata.getName()}"`,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<GenericExpressionField
|
||||
expressionType="string"
|
||||
onGetAdditionalAutocompletions={expression =>
|
||||
parameterNames.filter(
|
||||
({ completion }) => completion.indexOf(expression) === 0
|
||||
)
|
||||
}
|
||||
ref={field => (this._field = field)}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@@ -63,14 +63,14 @@ export default class ExpressionParametersEditorDialog extends Component<
|
||||
title={<Trans>Enter the expression parameters</Trans>}
|
||||
cannotBeDismissed={true}
|
||||
open
|
||||
actions={
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="apply"
|
||||
label={<Trans>Apply</Trans>}
|
||||
primary
|
||||
onClick={() => this.props.onDone(this.state.parameterValues)}
|
||||
/>
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
noMargin
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
>
|
||||
|
@@ -6,16 +6,6 @@ import { type ParameterFieldProps } from './ParameterFieldCommons';
|
||||
import SemiControlledAutoComplete from '../../UI/SemiControlledAutoComplete';
|
||||
|
||||
const keyNames = [
|
||||
'Num0',
|
||||
'Num1',
|
||||
'Num2',
|
||||
'Num3',
|
||||
'Num4',
|
||||
'Num5',
|
||||
'Num6',
|
||||
'Num7',
|
||||
'Num8',
|
||||
'Num9',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
@@ -42,9 +32,16 @@ const keyNames = [
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
'LBracket',
|
||||
'RBracket',
|
||||
'SemiColon',
|
||||
'Num0',
|
||||
'Num1',
|
||||
'Num2',
|
||||
'Num3',
|
||||
'Num4',
|
||||
'Num5',
|
||||
'Num6',
|
||||
'Num7',
|
||||
'Num8',
|
||||
'Num9',
|
||||
'Numpad0',
|
||||
'Numpad1',
|
||||
'Numpad2',
|
||||
@@ -55,17 +52,39 @@ const keyNames = [
|
||||
'Numpad7',
|
||||
'Numpad8',
|
||||
'Numpad9',
|
||||
'Escape',
|
||||
'LShift',
|
||||
'RShift',
|
||||
'LControl',
|
||||
'RControl',
|
||||
'LAlt',
|
||||
'RAlt',
|
||||
'LSystem',
|
||||
'RSystem',
|
||||
'SemiColon',
|
||||
'Comma',
|
||||
'Period',
|
||||
'Quote',
|
||||
'Slash',
|
||||
'BackSlash',
|
||||
'Tilde',
|
||||
'Equal',
|
||||
'Dash',
|
||||
'Space',
|
||||
'Return',
|
||||
'Back',
|
||||
'Tab',
|
||||
'Delete',
|
||||
'Insert',
|
||||
'Escape',
|
||||
'PageUp',
|
||||
'PageDown',
|
||||
'End',
|
||||
'Home',
|
||||
'Insert',
|
||||
'Delete',
|
||||
'Return',
|
||||
'NumpadPageUp',
|
||||
'NumpadPageDown',
|
||||
'NumpadEnd',
|
||||
'NumpadHome',
|
||||
'NumpadReturn',
|
||||
'Add',
|
||||
'Subtract',
|
||||
'Multiply',
|
||||
@@ -75,12 +94,12 @@ const keyNames = [
|
||||
'NumpadMultiply',
|
||||
'NumpadDivide',
|
||||
'Left',
|
||||
'Right',
|
||||
'Up',
|
||||
'Right',
|
||||
'Down',
|
||||
'NumpadLeft',
|
||||
'NumpadRight',
|
||||
'NumpadUp',
|
||||
'NumpadRight',
|
||||
'NumpadDown',
|
||||
'F1',
|
||||
'F2',
|
||||
@@ -95,12 +114,6 @@ const keyNames = [
|
||||
'F11',
|
||||
'F12',
|
||||
'Pause',
|
||||
'RControl',
|
||||
'LControl',
|
||||
'RAlt',
|
||||
'LAlt',
|
||||
'RShift',
|
||||
'LShift',
|
||||
];
|
||||
|
||||
const isKeyValid = (key: string) => keyNames.indexOf(key) !== -1;
|
||||
|
@@ -52,6 +52,7 @@ import ObjectEffectNameField from './ParameterFields/ObjectEffectNameField';
|
||||
import ObjectEffectParameterNameField from './ParameterFields/ObjectEffectParameterNameField';
|
||||
import ObjectPointNameField from './ParameterFields/ObjectPointNameField';
|
||||
import ObjectAnimationNameField from './ParameterFields/ObjectAnimationNameField';
|
||||
import FunctionParameterNameField from './ParameterFields/FunctionParameterNameField';
|
||||
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -89,6 +90,7 @@ const components = {
|
||||
objectEffectParameterName: ObjectEffectParameterNameField,
|
||||
objectPointName: ObjectPointNameField,
|
||||
objectAnimationName: ObjectAnimationNameField,
|
||||
functionParameterName: FunctionParameterNameField,
|
||||
};
|
||||
const inlineRenderers: { [string]: ParameterInlineRenderer } = {
|
||||
default: renderInlineDefaultField,
|
||||
@@ -134,6 +136,7 @@ const userFriendlyTypeName: { [string]: MessageDescriptor } = {
|
||||
objectEffectParameterName: t`Object effect parameter name`,
|
||||
objectPointName: t`Object point name`,
|
||||
objectAnimationName: t`Object animation name`,
|
||||
functionParameterName: t`Parameter name`,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@@ -43,7 +43,9 @@ export const getInitialSelection = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getSelectedEvents = (selection: SelectionState): Array<Object> => {
|
||||
export const getSelectedEvents = (
|
||||
selection: SelectionState
|
||||
): Array<gdBaseEvent> => {
|
||||
return values(selection.selectedEvents).map(
|
||||
(eventContext: EventContext) => eventContext.event
|
||||
);
|
||||
|
@@ -90,7 +90,6 @@ import AuthenticatedUserContext, {
|
||||
import {
|
||||
addCreateBadgePreHookIfNotClaimed,
|
||||
TRIVIAL_FIRST_EVENT,
|
||||
ACHIEVEMENT_FEATURE_FLAG,
|
||||
} from '../Utils/GDevelopServices/Badge';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -258,15 +257,11 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
|
||||
constructor(props: ComponentProps) {
|
||||
super(props);
|
||||
if (ACHIEVEMENT_FEATURE_FLAG) {
|
||||
this.addNewEvent = addCreateBadgePreHookIfNotClaimed(
|
||||
this.props.authenticatedUser,
|
||||
TRIVIAL_FIRST_EVENT,
|
||||
this._addNewEvent
|
||||
);
|
||||
} else {
|
||||
this.addNewEvent = this._addNewEvent;
|
||||
}
|
||||
this.addNewEvent = addCreateBadgePreHookIfNotClaimed(
|
||||
this.props.authenticatedUser,
|
||||
TRIVIAL_FIRST_EVENT,
|
||||
this._addNewEvent
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -274,15 +269,12 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ComponentProps, prevState: State) {
|
||||
if (ACHIEVEMENT_FEATURE_FLAG) {
|
||||
this.addNewEvent = addCreateBadgePreHookIfNotClaimed(
|
||||
this.props.authenticatedUser,
|
||||
TRIVIAL_FIRST_EVENT,
|
||||
this._addNewEvent
|
||||
);
|
||||
} else {
|
||||
this.addNewEvent = this._addNewEvent;
|
||||
}
|
||||
this.addNewEvent = addCreateBadgePreHookIfNotClaimed(
|
||||
this.props.authenticatedUser,
|
||||
TRIVIAL_FIRST_EVENT,
|
||||
this._addNewEvent
|
||||
);
|
||||
|
||||
if (this.state.history !== prevState.history)
|
||||
if (this.props.unsavedChanges)
|
||||
this.props.unsavedChanges.triggerUnsavedChanges();
|
||||
@@ -603,6 +595,14 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
);
|
||||
};
|
||||
|
||||
collapseAll = () => {
|
||||
if (this._eventsTree) this._eventsTree.foldAll();
|
||||
};
|
||||
|
||||
expandToLevel = (level: number) => {
|
||||
if (this._eventsTree) this._eventsTree.unfoldToLevel(level);
|
||||
};
|
||||
|
||||
_buildEventContextMenu = (i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Edit`),
|
||||
@@ -671,6 +671,27 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
accelerator: 'CmdOrCtrl+Shift+Z',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Collapse all`),
|
||||
click: this.collapseAll,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Expand all to level`),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n._(t`All`),
|
||||
click: () => this.expandToLevel(-1),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
...[0, 1, 2, 3, 4, 5, 6, 7, 8].map(index => {
|
||||
return {
|
||||
label: i18n._(t`Level ${index + 1}`),
|
||||
click: () => this.expandToLevel(index),
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Extract Events to a Function`),
|
||||
click: () => this.extractEventsToFunction(),
|
||||
|
@@ -58,6 +58,8 @@ export const browserCordovaExportPipeline: ExportPipeline<
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
isNavigationDisabled: () => false,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
@@ -58,6 +58,8 @@ export const browserElectronExportPipeline: ExportPipeline<
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
isNavigationDisabled: () => false,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
@@ -57,6 +57,8 @@ export const browserFacebookInstantGamesExportPipeline: ExportPipeline<
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
isNavigationDisabled: () => false,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { BlobDownloadUrlHolder } from '../../Utils/BlobDownloadUrlHolder';
|
||||
import { ExplanationHeader, DoneFooter } from '../GenericExporters/HTML5Export';
|
||||
import { openBlobDownloadUrl } from '.';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
@@ -40,21 +41,6 @@ type ResourcesDownloadOutput = {|
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
const openBlobDownloadUrl = (url: string, filename: string) => {
|
||||
const { body } = document;
|
||||
if (!body) return;
|
||||
|
||||
// Not using Window.openExternalURL because blob urls are blocked
|
||||
// by Adblock Plus (and maybe other ad blockers).
|
||||
const a = document.createElement('a');
|
||||
body.appendChild(a);
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
body.removeChild(a);
|
||||
};
|
||||
|
||||
export const browserHTML5ExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
@@ -68,6 +54,8 @@ export const browserHTML5ExportPipeline: ExportPipeline<
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
isNavigationDisabled: () => false,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Export as a HTML5 game</Trans>,
|
||||
|
@@ -64,7 +64,16 @@ export const browserOnlineCordovaExportPipeline: ExportPipeline<
|
||||
signingDialogOpen: false,
|
||||
}),
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
// Build can be launched only if just opened the dialog or build errored.
|
||||
canLaunchBuild: (exportState, errored, exportStep) =>
|
||||
errored || exportStep === '',
|
||||
|
||||
// Navigation is enabled when the build is errored or whilst uploading.
|
||||
isNavigationDisabled: (exportStep, errored) =>
|
||||
!errored &&
|
||||
['register', 'export', 'resources-download', 'compress', 'upload'].includes(
|
||||
exportStep
|
||||
),
|
||||
|
||||
renderHeader: props => <SetupExportHeader {...props} />,
|
||||
|
||||
@@ -153,7 +162,8 @@ export const browserOnlineCordovaExportPipeline: ExportPipeline<
|
||||
launchOnlineBuild: (
|
||||
exportState: ExportState,
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
uploadBucketKey: string
|
||||
uploadBucketKey: string,
|
||||
gameId: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, firebaseUser } = authenticatedUser;
|
||||
if (!firebaseUser)
|
||||
@@ -164,7 +174,8 @@ export const browserOnlineCordovaExportPipeline: ExportPipeline<
|
||||
firebaseUser.uid,
|
||||
uploadBucketKey,
|
||||
exportState.targets,
|
||||
exportState.keystore
|
||||
exportState.keystore,
|
||||
gameId
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@@ -62,7 +62,16 @@ export const browserOnlineElectronExportPipeline: ExportPipeline<
|
||||
targets: ['winExe'],
|
||||
}),
|
||||
|
||||
canLaunchBuild: (exportState: ExportState) => !!exportState.targets.length,
|
||||
// Build can be launched only if just opened the dialog or build errored.
|
||||
canLaunchBuild: (exportState, errored, exportStep) =>
|
||||
!!exportState.targets.length && (errored || exportStep === ''),
|
||||
|
||||
// Navigation is enabled when the build is errored or whilst uploading.
|
||||
isNavigationDisabled: (exportStep, errored) =>
|
||||
!errored &&
|
||||
['register', 'export', 'resources-download', 'compress', 'upload'].includes(
|
||||
exportStep
|
||||
),
|
||||
|
||||
renderHeader: props => <SetupExportHeader {...props} />,
|
||||
|
||||
@@ -151,7 +160,8 @@ export const browserOnlineElectronExportPipeline: ExportPipeline<
|
||||
launchOnlineBuild: (
|
||||
exportState: ExportState,
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
uploadBucketKey: string
|
||||
uploadBucketKey: string,
|
||||
gameId: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, firebaseUser } = authenticatedUser;
|
||||
if (!firebaseUser)
|
||||
@@ -161,7 +171,8 @@ export const browserOnlineElectronExportPipeline: ExportPipeline<
|
||||
getAuthorizationHeader,
|
||||
firebaseUser.uid,
|
||||
uploadBucketKey,
|
||||
exportState.targets
|
||||
exportState.targets,
|
||||
gameId
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@@ -26,6 +26,7 @@ import {
|
||||
ExplanationHeader,
|
||||
WebProjectLink,
|
||||
} from '../GenericExporters/OnlineWebExport';
|
||||
import { type BuildStep } from '../Builds/BuildStepsProgress';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
@@ -60,14 +61,24 @@ export const browserOnlineWebExportPipeline: ExportPipeline<
|
||||
|
||||
getInitialExportState: () => null,
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
// Build can be launched if just opened the dialog or build errored, re-enabled when done.
|
||||
canLaunchBuild: (exportState, errored, exportStep) =>
|
||||
errored || exportStep === '' || exportStep === 'done',
|
||||
|
||||
// Navigation is enabled when the build is errored or if the build is not done.
|
||||
isNavigationDisabled: (exportStep, errored) =>
|
||||
!errored && !['', 'done'].includes(exportStep),
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Generate link</Trans>,
|
||||
|
||||
renderCustomStepsProgress: (build: ?Build, loading: boolean) => (
|
||||
<WebProjectLink build={build} loading={loading} />
|
||||
renderCustomStepsProgress: (
|
||||
build: ?Build,
|
||||
errored: boolean,
|
||||
exportStep: BuildStep
|
||||
) => (
|
||||
<WebProjectLink build={build} errored={errored} exportStep={exportStep} />
|
||||
),
|
||||
|
||||
prepareExporter: (
|
||||
@@ -152,12 +163,18 @@ export const browserOnlineWebExportPipeline: ExportPipeline<
|
||||
launchOnlineBuild: (
|
||||
exportState: ExportState,
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
uploadBucketKey: string
|
||||
uploadBucketKey: string,
|
||||
gameId: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, firebaseUser } = authenticatedUser;
|
||||
if (!firebaseUser)
|
||||
return Promise.reject(new Error('User is not authenticated'));
|
||||
|
||||
return buildWeb(getAuthorizationHeader, firebaseUser.uid, uploadBucketKey);
|
||||
return buildWeb(
|
||||
getAuthorizationHeader,
|
||||
firebaseUser.uid,
|
||||
uploadBucketKey,
|
||||
gameId
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@@ -56,6 +56,7 @@ export default class BrowserPreviewLinkDialog extends Component<Props> {
|
||||
onClick={this._makeOnOpen(i18n)}
|
||||
/>,
|
||||
]}
|
||||
onApply={this._makeOnOpen(i18n)}
|
||||
cannotBeDismissed={true}
|
||||
open
|
||||
>
|
||||
|
109
newIDE/app/src/Export/Builds/BuildCard.js
Normal file
109
newIDE/app/src/Export/Builds/BuildCard.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { differenceInCalendarDays, format } from 'date-fns';
|
||||
import { Line } from '../../UI/Grid';
|
||||
import { type Build } from '../../Utils/GDevelopServices/Build';
|
||||
import { type Game } from '../../Utils/GDevelopServices/Game';
|
||||
import { Card, CardActions, CardHeader } from '@material-ui/core';
|
||||
import BuildProgressAndActions from './BuildProgressAndActions';
|
||||
import EmptyMessage from '../../UI/EmptyMessage';
|
||||
import Chrome from '../../UI/CustomSvgIcons/Chrome';
|
||||
import PhoneIphone from '@material-ui/icons/PhoneIphone';
|
||||
import LaptopMac from '@material-ui/icons/LaptopMac';
|
||||
|
||||
const styles = {
|
||||
icon: {
|
||||
height: 30,
|
||||
width: 30,
|
||||
marginRight: 5,
|
||||
},
|
||||
};
|
||||
|
||||
const formatBuildText = (
|
||||
buildType: 'cordova-build' | 'electron-build' | 'web-build'
|
||||
) => {
|
||||
switch (buildType) {
|
||||
case 'cordova-build':
|
||||
return <Trans>Android Build</Trans>;
|
||||
case 'electron-build':
|
||||
return <Trans>Windows/macOS/Linux Build</Trans>;
|
||||
case 'web-build':
|
||||
return <Trans>Web (upload online) Build</Trans>;
|
||||
default:
|
||||
return buildType;
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = (
|
||||
buildType: 'cordova-build' | 'electron-build' | 'web-build'
|
||||
) => {
|
||||
switch (buildType) {
|
||||
case 'cordova-build':
|
||||
return <PhoneIphone style={styles.icon} />;
|
||||
case 'electron-build':
|
||||
return <LaptopMac style={styles.icon} />;
|
||||
case 'web-build':
|
||||
return <Chrome style={styles.icon} />;
|
||||
default:
|
||||
return <Chrome style={styles.icon} />;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
build: Build,
|
||||
game: Game,
|
||||
onGameUpdated?: Game => void,
|
||||
gameUpdating: boolean,
|
||||
setGameUpdating: boolean => void,
|
||||
|};
|
||||
|
||||
export const BuildCard = ({
|
||||
build,
|
||||
game,
|
||||
onGameUpdated,
|
||||
gameUpdating,
|
||||
setGameUpdating,
|
||||
}: Props) => {
|
||||
const isOld =
|
||||
build &&
|
||||
build.type !== 'web-build' &&
|
||||
differenceInCalendarDays(Date.now(), build.updatedAt) > 6;
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={build.id}
|
||||
subheader={
|
||||
<Line alignItems="center" noMargin>
|
||||
{getIcon(build.type)}
|
||||
<Trans>
|
||||
{formatBuildText(build.type)} - <Trans>Last updated on</Trans>{' '}
|
||||
{format(build.updatedAt, 'yyyy-MM-dd HH:mm:ss')}
|
||||
</Trans>
|
||||
</Line>
|
||||
}
|
||||
/>
|
||||
<CardActions>
|
||||
<Line expand noMargin justifyContent="flex-end">
|
||||
{!isOld && (
|
||||
<BuildProgressAndActions
|
||||
build={build}
|
||||
game={game}
|
||||
onGameUpdated={onGameUpdated}
|
||||
gameUpdating={gameUpdating}
|
||||
setGameUpdating={setGameUpdating}
|
||||
/>
|
||||
)}
|
||||
{isOld && (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This build is old and the generated games can't be downloaded
|
||||
anymore.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
)}
|
||||
</Line>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
@@ -11,9 +11,14 @@ import differenceInSeconds from 'date-fns/differenceInSeconds';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import Text from '../../UI/Text';
|
||||
import {
|
||||
getBuildArtifactUrl,
|
||||
type Build,
|
||||
type BuildArtifactKeyName,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
import { type Game, updateGame } from '../../Utils/GDevelopServices/Game';
|
||||
import Window from '../../Utils/Window';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
|
||||
const buildTypesConfig = {
|
||||
'cordova-build': {
|
||||
@@ -32,7 +37,7 @@ const buildTypesConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
const downloadButtons = [
|
||||
{
|
||||
displayName: t`Download (APK)`,
|
||||
key: 'apkKey',
|
||||
@@ -65,14 +70,26 @@ const buttons = [
|
||||
|
||||
type Props = {|
|
||||
build: Build,
|
||||
onDownload: (key: BuildArtifactKeyName) => void,
|
||||
game?: ?Game,
|
||||
onGameUpdated?: Game => void,
|
||||
gameUpdating?: boolean,
|
||||
setGameUpdating?: boolean => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Show an estimate of the progress of a build or the button
|
||||
* to download the artifacts.
|
||||
*/
|
||||
export default ({ build, onDownload }: Props) => {
|
||||
export default ({
|
||||
build,
|
||||
game,
|
||||
onGameUpdated,
|
||||
gameUpdating,
|
||||
setGameUpdating,
|
||||
}: Props) => {
|
||||
const { getAuthorizationHeader, profile } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const config = buildTypesConfig[build.type];
|
||||
const secondsSinceLastUpdate = Math.abs(
|
||||
differenceInSeconds(build.updatedAt, Date.now())
|
||||
@@ -82,6 +99,38 @@ export default ({ build, onDownload }: Props) => {
|
||||
0
|
||||
);
|
||||
|
||||
const onDownload = (key: BuildArtifactKeyName) => {
|
||||
const url = getBuildArtifactUrl(build, key);
|
||||
if (url) Window.openExternalURL(url);
|
||||
};
|
||||
|
||||
const onUpdatePublicBuild = React.useCallback(
|
||||
async (buildId: ?string) => {
|
||||
if (!profile || !game || !onGameUpdated || !setGameUpdating) return;
|
||||
|
||||
const { id } = profile;
|
||||
try {
|
||||
setGameUpdating(true);
|
||||
const updatedGame = await updateGame(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
game.id,
|
||||
{
|
||||
publicWebBuildId: buildId,
|
||||
}
|
||||
);
|
||||
onGameUpdated(updatedGame);
|
||||
setGameUpdating(false);
|
||||
} catch (err) {
|
||||
console.error('Unable to update the game', err);
|
||||
setGameUpdating(false);
|
||||
}
|
||||
},
|
||||
[game, getAuthorizationHeader, profile, onGameUpdated, setGameUpdating]
|
||||
);
|
||||
|
||||
const isBuildPublished = game && game.publicWebBuildId === build.id;
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) =>
|
||||
@@ -136,18 +185,33 @@ export default ({ build, onDownload }: Props) => {
|
||||
)}
|
||||
</Line>
|
||||
) : build.status === 'complete' ? (
|
||||
<React.Fragment>
|
||||
<Line expand>
|
||||
{buttons
|
||||
<ColumnStackLayout>
|
||||
<Line expand justifyContent="flex-end">
|
||||
{game && !!build.s3Key && !isBuildPublished && (
|
||||
<RaisedButton
|
||||
label={<Trans>Publish this build</Trans>}
|
||||
onClick={() => onUpdatePublicBuild(build.id)}
|
||||
disabled={gameUpdating}
|
||||
/>
|
||||
)}
|
||||
{game && !!build.s3Key && isBuildPublished && (
|
||||
<FlatButton
|
||||
label={<Trans>Unpublish this build</Trans>}
|
||||
onClick={() => onUpdatePublicBuild(null)}
|
||||
disabled={gameUpdating}
|
||||
/>
|
||||
)}
|
||||
<Spacer />
|
||||
{downloadButtons
|
||||
.filter(button => !!build[button.key])
|
||||
.map((button, index) => (
|
||||
.map(button => (
|
||||
<React.Fragment key={button.key}>
|
||||
{index !== 0 && <Spacer />}
|
||||
<RaisedButton
|
||||
label={i18n._(button.displayName)}
|
||||
primary
|
||||
onClick={() => onDownload(button.key)}
|
||||
/>
|
||||
<Spacer />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<FlatButton
|
||||
@@ -155,10 +219,12 @@ export default ({ build, onDownload }: Props) => {
|
||||
onClick={() => onDownload('logsKey')}
|
||||
/>
|
||||
</Line>
|
||||
<Line expand>
|
||||
{config && <Text>{config.completeDescription}</Text>}
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
{config && config.completeDescription && (
|
||||
<Line expand justifyContent="flex-end">
|
||||
<Text>{config.completeDescription}</Text>
|
||||
</Line>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
) : (
|
||||
<Line>
|
||||
<Trans>Unknown status</Trans>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user