Compare commits

...

74 Commits

Author SHA1 Message Date
Florian Rival
3d4766e5eb Fix issues with npm install and npm version 2022-01-11 11:42:26 +00:00
D8H
a1f0bbf25f Add autocompletion for fields and expressions asking for a function parameter name (#3470) 2022-01-10 21:42:20 +00:00
AlexandreS
8af84bf3a4 Only run auto closes and comments on issue opening (#3468)
Do not show in changelog
2022-01-10 18:48:03 +01:00
Florian Rival
1ebe50a30c Fix npm install on Travis (#3455) 2022-01-10 17:47:03 +00:00
AlexandreS
370e20fc97 Attempt to solve special characters use issue 3 - Remove escaping first set to escape quotes (#3465)
Do not show in changelog
2022-01-10 18:39:12 +01:00
AlexandreS
fb407848be Attempt to solve special characters use issue 3 - Use issue number instead of its id (#3462)
Do not show in changelog
2022-01-10 18:23:30 +01:00
AlexandreS
d2f53edb2c Attempt to solve special characters use issue 3
Do not show in changelog
2022-01-10 18:14:12 +01:00
AlexandreS
34a75a29c7 Improve a few things Bis repetita
Do not show in changelog
2022-01-10 18:06:18 +01:00
AlexandreS
3acd76ff02 Close ExtensionInstallDialog if successfully installed
Do not show in changelog
2022-01-10 16:33:37 +01:00
AlexandreS
0c6ffc23ed Few export fixes (#3452)
Do not show in changelog
2022-01-10 14:30:18 +01:00
Clément Pasteau
323809228a Update Games Platform local URL to use Liluo
Do not show in changelog
2022-01-10 14:08:28 +01:00
github-actions[bot]
145764bfdb Update translations [skip ci] (#3364)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2022-01-09 17:06:17 +00:00
Leo_Red
dba4b7aeb7 Update new behavior method dialog step icon to be hollow (#3441) 2022-01-09 12:59:34 +00:00
AlexandreS
1f19be3ec2 Fixes presence of ghost collision masks for objects with platform behavior
Co-authored-by: D8H <Davy.Helard@gmail.com>
2022-01-08 11:55:33 +00:00
Florian Rival
bf60470c3d Make the GDevelop Windows version signed with a certificate to avoid warnings when opening the installer (#3435)
* The Windows warning should be gone once a sufficient number of people install the app
2022-01-07 23:57:23 +00:00
Florian Rival
2afa702080 Bump newIDE version (testing)
Don't show in changelog
2022-01-07 17:13:33 +00:00
AlexandreS
365bc56940 Change the way one can apply filters when searching for extensions/behaviors/examples in the stores
Co-authored-by: Bouh <bouhvivez@gmail.com>
2022-01-07 15:28:36 +00:00
Clément Pasteau
8aaa3bcbb6 Introduce stable URLs for games
* A unique URL is now available for each game, on liluo.io
* The list of builds has been improved to allow opening web builds and decide which one to publish live
* Web export is now split in 2 steps, providing an instant-build URL, and the stable game URL
2022-01-06 16:45:08 +01:00
Clément Pasteau
90c3195b5e Improve the mouse conditions to autocomplete possible values (#3433) 2022-01-06 10:26:48 +01:00
Aurélien Vivet
ad3c7e4fad Fix typo (#3434)
Do not show in changelog
2022-01-06 10:05:13 +01:00
AlexandreS
ba50c73485 Add possibility to add an extension or a behavior when adding an instruction in the event sheet 2022-01-05 15:36:03 +01:00
AlexandreS
933287ec6b Add notion of origin to extensions (#3429)
Do not show in changelog.
2022-01-04 15:14:52 +01:00
Florian Rival
e2afa946a2 Fix the SerializedExtension type, which is not a type containing the same fields as the extension "headers" coming from the extensions store.
Don't show in changelog
2022-01-04 12:35:20 +01:00
D8H
ae6a77da9f Improve calculations for the Platformer to make it more precise (#3417)
* This makes the movement less prone to approximations when there is a drop in FPS, making your game more precise.
2022-01-03 16:19:17 +01:00
Clément Pasteau
25453b70eb Fix Extension Install dialog being functional to use hooks
Do not show in changelog
2022-01-03 09:51:25 +01:00
D8H
cff585ed55 Improve calculations for the Pathfinding to make it more precise (#3421)
* This makes the movement less prone to approximations when there is a drop in FPS, making your game more precise.
2022-01-02 19:28:03 +01:00
Arthur Pacaud
79a4162ad0 Add logs to audio actions when trying to manipulate a sound/music on a non-existing channel (#3241)
Only show in developer changelog
2022-01-02 14:59:42 +01:00
Florian Rival
9e21cf0a08 Add options and logos to build properly the AppX (#3015)
* Fix wrong executable name because of the space in "GDevelop 5.exe" in the generated appx.
* Use Node.js 16 on Linux CI
* Update version [skip ci]
* Upgrade to electron-builder 22.11.11

Only show in developer changelog
2021-12-30 10:22:43 +01:00
AlexandreS
63332f8123 Add possibility to use Cmd+Enter/Ctrl+Enter to submit a dialog 2021-12-29 11:27:03 +01:00
Florian Rival
ce986fe1d3 Add a test game for shape painter transformations
Don't show in changelog
2021-12-28 10:11:46 +01:00
D8H
439d185ce8 Add a condition to the Platformer character behavior to check if a control is pressed (or simulated) (#3406) 2021-12-28 10:09:04 +01:00
AlexandreS
ec42219d2f Remove tendentious adjective from project name generator
Do not show in changelog
2021-12-28 10:06:47 +01:00
D8H
fa5671a3ee Improve calculations for the Top Down movement behavior to make it more precise when accelerating or decelerating (#3412)
* This makes the movement less prone to approximations when there is a drop in FPS, making your game more precise.
2021-12-28 09:57:10 +01:00
Clément Pasteau
399c4c5edd Make Builds list consistent across the app
Do not show in changelog
2021-12-27 19:37:26 +01:00
Clément Pasteau
3cc3f612e6 Display the list of builds per game in the game dashboard 2021-12-27 14:42:03 +01:00
Clément Pasteau
3e1799dddb Send gameId when creating a build
Do not show in changelog
2021-12-27 09:14:00 +01:00
D8H
eb6628af49 Add 2 tests for Platformer jump at 30 fps and 120 fps. (#3408)
Only show in developer changelog
2021-12-27 09:03:04 +01:00
D8H
fc6082c35b Add support for rotating, scaling and flipping a Shape Painter object (#3402)
* A custom center point can be set (and will be at the center of the drawing if not specified).
* The collision mask is now a rectangle surrounding what is drawn on screen. 
* If this changes something in your game, or you want another collision mask, you can use the action to set a custom rectangle as the collision mask of the object.
2021-12-26 22:13:51 +00:00
D8H
819ffc52c7 Show objects position instead of origin in the in-game debugging view (#3403)
* Still show the "origin" point for rotated/scaled Sprite objects.
2021-12-25 18:51:57 +01:00
Arthur Pacaud
f36c9940ed Fix the special 'Not' condition not working with some conditions related to objects (#3367) 2021-12-24 13:36:58 +01:00
Florian Rival
cd8901a524 Fix typo 2021-12-24 10:56:22 +01:00
Aurélien Vivet
5693b257c0 Fix the expression to get the user home path (#3400) 2021-12-23 15:14:42 +01:00
Clément Pasteau
48467e4654 Add missing keys in the keyboard expression autocomplete 2021-12-23 11:09:10 +01:00
Rahul Saini
78dfedf66b Allow multi-selection and sorting in the instances list (#3343)
* Also allow to lock/unlock by clicking on the lock icon
2021-12-23 11:08:53 +01:00
AlexandreS
d3ef6fe729 Add context menu items to collapse all events and expand events to certain levels (#3396) 2021-12-23 09:49:08 +01:00
D8H
b3e0540fed Group properties in some behaviors for enhanced clarity (#3373)
* Also allow custom behaviors made in the editor to define property groups.
2021-12-22 17:10:06 +00:00
Clément Pasteau
510d8d7c1d Fix user profile being refreshed too often when building
Do not show in changelog
2021-12-22 10:13:47 +01:00
AlexandreS
650975ba6e Improve homepage layout and Create project dialog options (#3386) 2021-12-21 14:28:13 +01:00
Aurélien Vivet
6bf293bcb5 Add backdrop click on create new project dialog
Don't show in changelog
2021-12-21 04:15:52 +01:00
D8H
dac4b3ba51 Improve clarity of code (#3375)
Don't show in changelog
2021-12-17 16:38:37 +01:00
D8H
b344f5b956 Allow actions/conditions/expressions created in the editor to be sorted in groups (#3366) 2021-12-17 15:43:58 +01:00
AlexandreS
44db5362d3 Add notification badge on user chip and achievements 2021-12-17 10:59:11 +01:00
Clément Pasteau
cca0e6e66f Fix toolbar not centered on the extensions screen (#3374) 2021-12-17 10:37:11 +01:00
Arthur Pacaud
81c65f7ff7 Fix wrong network preview address shown when running in VirtualBox/VMware (#3368) 2021-12-16 10:20:58 +01:00
Clément Pasteau
b3ea46d7e6 Fix sharing buttons
Do not show in changelog
2021-12-15 13:57:25 +01:00
github-actions[bot]
0e8adaab92 Update translations 2021-12-15 13:01:35 +01:00
D8H
c055fbcb3c Split the Platformer tests in 5 files (#3360)
Only show in developer changelog
2021-12-15 11:57:43 +01:00
AlexandreS
5051de0787 Add Tutorial Opened event when clicking on tutorial on homepage
Do not show in changelog
2021-12-15 11:22:32 +01:00
Clément Pasteau
4976d8ef8b Bump IDE version
Do not show in changelog
2021-12-15 10:48:30 +01:00
Clément Pasteau
bf1ffd3e65 Improve layer visibility toggle with an explanation text 2021-12-15 10:44:52 +01:00
Clément Pasteau
9163e998f9 Improve web export with multiple sharing capabilities 2021-12-15 10:25:41 +01:00
AlexandreS
a4d0c591a8 Add possibility to choose project name before it is created
A random and fun name is generated as a suggestion
2021-12-14 17:51:26 +01:00
Florian Rival
8c717ba910 Fix videos wrongly removed removing unused resources in the resources editor
Fix #3356
2021-12-14 10:15:55 +01:00
Leo_Red
be0f760f02 Add minor UI improvements to the profile dialog (#3351) 2021-12-12 11:45:14 +00:00
Florian Rival
919d596d07 Move the copy button for the web export link to the end of the input (#3350)
Also use a component doing the layout of the button on the right.

Don't show in changelog
2021-12-11 13:06:39 +00:00
AlexandreS
35cfd627ad Clean up after new homepage
Do not show in changelog
2021-12-10 16:24:06 +01:00
D8H
ba687aa60c Allow grid based object to optimise collision checks (#3245)
* Allow to get the hit boxes for a given area.
* Also remove useless array and wrong sharing of vertices in Light object renderers

Only show in developer changelog
2021-12-10 15:16:12 +00:00
Clément Pasteau
4d8e835b9a Fix light textures sometimes not behaving properly when close to an obstacle 2021-12-10 14:45:02 +01:00
Clément Pasteau
834a28ddbc Improve the whole export flow
* The Preview & Publish buttons are now centred in the interface
* The automated web upload with a provided link is put forward in the Export home dialog
* The export flow has been reworked to be split into "Automated" and "Manual'
2021-12-10 12:36:48 +01:00
AlexandreS
945555a8e9 Fix switch case condition in showcased game buttons
Don't show in changelog
2021-12-10 10:32:05 +00:00
AlexandreS
ad3d1dd8c3 Change homepage to display starters, tutorials and the game showcase
Additionally:
- Add button with split menu to open recent projects
- Remove starters tab in the dialog to create a project
2021-12-09 17:46:52 +01:00
Florian Rival
fd47282456 Improve resilience of resources fetching of the desktop app (#3342)
Do more requests at the same time but retry the failing ones.

Don't show in changelog
2021-12-08 09:54:20 +00:00
Florian Rival
dff1c88ef7 Increase parallel requests when fetching resources in the desktop app
Don't show in changelog
2021-12-07 22:35:27 +00:00
Florian Rival
4ea622ff99 Fix typo 2021-12-05 13:04:40 +00:00
306 changed files with 19648 additions and 8135 deletions

View File

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

View File

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

View File

@@ -17,10 +17,6 @@ cache:
directories:
- $HOME/.npm
env:
global:
- GCC_VERSION="4.8"
services:
# Virtual Framebuffer 'fake' X server for SFML
- xvfb
@@ -40,7 +36,6 @@ addons:
# Build dependencies:
- cmake
- p7zip-full
- g++-4.8
# SFML dependencies:
- libopenal-dev
- libjpeg-dev
@@ -60,28 +55,24 @@ before_install:
- sudo dpkg --force-all -i libstdc++6
install:
#Get the correct version of gcc/g++
- if [ "$CXX" = "g++" ]; then export CXX="g++-${GCC_VERSION}" CC="gcc-${GCC_VERSION}"; fi
# Ensure we use a recent version of Node.js (and npm).
- nvm install v16 && nvm use v16
#Compile the tests only for GDCore
- mkdir .build-tests
- cd .build-tests
- cmake -DBUILD_GDJS=FALSE -DBUILD_TESTS=TRUE -DCMAKE_CXX_COMPILER=$(which $CXX) -DCMAKE_C_COMPILER=$(which $CC) ..
- make -j 4
- cd ..
#- mkdir .build-tests
#- cd .build-tests
#- cmake -DBUILD_GDJS=FALSE -DBUILD_TESTS=TRUE -DCMAKE_CXX_COMPILER=$(which $CXX) -DCMAKE_C_COMPILER=$(which $CC) ..
#- make -j 4
#- cd ..
# Install Emscripten (for GDevelop.js)
- git clone https://github.com/juj/emsdk.git
- cd emsdk
- ./emsdk install 1.39.6
- ./emsdk activate 1.39.6
- source ./emsdk_env.sh
- cd ..
# Install GDevelop.js dependencies and compile it
- cd GDevelop.js
- npm install -g grunt-cli
- npm install
- npm run build
- cd ..
- cd emsdk && ./emsdk install 1.39.6 && ./emsdk activate 1.39.6 && cd ..
# Install GDevelop.js dependencies
- cd GDevelop.js && npm install && cd ..
# Build GDevelop.js
- cd GDevelop.js && source ../emsdk/emsdk_env.sh && npm run build && cd ..
- nvm use v16 # Restore latest Node.js version.
#Install newIDE tests dependencies
- npm -v
- cd newIDE/app && npm install
- cd ../..
#Install GDJS tests dependencies

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,16 @@ void ArbitraryResourceWorker::ExposeImage(gd::String& imageName){
// do.
};
void ArbitraryResourceWorker::ExposeJson(gd::String& jsonName){
// Nothing to do by default - each child class can define here the action to
// do.
};
void ArbitraryResourceWorker::ExposeVideo(gd::String& videoName){
// Nothing to do by default - each child class can define here the action to
// do.
};
void ArbitraryResourceWorker::ExposeBitmapFont(gd::String& bitmapFontName){
// Nothing to do by default - each child class can define here the action to
// do.

View File

@@ -70,6 +70,16 @@ class GD_CORE_API ArbitraryResourceWorker {
*/
virtual void ExposeFont(gd::String &fontName);
/**
* \brief Expose a JSON, which is always a reference to a "json" resource.
*/
virtual void ExposeJson(gd::String &jsonName);
/**
* \brief Expose a video, which is always a reference to a "video" resource.
*/
virtual void ExposeVideo(gd::String &videoName);
/**
* \brief Expose a bitmap font, which is always a reference to a "bitmapFont" resource.
*/

View File

@@ -10,6 +10,7 @@
#include <set>
#include <vector>
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
#include "GDCore/String.h"
@@ -36,17 +37,20 @@ class ResourcesInUseHelper : public gd::ArbitraryResourceWorker {
virtual ~ResourcesInUseHelper(){};
std::set<gd::String>& GetAllImages() { return GetAll("image"); };
std::set<gd::String>& GetAllFonts() { return GetAll("font"); };
std::set<gd::String>& GetAllAudios() { return GetAll("audio"); };
std::set<gd::String>& GetAllFonts() { return GetAll("font"); };
std::set<gd::String>& GetAllJsons() { return GetAll("json"); };
std::set<gd::String>& GetAllVideos() { return GetAll("video"); };
std::set<gd::String>& GetAllBitmapFonts() { return GetAll("bitmapFont"); };
std::set<gd::String>& GetAll(const gd::String& resourceType) {
return resourceType == "image"
? allImages
: (resourceType == "audio"
? allAudios
: (resourceType == "font")
? allFonts
: (resourceType == "bitmapFont") ? allBitmapFonts : emptyResources);
if (resourceType == "image") return allImages;
if (resourceType == "audio") return allAudios;
if (resourceType == "font") return allFonts;
if (resourceType == "json") return allJsons;
if (resourceType == "video") return allVideos;
if (resourceType == "bitmapFont") return allBitmapFonts;
return emptyResources;
};
virtual void ExposeFile(gd::String& resource) override{
@@ -61,6 +65,12 @@ class ResourcesInUseHelper : public gd::ArbitraryResourceWorker {
virtual void ExposeFont(gd::String& fontResourceName) override {
allFonts.insert(fontResourceName);
};
virtual void ExposeJson(gd::String& jsonResourceName) override {
allJsons.insert(jsonResourceName);
};
virtual void ExposeVideo(gd::String& videoResourceName) override {
allVideos.insert(videoResourceName);
};
virtual void ExposeBitmapFont(gd::String& bitmapFontResourceName) override {
allBitmapFonts.insert(bitmapFontResourceName);
};
@@ -69,6 +79,8 @@ class ResourcesInUseHelper : public gd::ArbitraryResourceWorker {
std::set<gd::String> allImages;
std::set<gd::String> allAudios;
std::set<gd::String> allFonts;
std::set<gd::String> allJsons;
std::set<gd::String> allVideos;
std::set<gd::String> allBitmapFonts;
std::set<gd::String> emptyResources;
};

View File

@@ -46,6 +46,12 @@ class ResourcesRenamer : public gd::ArbitraryResourceWorker {
virtual void ExposeFont(gd::String& fontResourceName) override {
RenameIfNeeded(fontResourceName);
};
virtual void ExposeJson(gd::String& jsonResourceName) override {
RenameIfNeeded(jsonResourceName);
};
virtual void ExposeVideo(gd::String& videoResourceName) override {
RenameIfNeeded(videoResourceName);
};
virtual void ExposeBitmapFont(gd::String& bitmapFontName) override {
RenameIfNeeded(bitmapFontName);
};

View File

@@ -17,6 +17,7 @@ void EventsFunction::SerializeTo(SerializerElement& element) const {
element.SetAttribute("fullName", fullName);
element.SetAttribute("description", description);
element.SetAttribute("sentence", sentence);
element.SetAttribute("group", group);
element.SetBoolAttribute("private", isPrivate);
events.SerializeTo(element.AddChild("events"));
@@ -44,6 +45,7 @@ void EventsFunction::UnserializeFrom(gd::Project& project,
fullName = element.GetStringAttribute("fullName");
description = element.GetStringAttribute("description");
sentence = element.GetStringAttribute("sentence");
group = element.GetStringAttribute("group");
isPrivate = element.GetBoolAttribute("private");
events.UnserializeFrom(project, element.GetChild("events"));

View File

@@ -102,6 +102,19 @@ class GD_CORE_API EventsFunction {
return *this;
}
/**
* \brief Get the group of the instruction in the editor.
*/
const gd::String& GetGroup() const { return group; };
/**
* \brief Set the group of the instruction in the editor.
*/
EventsFunction& SetGroup(const gd::String& group_) {
group = group_;
return *this;
}
enum FunctionType { Action, Condition, Expression, StringExpression };
/**
@@ -188,6 +201,7 @@ class GD_CORE_API EventsFunction {
gd::String fullName;
gd::String description;
gd::String sentence;
gd::String group;
gd::EventsList events;
FunctionType functionType;
std::vector<gd::ParameterMetadata> parameters;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -179,7 +179,8 @@ module.exports = {
.setValue(
behaviorContent.getBoolAttribute('property2') ? 'true' : 'false'
)
.setType('Boolean');
.setType('Boolean')
.setGroup(_('Look and Feel'));
return behaviorProperties;
};

View File

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

View File

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

View File

@@ -87,12 +87,16 @@ namespace gdjs {
_manager: any;
_registeredInManager: boolean = false;
constructor(runtimeScene, behaviorData, owner) {
constructor(
runtimeScene: gdjs.RuntimeScene,
behaviorData,
owner: gdjs.RuntimeObject
) {
super(runtimeScene, behaviorData, owner);
this._manager = LightObstaclesManager.getManager(runtimeScene);
}
doStepPreEvents(runtimeScene) {
doStepPreEvents(runtimeScene: gdjs.RuntimeScene) {
// Make sure the obstacle is or is not in the obstacles manager.
if (!this.activated() && this._registeredInManager) {
this._manager.removeObstacle(this);

View File

@@ -8,19 +8,24 @@ namespace gdjs {
export class LightRuntimeObjectPixiRenderer {
_object: gdjs.LightRuntimeObject;
_runtimeScene: gdjs.RuntimeScene;
_manager: any;
_manager: gdjs.LightObstaclesManager;
_radius: number;
_color: any;
_color: [number, number, number];
_texture: PIXI.Texture | null = null;
_center: any;
_defaultVertexBuffer: any;
_vertexBuffer: any;
_indexBuffer: any;
_center: Float32Array;
_defaultVertexBuffer: Float32Array;
_vertexBuffer: Float32Array;
_indexBuffer: Uint16Array;
_light: PIXI.Mesh<PIXI.Shader> | null = null;
_isPreview: boolean;
_debugMode: any = null;
_debugMode: boolean = false;
_debugLight: PIXI.Container | null = null;
_debugGraphics: PIXI.Graphics | null = null;
/**
* A polygon updated when vertices of the light are computed
* to be a polygon bounding the light and its obstacles.
*/
_lightBoundingPoly: gdjs.Polygon;
constructor(
@@ -53,13 +58,8 @@ namespace gdjs {
this._indexBuffer = new Uint16Array([0, 1, 2, 0, 2, 3]);
this.updateMesh();
this._isPreview = runtimeScene.getGame().isPreview();
this._lightBoundingPoly = gdjs.Polygon.createRectangle(0, 0);
this._lightBoundingPoly = new gdjs.Polygon();
for (let i = 0; i < 4; i++) {
this._lightBoundingPoly.vertices.push(
runtimeObject.getHitBoxes()[0].vertices[i]
);
}
this.updateDebugMode();
// Objects will be added in lighting layer, this is just to maintain consistency.
@@ -85,10 +85,10 @@ namespace gdjs {
}
static _computeClosestIntersectionPoint(
lightObject,
angle,
polygons,
boundingSquareHalfDiag
lightObject: gdjs.LightRuntimeObject,
angle: float,
polygons: Array<gdjs.Polygon>,
boundingSquareHalfDiag: float
) {
const centerX = lightObject.getX();
const centerY = lightObject.getY();
@@ -308,8 +308,8 @@ namespace gdjs {
// and instead use a subarray. Otherwise, allocate new array buffers as
// there would be memory wastage.
let isSubArrayUsed = false;
let vertexBufferSubArray = null;
let indexBufferSubArray = null;
let vertexBufferSubArray: Float32Array | null = null;
let indexBufferSubArray: Uint16Array | null = null;
if (this._vertexBuffer.length > 2 * verticesCount + 2) {
if (this._vertexBuffer.length < 4 * verticesCount + 4) {
isSubArrayUsed = true;
@@ -368,7 +368,7 @@ namespace gdjs {
* Computes the vertices of mesh using raycasting.
* @returns the vertices of mesh.
*/
_computeLightVertices(): Array<any> {
_computeLightVertices(): Array<FloatPoint> {
const lightObstacles: gdjs.BehaviorRBushAABB<
LightObstacleRuntimeBehavior
>[] = [];
@@ -379,38 +379,47 @@ namespace gdjs {
lightObstacles
);
}
const searchAreaLeft = this._object.getX() - this._radius;
const searchAreaTop = this._object.getY() - this._radius;
const searchAreaRight = this._object.getX() + this._radius;
const searchAreaBottom = this._object.getY() + this._radius;
// Bail out early if there are no obstacles.
if (lightObstacles.length === 0) {
// @ts-ignore TODO the array should probably be pass as a parameter.
return lightObstacles;
}
// Synchronize light bounding polygon with the hitbox.
const lightHitboxPoly = this._object.getHitBoxes()[0];
// Note: we suppose the hitbox is always a single rectangle.
const objectHitBox = this._object.getHitBoxes()[0];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 2; j++) {
this._lightBoundingPoly.vertices[i][j] =
lightHitboxPoly.vertices[i][j];
}
this._lightBoundingPoly.vertices[i][0] = objectHitBox.vertices[i][0];
this._lightBoundingPoly.vertices[i][1] = objectHitBox.vertices[i][1];
}
const obstaclesCount = lightObstacles.length;
const obstacleHitBoxes = new Array(obstaclesCount);
for (let i = 0; i < obstaclesCount; i++) {
obstacleHitBoxes[i] = lightObstacles[i].behavior.owner.getHitBoxes();
}
const obstaclePolygons: Array<any> = [];
// Create the list of polygons to compute the light vertices
const obstaclePolygons: Array<gdjs.Polygon> = [];
obstaclePolygons.push(this._lightBoundingPoly);
for (let i = 0; i < obstaclesCount; i++) {
const noOfHitBoxes = obstacleHitBoxes[i].length;
for (let j = 0; j < noOfHitBoxes; j++) {
obstaclePolygons.push(obstacleHitBoxes[i][j]);
for (let i = 0; i < lightObstacles.length; i++) {
const obstacleHitBoxes = lightObstacles[
i
].behavior.owner.getHitBoxesAround(
searchAreaLeft,
searchAreaTop,
searchAreaRight,
searchAreaBottom
);
for (const hitbox of obstacleHitBoxes) {
obstaclePolygons.push(hitbox);
}
}
let maxX = this._object.x + this._radius;
let minX = this._object.x - this._radius;
let maxY = this._object.y + this._radius;
let minY = this._object.y - this._radius;
const flattenVertices: Array<any> = [];
const flattenVertices: Array<FloatPoint> = [];
for (let i = 1; i < obstaclePolygons.length; i++) {
const vertices = obstaclePolygons[i].vertices;
const verticesCount = vertices.length;
@@ -452,6 +461,7 @@ namespace gdjs {
(maxY - this._object.y) * (maxY - this._object.y)
)
);
// Add this._object.hitBoxes vertices.
for (let i = 0; i < 4; i++) {
flattenVertices.push(obstaclePolygons[0].vertices[i]);
}
@@ -546,9 +556,11 @@ namespace gdjs {
varying vec2 vPos;
void main() {
vec2 topleft = vec2(center.x - radius, center.y - radius);
vec2 texCoord = (vPos - topleft)/(2.0 * radius);
gl_FragColor = vec4(color, 1.0) * texture2D(uSampler, texCoord);
vec2 topleft = vec2(center.x - radius, center.y - radius);
vec2 texCoord = (vPos - topleft)/(2.0 * radius);
gl_FragColor = (texCoord.x > 0.0 && texCoord.x < 1.0 && texCoord.y > 0.0 && texCoord.y < 1.0)
? vec4(color, 1.0) * texture2D(uSampler, texCoord)
: vec4(0.0, 0.0, 0.0, 0.0);
}`;
}

View File

@@ -149,10 +149,10 @@ namespace gdjs {
}
/**
* Get the light obstacles manager if objects with the behavior exist, null otherwise.
* @returns gdjs.LightObstaclesManager if it exists, otherwise null.
* Get the light obstacles manager.
* @returns the light obstacles manager.
*/
getObstaclesManager(): gdjs.LightObstaclesManager | null {
getObstaclesManager(): gdjs.LightObstaclesManager {
return this._obstaclesManager;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior Benchmark', function () {
const stepCount = 6000;
beforeEach(function () {
runtimeScene = makeTestRuntimeScene();
runtimeScene = makePlatformerTestRuntimeScene();
objects = new Array(duplicateCount);
for (let i = 0; i < duplicateCount; ++i) {

View File

@@ -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 ||
@@ -770,7 +833,12 @@ namespace gdjs {
return context;
}
for (const hitbox of platformObject.getHitBoxes()) {
for (const hitbox of platformObject.getHitBoxesAround(
context.ownerMinX,
context.headMinY,
context.ownerMaxX,
context.floorMaxY
)) {
if (hitbox.vertices.length < 3) {
continue;
}
@@ -856,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
)
) {
@@ -909,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
)
) {
@@ -930,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
)
) {
@@ -947,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;
}
}
@@ -960,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
);
@@ -970,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++;
@@ -1002,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.
@@ -1785,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 &&
@@ -1806,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();
}

View File

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

View File

@@ -0,0 +1,783 @@
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
const epsilon = 1 / (2 << 16);
[0, 60].forEach((slopeMaxAngle) => {
describe(`(walk on flat floors, slopeMaxAngle: ${slopeMaxAngle}°)`, function () {
let runtimeScene;
let object;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// 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: slopeMaxAngle,
jumpSustainTime: 0.2,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
});
const fall = (frameCount) => {
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.above(lastY);
}
};
const walkRight = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.be.above(lastX);
// Check that the object doesn't stop
expect(behavior.getCurrentSpeed()).to.be.above(lastSpeed);
}
};
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);
};
const slopesDimensions = {
26: { width: 50, height: 25 },
45: { width: 50, height: 50 },
};
it('can walk from a platform to another one', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
const platform2 = addPlatformObject(runtimeScene);
platform2.setPosition(
platform.getX() + platform.getWidth(),
platform.getY()
);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(platform2.getX());
expect(object.getY()).to.be(platform2.getY() - object.getHeight());
});
it('can walk from a platform to a jump through', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
const jumpThroughPlatform = addJumpThroughPlatformObject(runtimeScene);
jumpThroughPlatform.setPosition(
platform.getX() + platform.getWidth(),
platform.getY()
);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(jumpThroughPlatform.getX());
expect(object.getY()).to.be(
jumpThroughPlatform.getY() - object.getHeight()
);
});
it('can walk on a platform and go through a jump through', function () {
// Jumpthru that are ignored had a side effects on the search context.
// It made jumpthru appear solid when a platform was tested after them.
// Add the jumptru 1st to make RBrush gives it 1st.
// There is no causality but it does in the current implementation.
const jumpThroughPlatform = addJumpThroughPlatformObject(runtimeScene);
jumpThroughPlatform.setPosition(30, -15);
jumpThroughPlatform.setCustomWidthAndHeight(60, 10);
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
object.setPosition(10, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Walk from the 1st platform to the 2nd one.
walkRight(20);
expect(object.getX()).to.be.above(jumpThroughPlatform.getX());
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
it('can walk from a platform to another one that not aligned', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
const platform2 = addPlatformObject(runtimeScene);
platform2.setPosition(
platform.getX() + platform.getWidth(),
// the 2nd platform is 1 pixel higher
platform.getY() - 1
);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(platform2.getX());
expect(object.getY()).to.be(platform2.getY() - object.getHeight());
});
it('can walk from a platform to another one with a speed under 1 pixel/second', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
const platform2 = addPlatformObject(runtimeScene);
platform2.setPosition(
platform.getX() + platform.getWidth(),
// The 2nd platform is 1 pixels higher.
platform.getY() - 1
);
// Put the object just to the left of platform2 so that
// it try climbing on it with a very small speed.
object.setPosition(platform2.getX() - object.getWidth(), -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(platform2.getX());
expect(object.getY()).to.be(platform2.getY() - object.getHeight());
});
it("can't walk from a platform to another one that is a bit too high", function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
const platform2 = addPlatformObject(runtimeScene);
platform2.setPosition(
platform.getX() + platform.getWidth(),
// The 2nd platform is 2 pixels higher.
platform.getY() - 2
);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// walk right
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
}
// is blocked by the 2nd platform
expect(object.getX()).to.be(platform2.getX() - object.getWidth());
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
it('can walk on a platform and be blocked by a wall', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
// the 2nd platform is 2 pixels higher
const platform2 = addPlatformObject(runtimeScene);
platform2.setPosition(
platform.getX() + platform.getWidth(),
// The platform's top is over the object
// and platform's bottom is under the object.
platform.getY() - platform2.getHeight() + 5
);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// walk right
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
}
// is blocked by the 2nd platform
expect(object.getX()).to.be(platform2.getX() - object.getWidth());
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
it('can walk from a platform and fell through a jump through that is at the right but 1 pixel higher', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
const jumpThroughPlatform = addJumpThroughPlatformObject(runtimeScene);
jumpThroughPlatform.setPosition(
platform.getX() + platform.getWidth(),
// Even 1 pixel is too high to follow a jump through
// because it's like it's gone through its right or left side.
platform.getY() - 1
);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Walk right
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
}
// Fall under the jump through platform
for (let i = 0; i < 11; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
}
expect(object.getY()).to.be.above(platform.getY());
});
it('can walk inside a tunnel platform', function () {
// Put a platform.
const platform = addTunnelPlatformObject(runtimeScene);
platform.setPosition(0, 0);
object.setPosition(0, 160);
// The object falls on the bottom part of the platform
fallOnPlatform(10);
expect(object.getY()).to.be(200 - object.getHeight());
// The object walk on the bottom part of the platform.
walkRight(30);
expect(object.getX()).to.be.above(60);
expect(object.getY()).to.be(200 - object.getHeight());
});
});
});
[
// less than 1 pixel per frame (50/60)
50,
// a commonly used value
1500,
].forEach((maxFallingSpeed) => {
describe(`(on floor, maxFallingSpeed=${
maxFallingSpeed / 60
} pixels per frame)`, function () {
let runtimeScene;
let object;
let platform;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object in the air.
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: maxFallingSpeed,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 1500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -100);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
});
// TODO The character falls one frame then land instead of staying on the platform.
it.skip('must not move when on the floor at startup', function () {
object.setPosition(0, platform.getY() - object.getHeight());
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
// Check the platformer object stays still.
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);
}
});
it('must not move when put on a platform while falling', function () {
object.setPosition(0, platform.getY() - object.getHeight() - 300);
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
);
}
object.setPosition(0, platform.getY() - object.getHeight());
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
// Check the platformer object stays still.
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);
}
});
it('can track object height changes', function () {
// Put the character near the right ledge of the platform.
object.setPosition(
platform.getX() + 10,
platform.getY() - object.getHeight() + 1
);
for (let i = 0; i < 15; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getX()).to.be(10);
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);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
expect(object.getY()).to.be(-19);
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)
});
it('can track platform angle changes', function () {
// The initial pltaforms AABB are put in RBush.
runtimeScene.renderAndStep(1000 / 60);
// Now change the angle to check that the AABB is updated in RBush.
platform.setAngle(90);
// Put the character above the rotated platform.
object.setPosition(
platform.getX() + platform.getWidth() / 2,
platform.getY() +
(platform.getHeight() - platform.getWidth()) / 2 -
object.getHeight() -
10
);
for (let i = 0; i < 15; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// The character should land on it.
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getX()).to.be(30);
expect(object.getY()).to.be(-44);
});
});
});
describe(`(walk on flat floors with custom hitbox)`, function () {
let runtimeScene;
let object;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform
object = new gdjs.TestSpriteRuntimeObject(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,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 25, y: 25 },
centerPoint: { x: 50, y: 50 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 50, y: 50 },
],
hasCustomCollisionMask: true,
customCollisionMask: [
[
{ x: 25, y: 25 },
{ x: 75, y: 25 },
{ x: 75, y: 75 },
{ x: 25, y: 75 },
],
],
},
],
},
],
},
],
});
object.setUnscaledWidthAndHeight(100, 100);
object.setCustomWidthAndHeight(20, 40);
runtimeScene.addObject(object);
});
// The actual hitbox is 10x20.
const objectWidth = 10;
const objectHeight = 20;
const fall = (frameCount) => {
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.above(lastY);
}
};
const walkRight = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.be.above(lastX);
// Check that the object doesn't stop
expect(behavior.getCurrentSpeed()).to.be.above(lastSpeed);
}
};
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 on a platform and be blocked by a wall', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
// the 2nd platform is 2 pixels higher
const wall = addPlatformObject(runtimeScene);
wall.setPosition(
platform.getX() + platform.getWidth(),
// The platform is top is over the object
// and platform is bottom is under the object.
platform.getY() - wall.getHeight() + 5
);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// walk right
for (let i = 0; i < 25; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
}
// is blocked by the wall
expect(object.getX()).to.be(wall.getX() - objectWidth);
expect(object.getY()).to.be(platform.getY() - objectHeight);
});
});
describe('Floating-point error mitigations', function () {
it('Specific coordinates with slopeMaxAngle=0 creating Y oscillations and drift on a moving floor', function () {
const runtimeScene = makePlatformerTestRuntimeScene();
// Create a Sprite object that has the origin at a specific position (see below)
// and that has a slope max angle of 0 (so it can't climb on a floor even if it's a bit higher
// than the bottom of the object).
const object = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 1300,
maxFallingSpeed: 1000,
acceleration: 500,
deceleration: 1500,
maxSpeed: 280,
jumpSpeed: 750,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 0,
jumpSustainTime: 0.2,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 5, y: 19 },
centerPoint: { x: 5, y: 46 },
points: [
{ name: 'Center', x: 5, y: 46 },
{ name: 'Origin', x: 5, y: 19 },
],
hasCustomCollisionMask: false,
},
],
},
],
},
],
});
// Set the size of the object so that it results in a specific
// Y position for the bottom of the object AABB:
object.setUnscaledWidthAndHeight(10, 92);
object.setCustomWidthAndHeight(10, 66.0008);
// Origin Y is originally 19.
// After the scaling, it is now 19*66.0008/92=13.6306.
// Set the Y position so that the object falls at a Y position on the floor
// that would generate oscillations.
object.setPosition(0, 139.3118);
runtimeScene.addObject(object);
// Put a platform at a specific Y that can cause oscillations.
const platform = addJumpThroughPlatformObject(runtimeScene);
platform.setPosition(0, 193.000000000001);
// This means that the exact Y position the object should take is:
// platform Y - height + origin Y = 193.000000000001-66.0008+13.6306 = 140.6298
// Wait for the object to fall on the floor
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isOnFloor()).to.be(false);
// Ensure it is on the floor
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
// The Y position won't be exact because of floating point errors.
// expect(object.getY()).to.be(140.6298)
expect(object.getY()).to.be.within(140.6297999, 140.6298001);
// Move the platform by 6 pixels to the right.
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
// Ensure the object followed the platform on the X axis.
// If the floating point errors caused oscillations between two Y positions,
// it won't work because the object will get repositioned back to its old X position
// whenever the floor is considered "too high" for the object to reach.
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getY()).to.be.within(140.6297999, 140.6298001);
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);
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,556 @@
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
describe('(grab platforms)', function () {
let runtimeScene;
let object;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object in the air.
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 1500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -100);
});
it('can grab, and release, a platform', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
runtimeScene.renderAndStep(1000 / 60);
// Put the character near the right ledge of the platform.
object.setPosition(
platform.getX() + platform.getWidth() + 2,
platform.getY() - 10
);
for (let i = 0; i < 35; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// 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);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
}
expect(object.getY()).to.be.above(0);
});
[true, false].forEach((addTopPlatformFirst) => {
it('can grab every platform when colliding 2', function () {
// The 2 platforms will be simultaneously in collision
// with the object when it grabs one.
let upperPlatform, lowerPlatform;
if (addTopPlatformFirst) {
upperPlatform = addPlatformObject(runtimeScene);
upperPlatform.setPosition(0, -10);
upperPlatform.setCustomWidthAndHeight(60, 10);
lowerPlatform = addPlatformObject(runtimeScene);
lowerPlatform.setPosition(0, 0);
lowerPlatform.setCustomWidthAndHeight(60, 10);
} else {
lowerPlatform = addPlatformObject(runtimeScene);
lowerPlatform.setPosition(0, 0);
lowerPlatform.setCustomWidthAndHeight(60, 10);
upperPlatform = addPlatformObject(runtimeScene);
upperPlatform.setPosition(0, -10);
upperPlatform.setCustomWidthAndHeight(60, 10);
}
// Put the object near the right ledge of the platform.
object.setPosition(
upperPlatform.getX() + upperPlatform.getWidth() + 2,
upperPlatform.getY() - 10
);
runtimeScene.renderAndStep(1000 / 60);
for (let i = 0; i < 35; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check that the object grabbed the upper platform
expect(object.getX()).to.be.within(
upperPlatform.getX() + upperPlatform.getWidth() + 0,
upperPlatform.getX() + upperPlatform.getWidth() + 1
);
expect(object.getY()).to.be(upperPlatform.getY());
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(true);
// Release upper platform
object.getBehavior('auto1').simulateReleasePlatformKey();
for (let i = 0; i < 35; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check that the object grabbed the lower platform
expect(object.getX()).to.be.within(
lowerPlatform.getX() + lowerPlatform.getWidth() + 0,
lowerPlatform.getX() + lowerPlatform.getWidth() + 1
);
expect(object.getY()).to.be(lowerPlatform.getY());
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(true);
});
});
it('can grab a platform and jump', function () {
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
runtimeScene.renderAndStep(1000 / 60);
//Put the object near the right ledge of the platform.
object.setPosition(
platform.getX() + platform.getWidth() + 2,
platform.getY() - 10
);
for (let i = 0; i < 35; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
//Check that the object grabbed the platform
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(true);
expect(object.getX()).to.be.within(
platform.getX() + platform.getWidth() + 0,
platform.getX() + platform.getWidth() + 1
);
expect(object.getY()).to.be(platform.getY());
object.getBehavior('auto1').simulateJumpKey();
//Check that the object is jumping
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
}
expect(object.getY()).to.be.below(platform.getY());
});
});
describe('(ladder)', function () {
let runtimeScene;
let object;
var scale;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// 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,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
ladder = addLadderObject(runtimeScene);
ladder.setPosition(30, -10 - ladder.getHeight());
});
const fall = (frameCount) => {
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.above(lastY);
}
};
const climbLadder = (frameCount) => {
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
object.getBehavior('auto1').simulateUpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.below(lastY);
}
};
const releaseLadder = (frameCount) => {
object.getBehavior('auto1').simulateReleaseLadderKey();
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.above(lastY);
}
};
const stayOnLadder = (frameCount) => {
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
expect(object.getY()).to.be(lastY);
}
};
const jumpAndAscend = (frameCount) => {
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
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
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.below(lastY);
}
};
const jumpAndDescend = (frameCount) => {
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
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
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.above(lastY);
}
};
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.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
};
it('can climb and release a ladder', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Climb the ladder
object.getBehavior('auto1').simulateLadderKey();
climbLadder(10);
stayOnLadder(10);
const objectPositionAfterFirstClimb = object.getY();
releaseLadder(10);
object.getBehavior('auto1').simulateLadderKey();
expect(object.getY()).to.be.within(
// gravity is 1500, 10 frames falling ~ 21px
objectPositionAfterFirstClimb + 20,
objectPositionAfterFirstClimb + 21
);
climbLadder(23);
// Check that we reached the maximum height
const playerAtLadderTop = ladder.getY() - object.getHeight();
expect(object.getY()).to.be.within(
playerAtLadderTop - 3,
playerAtLadderTop
);
// The player goes a little over the ladder...
object.getBehavior('auto1').simulateUpKey();
// ...and it falls even if up is pressed
for (let i = 0; i < 13; ++i) {
object.getBehavior('auto1').simulateUpKey();
fall(1);
}
});
it('can jump and grab a ladder even on the ascending phase of a jump the 1st time', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Jump
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
for (let i = 0; i < 2; ++i) {
object.getBehavior('auto1').simulateUpKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.below(-30);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
// Grab the ladder
object.getBehavior('auto1').simulateLadderKey();
runtimeScene.renderAndStep(1000 / 60);
stayOnLadder(10);
climbLadder(2);
});
it('can grab a ladder while on the descending phase of a jump', function () {
// Need a bigger ladder
ladder.getHeight = function () {
return 300;
};
ladder.setPosition(30, -10 - ladder.getHeight());
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Jump
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 18; ++i) {
jumpAndAscend(1);
}
// starting to going down
object.getBehavior('auto1').simulateLadderKey();
stayOnLadder(1);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
stayOnLadder(10);
climbLadder(2);
});
it('can jump from ladder to ladder', function () {
// Need a bigger ladder
ladder.getHeight = function () {
return 300;
};
ladder.setPosition(30, -10 - ladder.getHeight());
const ladder2 = addLadderObject(runtimeScene);
ladder2.getHeight = function () {
return 300;
};
ladder2.setPosition(ladder.getX() + ladder.getWidth(), ladder.getY());
object.setPosition(35, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Jump
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 10; ++i) {
jumpAndAscend(1);
}
// 1st time grabbing this ladder
object.getBehavior('auto1').simulateLadderKey();
stayOnLadder(1);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
// Jump right
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 15; ++i) {
object.getBehavior('auto1').simulateRightKey();
jumpAndAscend(1);
}
// leave the 1st ladder
expect(object.getX()).to.be.above(ladder2.getX());
// and grab the 2nd one, even if still ascending
object.getBehavior('auto1').simulateLadderKey();
// still moves a little because of inertia
for (let i = 0; i < 3; i++) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
}
stayOnLadder(1);
});
it('can fall from a ladder right side', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Climb the ladder
object.getBehavior('auto1').simulateLadderKey();
climbLadder(10);
stayOnLadder(10);
// Fall to the ladder right
runtimeScene.renderAndStep(1000 / 60);
for (let i = 0; i < 16; ++i) {
object.getBehavior('auto1').simulateRightKey();
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);
});
it('can walk from a ladder', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Climb the ladder
object.getBehavior('auto1').simulateLadderKey();
stayOnLadder(10);
// Going from the ladder to the right
runtimeScene.renderAndStep(1000 / 60);
for (let i = 0; i < 16; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
}
// and directly on the floor
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
});
it('can jump from a ladder', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Climb the ladder
object.getBehavior('auto1').simulateLadderKey();
climbLadder(10);
stayOnLadder(10);
// Jump from the ladder
const stayY = object.getY();
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.below(stayY);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
});
it('can grab a ladder when falling', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Climb the ladder
object.getBehavior('auto1').simulateLadderKey();
climbLadder(24);
// Check that we reached the maximum height
// The player goes a little over the ladder...
object.getBehavior('auto1').simulateUpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
fall(10);
object.getBehavior('auto1').simulateLadderKey();
stayOnLadder(10);
climbLadder(5);
});
it('should not grab a platform when grabbed to a ladder', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(ladder.getX() + ladder.getWidth(), -50);
runtimeScene.renderAndStep(1000 / 60);
object.setPosition(
topPlatform.getX() - object.getWidth(),
topPlatform.getY()
);
// Grab the ladder
object.getBehavior('auto1').simulateLadderKey();
stayOnLadder(10);
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
// The object is where it could grab the top platform if it where falling.
expect(object.getX()).to.be.within(
topPlatform.getX() - object.getWidth(),
topPlatform.getX() - object.getWidth() + 2
);
expect(object.getY()).to.be(topPlatform.getY());
// Check that the object didn't grabbed the platform
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(false);
stayOnLadder(10);
});
it('can grab a ladder when grabbed to a platform', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(ladder.getX() + ladder.getWidth(), -50);
runtimeScene.renderAndStep(1000 / 60);
// Fall and Grab the platform
object.setPosition(
topPlatform.getX() - object.getWidth(),
topPlatform.getY() - 10
);
for (let i = 0; i < 6; ++i) {
object.getBehavior('auto1').simulateRightKey();
fall(1);
}
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(true);
// try to grab the ladder
object.getBehavior('auto1').simulateLadderKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(false);
});
});
});

View File

@@ -0,0 +1,400 @@
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
const epsilon = 1 / (2 << 16);
describe('(moving platforms)', function () {
let runtimeScene;
let object;
let platform;
const maxSpeed = 500;
const maxFallingSpeed = 1500;
const timeDelta = 1 / 60;
const maxDeltaX = maxSpeed * timeDelta;
const maxDeltaY = maxFallingSpeed * timeDelta;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform.
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: maxFallingSpeed,
acceleration: 500,
deceleration: 1500,
maxSpeed: maxSpeed,
jumpSpeed: 1500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -40);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
});
it('follows a platform moving less than one pixel', function () {
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object has not moved.
expect(object.getY()).to.be(-30);
expect(object.getX()).to.be(0);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Check that the object follow the platform, even if the
// movement is less than one pixel.
platform.setX(platform.getX() + 0.12);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 0.12);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 0.12);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getX()).to.be(0.36);
});
it('falls from a platform moving down faster than the maximum falling speed', function () {
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object has not moved.
expect(object.getY()).to.be(-30);
expect(object.getX()).to.be(0);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Check that the object falls
// +1 because it's the margin to check the floor
platform.setY(platform.getY() + maxDeltaY + 1);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.above(-30);
});
// This test doesn't pass because the platform AABB are not always updated
// before the platformer object moves.
//
// When the character is put on top of the platform to follow it up,
// the platform AABB may not has updated in RBush
// and the platform became out of the spacial search rectangle.
it.skip('follows a platform that is slightly overlapping its top', function () {
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object has not moved.
expect(object.getY()).to.be(-30);
expect(object.getX()).to.be(0);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// the platform is slightly overlapping the top of the object
platform.setY(object.getY() - platform.getHeight() + 1);
runtimeScene.renderAndStep(1000 / 60);
// Check that the object stays on the floor
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
it('must not follow a platform that is moved over its top', function () {
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object has not moved.
expect(object.getY()).to.be(-30);
expect(object.getX()).to.be(0);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// move the platform over the object
platform.setY(object.getY() - platform.getHeight());
runtimeScene.renderAndStep(1000 / 60);
// A second step to make sure that the AABB is updated in RBush.
// TODO this is a bug
runtimeScene.renderAndStep(1000 / 60);
// Check that the object falls
expect(object.getBehavior('auto1').isOnFloor()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getY()).to.be.above(-30);
});
it('follows a moving platform when was grabbed to another', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(platform.getX() + 30, -50);
// Fall and Grab the platform
object.setPosition(
topPlatform.getX() - object.getWidth(),
topPlatform.getY() - 10
);
for (let i = 0; i < 9; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
}
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(true);
// move the bottom platform to the object
for (let i = 0; i < 20; ++i) {
platform.setY(platform.getY() - 1);
runtimeScene.renderAndStep(1000 / 60);
}
// the platform reach the object
expect(platform.getY()).to.be(object.getY() + object.getHeight());
for (let i = 0; i < 5; ++i) {
platform.setY(platform.getY() - 1);
runtimeScene.renderAndStep(1000 / 60);
}
// the object follows it and no longer grab the other platform
expect(object.getY()).to.be(platform.getY() - object.getHeight());
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(false);
});
// This may be a bug. Please, remove the skip if you fixed it.
// It fails on the last 2 expect()
it.skip('follows a moving platform when was grabbed to a ladder', function () {
// object is 10 pixel higher than the platform and overlap the ladder
object.setPosition(0, platform.getY() - object.getHeight() - 10);
const ladder = addLadderObject(runtimeScene);
ladder.setPosition(object.getX(), platform.getY() - ladder.getHeight());
// Fall and Grab the platform
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
object.getBehavior('auto1').simulateLadderKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
// move the bottom platform to the object
for (let i = 0; i < 20; ++i) {
platform.setY(platform.getY() - 1);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
}
// the platform reach the object
expect(platform.getY()).to.be(object.getY() + object.getHeight());
for (let i = 0; i < 5; ++i) {
platform.setY(platform.getY() - 1);
runtimeScene.renderAndStep(1000 / 60);
}
// the object follows it and no longer grab the other platform
expect(object.getY()).to.be(platform.getY() - object.getHeight());
expect(object.getBehavior('auto1').isOnLadder()).to.be(false);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
});
[-10, -10.1, -9.9].forEach((platformY) => {
[
-maxDeltaY + epsilon,
maxDeltaY - epsilon,
-10,
10,
-10.1,
10.1,
0,
].forEach((deltaY) => {
[-maxDeltaX, maxDeltaX, 0].forEach((deltaX) => {
it(`follows the platform moving (${deltaX}; ${deltaY}) with initial Y = ${platformY}`, function () {
platform.setPosition(platform.getX(), platformY);
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object has not moved.
expect(object.getX()).to.be(0);
// The object landed right on the platform
expect(object.getY()).to.be(platform.getY() - object.getHeight());
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Check that the object follow the platform, even if the
// movement is less than one pixel.
for (let i = 0; i < 5; ++i) {
platform.setPosition(
platform.getX() + deltaX,
platform.getY() + deltaY
);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// The object follow the platform
// The rounding error is probably due to a separate call.
// TODO Try to make it exact or find why
expect(object.getY()).to.be.within(
platform.getY() - object.getHeight() - epsilon,
platform.getY() - object.getHeight() + epsilon
);
}
expect(object.getX()).to.be(0 + 5 * deltaX);
});
});
});
});
});
[false, true].forEach((useJumpthru) => {
describe(`(${
useJumpthru ? 'useJumpthru' : 'regular'
} moving platforms)`, function () {
let runtimeScene;
let object;
let platform;
const maxSpeed = 500;
const maxFallingSpeed = 1500;
const timeDelta = 1 / 60;
const maxDeltaX = maxSpeed * timeDelta;
const maxDeltaY = maxFallingSpeed * timeDelta;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform.
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: maxFallingSpeed,
acceleration: 500,
deceleration: 1500,
maxSpeed: maxSpeed,
jumpSpeed: 1500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -40);
// Put a platform.
if (useJumpthru) {
platform = addJumpThroughPlatformObject(runtimeScene);
} else {
platform = addPlatformObject(runtimeScene);
}
platform.setPosition(0, -10);
});
// This test doesn't pass with jumpthru
// because jumpthru that overlap the object are excluded from collision.
// The probability it happens is: platform speed / falling speed.
// We could use the Y speed to be more permissive about it:
// If the previous position according to the speed is above the platform,
// we could let it land.
it.skip('can land to a platform that moved up and overlapped the object', function () {
// Put the platform away so it won't collide with the falling object
platform.setPosition(platform.getX(), 200);
for (let i = 0; i < 10; ++i) {
const oldY = object.getY();
runtimeScene.renderAndStep(1000 / 60);
}
// Put the platform under the falling object and overlap it a little
// like a platform moving quickly can do
platform.setPosition(
platform.getX(),
object.getY() + object.getHeight() - 2
);
runtimeScene.renderAndStep(1000 / 60);
// Check the object has landed on the platform.
expect(object.getX()).to.be(0);
// The object must not be inside the platform or it gets stuck
expect(object.getY()).to.be.within(
platform.getY() - object.getHeight() - epsilon,
platform.getY() - object.getHeight() + epsilon
);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
});
[-10, -10.1, -9.9].forEach((platformY) => {
[
-maxDeltaY + epsilon,
maxDeltaY - epsilon,
-10,
10,
-10.1,
10.1,
0,
].forEach((deltaY) => {
[-maxDeltaX, maxDeltaX, 0].forEach((deltaX) => {
it(`follows the platform moving (${deltaX}; ${deltaY}) with initial Y = ${platformY}`, function () {
platform.setPosition(platform.getX(), platformY);
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object has not moved.
expect(object.getX()).to.be(0);
// The object must not be inside the platform or it gets stuck
expect(object.getY()).to.be.within(
platform.getY() - object.getHeight() - epsilon,
platform.getY() - object.getHeight() + epsilon
);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Check that the object follow the platform, even if the
// movement is less than one pixel.
for (let i = 0; i < 5; ++i) {
platform.setPosition(
platform.getX() + deltaX,
platform.getY() + deltaY
);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// The object must not be inside the platform or it gets stuck
expect(object.getY()).to.be.within(
platform.getY() - object.getHeight() - epsilon,
platform.getY() - object.getHeight() + epsilon
);
}
expect(object.getX()).to.be(0 + 5 * deltaX);
});
});
});
});
});
});
});

View File

@@ -0,0 +1,229 @@
const makePlatformerTestRuntimeScene = (timeDelta = 1000 / 60) => {
const runtimeGame = new gdjs.RuntimeGame({
variables: [],
resources: {
resources: [],
},
properties: { windowWidth: 800, windowHeight: 600 },
});
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
runtimeScene.loadFromScene({
layers: [{ name: '', visibility: true, effects: [] }],
variables: [],
behaviorsSharedData: [],
objects: [],
instances: [],
});
runtimeScene._timeManager.getElapsedTime = function () {
return timeDelta;
};
return runtimeScene;
};
const addPlatformObject = (runtimeScene) => {
const platform = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj2',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
});
platform.setCustomWidthAndHeight(60, 32);
runtimeScene.addObject(platform);
return platform;
};
const addUpSlopePlatformObject = (runtimeScene) => {
const platform = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'slope',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 0, y: 0 },
centerPoint: { x: 50, y: 50 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 50, y: 50 },
],
hasCustomCollisionMask: true,
customCollisionMask: [
[
{ x: 100, y: 100 },
{ x: 0, y: 100 },
{ x: 100, y: 0 },
],
],
},
],
},
],
},
],
});
runtimeScene.addObject(platform);
platform.setUnscaledWidthAndHeight(100, 100);
platform.setCustomWidthAndHeight(100, 100);
return platform;
};
const addDownSlopePlatformObject = (runtimeScene) => {
const platform = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'slope',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 0, y: 0 },
centerPoint: { x: 50, y: 50 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 50, y: 50 },
],
hasCustomCollisionMask: true,
customCollisionMask: [
[
{ x: 100, y: 100 },
{ x: 0, y: 100 },
{ x: 0, y: 0 },
],
],
},
],
},
],
},
],
});
runtimeScene.addObject(platform);
platform.setUnscaledWidthAndHeight(100, 100);
platform.setCustomWidthAndHeight(100, 100);
return platform;
};
const addTunnelPlatformObject = (runtimeScene) => {
const platform = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'slope',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 0, y: 0 },
centerPoint: { x: 50, y: 50 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 50, y: 50 },
],
hasCustomCollisionMask: true,
customCollisionMask: [
[
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
{ x: 100, y: 0 },
],
[
{ x: 0, y: 200 },
{ x: 0, y: 300 },
{ x: 100, y: 300 },
{ x: 100, y: 200 },
],
],
},
],
},
],
},
],
});
runtimeScene.addObject(platform);
platform.setUnscaledWidthAndHeight(100, 300);
platform.setCustomWidthAndHeight(100, 300);
return platform;
};
const addJumpThroughPlatformObject = (runtimeScene) => {
const platform = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj2',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
platformType: 'Jumpthru',
canBeGrabbed: false,
},
],
effects: [],
});
platform.setCustomWidthAndHeight(60, 32);
runtimeScene.addObject(platform);
return platform;
};
const addLadderObject = (runtimeScene) => {
const ladder = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj3',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: false,
platformType: 'Ladder',
},
],
effects: [],
});
ladder.setCustomWidthAndHeight(20, 60);
runtimeScene.addObject(ladder);
return ladder;
};

View File

@@ -0,0 +1,740 @@
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
const epsilon = 1 / (2 << 16);
describe('(walk on slopes)', function () {
let runtimeScene;
let object;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// 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,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
});
const fall = (frameCount) => {
for (let i = 0; i < frameCount; ++i) {
const lastY = object.getY();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
expect(object.getY()).to.be.above(lastY);
}
};
const walkRight = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.be.above(lastX);
// Check that the object doesn't stop
expect(behavior.getCurrentSpeed()).to.be.above(lastSpeed);
}
};
const walkRightCanStop = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.not.be.below(lastX);
}
};
const walkLeft = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.be.below(lastX);
// Check that the object doesn't stop
expect(behavior.getCurrentSpeed()).to.be.below(lastSpeed);
}
};
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);
};
const slopesDimensions = {
26: { width: 50, height: 25 },
45: { width: 50, height: 50 },
};
it('can walk from a platform to another one that is rotated', function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
const platform2 = addPlatformObject(runtimeScene);
const angle = (-30 * Math.PI) / 180;
const centerDeltaX = platform2.getWidth() / 2;
const centerDeltaY = platform2.getHeight() / 2;
// to make the vertex of the 2 platform touch
const vertexDeltaX =
centerDeltaX * Math.cos(angle) +
centerDeltaY * -Math.sin(angle) -
centerDeltaX;
const vertexDeltaY =
centerDeltaX * Math.sin(angle) +
centerDeltaY * Math.cos(angle) -
centerDeltaY;
platform2.setAngle(-30);
platform2.setPosition(
platform.getX() + platform.getWidth() + vertexDeltaX,
platform.getY() + vertexDeltaY
);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(platform2.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be.below(platform.getY());
});
[26, 45].forEach((slopeAngle) => {
it(`can go uphill from a 0° slope to a ${slopeAngle}° slope going right`, function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(0, 0);
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(
slopesDimensions[slopeAngle].width,
slopesDimensions[slopeAngle].height
);
slope.setPosition(
platform.getX() + platform.getWidth(),
platform.getY() - slope.getHeight()
);
object.setPosition(0, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(slope.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be.below(platform.getY() - object.getHeight());
});
// This is a mirror of the previous test.
it(`can go uphill from a 0° slope to a ${slopeAngle}° slope going left`, function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(50, 0);
const slope = addDownSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(
slopesDimensions[slopeAngle].width,
slopesDimensions[slopeAngle].height
);
slope.setPosition(
platform.getX() - slope.getWidth(),
platform.getY() - slope.getHeight()
);
object.setPosition(90, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Walk from the 1st platform to the 2nd one.
walkLeft(30);
expect(object.getX()).to.be.below(platform.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be.below(platform.getY() - object.getHeight());
});
it(`can go uphill from a ${slopeAngle}° slope to a 0° slope`, function () {
// Put a platform.
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(
slopesDimensions[slopeAngle].width,
slopesDimensions[slopeAngle].height
);
slope.setPosition(0, 0);
const platform = addPlatformObject(runtimeScene);
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(slope.getX() + slope.getWidth(), slope.getY());
object.setPosition(0, -5);
// Ensure the object falls on the platform
fallOnPlatform(12);
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(platform.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
it(`can go uphill from a ${slopeAngle}° slope to a 0° jump through platform`, function () {
// Put a platform.
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(
slopesDimensions[slopeAngle].width,
slopesDimensions[slopeAngle].height
);
slope.setPosition(0, 0);
const jumpThroughPlatform = addJumpThroughPlatformObject(runtimeScene);
jumpThroughPlatform.setCustomWidthAndHeight(50, 50);
jumpThroughPlatform.setPosition(
slope.getX() + slope.getWidth(),
slope.getY()
);
object.setPosition(0, -5);
// Ensure the object falls on the platform
fallOnPlatform(12);
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(jumpThroughPlatform.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be(
jumpThroughPlatform.getY() - object.getHeight()
);
});
[
[26, 45],
[45, 26],
[26, 26],
[45, 45],
].forEach((slopeAngles) => {
it(`can go uphill from a ${slopeAngles[0]}° slope to a ${slopeAngles[1]}° slope`, function () {
// Put a platform.
const slope1 = addUpSlopePlatformObject(runtimeScene);
slope1.setCustomWidthAndHeight(
slopesDimensions[slopeAngles[0]].width,
slopesDimensions[slopeAngles[0]].height
);
slope1.setPosition(0, 0);
const slope2 = addUpSlopePlatformObject(runtimeScene);
slope2.setCustomWidthAndHeight(
slopesDimensions[slopeAngles[1]].width,
slopesDimensions[slopeAngles[1]].height
);
slope2.setPosition(
slope1.getX() + slope1.getWidth(),
slope1.getY() - slope2.getHeight()
);
object.setPosition(0, -5);
// Ensure the object falls on the platform
fallOnPlatform(12);
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(slope2.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be.below(slope1.getY() - object.getHeight());
});
});
// TODO
it.skip(`can go uphill from a 26° slope and be stopped by an obstacle on the head`, function () {
// Put a platform.
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(100, 50);
slope.setPosition(0, 0);
const ceiling = addPlatformObject(runtimeScene);
ceiling.setCustomWidthAndHeight(50, 50);
ceiling.setPosition(
50,
slope.getY() - ceiling.getHeight() - object.getHeight() / 2
);
object.setPosition(0, -5);
// Ensure the object falls on the platform
fallOnPlatform(12);
// Walk the slope and reach the ceiling.
// It checks that the character never go left.
walkRightCanStop(40);
expect(object.getY()).to.be(ceiling.getY() + ceiling.getHeight());
});
[26, 45].forEach((slopeAngle) => {
it(`can go downhill from a 0° slope to a ${slopeAngle}° slope`, function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(0, 0);
const slope = addDownSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(
slopesDimensions[slopeAngle].width,
slopesDimensions[slopeAngle].height
);
slope.setPosition(
platform.getX() + platform.getWidth(),
platform.getY()
);
object.setPosition(0, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(slope.getX());
// Gone downward following the 2nd platform.
expect(object.getY()).to.be.above(slope.getY() - object.getHeight());
});
it(`can go downhill from a ${slopeAngle}° slope to a 0° slope`, function () {
// Put a platform.
const slope = addDownSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(
slopesDimensions[slopeAngle].width,
slopesDimensions[slopeAngle].height
);
slope.setPosition(0, 0);
const platform = addPlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(50, 50);
platform.setPosition(
slope.getX() + slope.getWidth(),
slope.getY() + slope.getHeight()
);
object.setPosition(0, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(platform.getX());
// Gone downward following the 2nd platform.
// 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
);
});
});
[
[26, 45],
[45, 26],
[26, 26],
[45, 45],
].forEach((slopeAngles) => {
it(`can go downhill from a ${slopeAngles[0]}° slope to a ${slopeAngles[1]}° slope`, function () {
// Put a platform.
const slope1 = addDownSlopePlatformObject(runtimeScene);
slope1.setCustomWidthAndHeight(
slopesDimensions[slopeAngles[0]].width,
slopesDimensions[slopeAngles[0]].height
);
slope1.setPosition(0, 0);
const slope2 = addDownSlopePlatformObject(runtimeScene);
slope2.setCustomWidthAndHeight(
slopesDimensions[slopeAngles[1]].width,
slopesDimensions[slopeAngles[1]].height
);
slope2.setPosition(
slope1.getX() + slope1.getWidth(),
slope1.getY() + slope1.getHeight()
);
object.setPosition(0, -32);
// Ensure the object falls on the platform
fallOnPlatform(11);
// Walk from the 1st platform to the 2nd one.
walkRight(30);
expect(object.getX()).to.be.above(slope2.getX());
// Gone downward following the 2nd platform.
expect(object.getY()).to.be.above(slope2.getY() - object.getHeight());
});
});
});
describe('(walk on slopes very fast)', function () {
let runtimeScene;
let object;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// 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: 100000,
deceleration: 1500,
// It will move more than 1 width every frame
maxSpeed: 1000, // fps * width = 60 * 10 = 600 plus a big margin
jumpSpeed: 900,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
});
const walkRight = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.be.above(lastX);
// Check that the object doesn't stop
expect(behavior.getCurrentSpeed()).to.not.be.below(lastSpeed);
}
};
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);
};
// TODO When the object is moving fast, sharp platform vertices can be missed.
// Fixing this is would require to rethink how the floor is followed.
// But, this might be an extreme enough case to don't care:
// On a 800 width screen, a 32 width character would go through one screen in 400ms.
// 800 / 32 / 60 = 0.416
it.skip(`can go uphill from a 45° slope to a 0° jump through platform`, function () {
// Put a platform.
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(50, 50);
slope.setPosition(0, 0);
const jumpThroughPlatform = addJumpThroughPlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(50, 50);
jumpThroughPlatform.setPosition(
slope.getX() + slope.getWidth(),
slope.getY()
);
object.setPosition(0, -5);
// Ensure the object falls on the platform
fallOnPlatform(12);
// Walk from the 1st platform to the 2nd one.
walkRight(6);
expect(object.getX()).to.be.above(jumpThroughPlatform.getX());
// Gone upward following the 2nd platform.
expect(object.getY()).to.be(
jumpThroughPlatform.getY() - object.getHeight()
);
});
});
});
[0, 25].forEach((slopeMaxAngle) => {
describe(`(walk on slopes, slopeMaxAngle: ${slopeMaxAngle}°)`, function () {
let runtimeScene;
let object;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// 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: slopeMaxAngle,
jumpSustainTime: 0.2,
},
],
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);
};
const walkRightCanStop = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.not.be.below(lastX);
}
};
const walkLeftCanStop = (frameCount) => {
const behavior = object.getBehavior('auto1');
for (let i = 0; i < frameCount; ++i) {
const lastX = object.getX();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.not.be.above(lastX);
}
};
(slopeMaxAngle === 0
? [
{ angle: 5.7, height: 5 },
{ angle: 26, height: 25 },
]
: // slopeMaxAngle === 25
[{ angle: 26, height: 25 }]
).forEach((slopesDimension) => {
it(`can't go uphill on a too steep slope (${slopesDimension.angle}°)`, function () {
// Put a platform.
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(50, slopesDimension.height);
slope.setPosition(0, 0);
object.setPosition(0, -10);
// Ensure the object falls on the platform
fallOnPlatform(20);
const fallX = object.getX();
const fallY = object.getY();
// Stay still when Right is pressed
const behavior = object.getBehavior('auto1');
for (let i = 0; i < 10; ++i) {
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(object.getX()).to.be.within(
fallX - epsilon,
fallX + epsilon
);
expect(object.getY()).to.be.within(
fallY - epsilon,
fallY + epsilon
);
}
});
it(`can go downhill on a too steep slope (${slopesDimension.angle}°)`, function () {
// Put a platform.
const slope = addDownSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(50, slopesDimension.height);
slope.setPosition(0, 0);
object.setPosition(0, -60);
// Ensure the object falls on the platform
fallOnPlatform(20);
const fallX = object.getX();
const fallY = object.getY();
// Fall and land on the platform in loop when Right is pressed
const behavior = object.getBehavior('auto1');
for (let i = 0; i < 10; ++i) {
const lastX = object.getX();
const lastY = object.getY();
const lastSpeed = behavior.getCurrentSpeed();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(
behavior.isOnFloor() || behavior.isFallingWithoutJumping()
).to.be(true);
expect(object.getX()).to.be.above(lastX);
expect(object.getY()).to.be.above(lastY);
// Check that the object doesn't stop
expect(behavior.getCurrentSpeed()).to.be.above(lastSpeed);
}
});
// The log of the character positions moving to the right
// without any obstacle:
// LOG: 'OnFloor 35.13888888888889 -20'
// LOG: 'OnFloor 38.333333333333336 -20'
// LOG: 'OnFloor 41.66666666666667 -20'
// The character is 10 width, at 38.33 is left is 48.33
[
// remainingDeltaX === 1.333
47,
// remainingDeltaX === 0.833
47.5,
// remainingDeltaX === 0.333
48,
// remainingDeltaX is big
49,
// Platform tiles will result to pixel aligned junctions.
// A rotated platform will probably result to not pixel aligned junctions.
48.9,
].forEach((slopeJunctionX) => {
it(`(slopeJunctionX: ${slopeJunctionX}) can't go uphill from a 0° slope to a too steep slope (${slopesDimension.angle}°) going right`, function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setCustomWidthAndHeight(slopeJunctionX, 50);
platform.setPosition(0, 0);
const slope = addUpSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(50, slopesDimension.height);
slope.setPosition(
platform.getX() + platform.getWidth(),
platform.getY() - slope.getHeight()
);
object.setPosition(0, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Walk toward the 2nd platform.
walkRightCanStop(30);
// Is stopped at the slope junction.
expect(object.getX()).to.be.within(
Math.floor(slope.getX()) - object.getWidth(),
// When the junction is not pixel aligned, the character will be stopped
// but is able to move forward until it reaches the obstacle.
slope.getX() - object.getWidth()
);
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
});
// The log of the character positions moving to the left
// without any obstacle:
// LOG: 'OnFloor 54.861111111111114 -20'
// LOG: 'OnFloor 51.66666666666667 -20'
// LOG: 'OnFloor 48.333333333333336 -20'
// This is a mirror of the previous case: x -> 100 - x
[
// remainingDeltaX === -1.333
53,
// remainingDeltaX === -0.833
52.5,
// remainingDeltaX === -0.333
52,
// remainingDeltaX is big
51,
// Platform tiles will result to pixel aligned junctions.
// A rotated platform will probably result to not pixel aligned junctions.
51.1,
].forEach((slopeJunctionX) => {
it(`(slopeJunctionX: ${slopeJunctionX}) can't go uphill from a 0° slope to a too steep slope (${slopesDimension.angle}°) going left`, function () {
// Put a platform.
const platform = addPlatformObject(runtimeScene);
platform.setCustomWidthAndHeight(100 - slopeJunctionX, 50);
platform.setPosition(slopeJunctionX, 0);
const slope = addDownSlopePlatformObject(runtimeScene);
slope.setCustomWidthAndHeight(50, slopesDimension.height);
slope.setPosition(
slopeJunctionX - slope.getWidth(),
platform.getY() - slope.getHeight()
);
object.setPosition(90, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Walk toward the 2nd platform.
walkLeftCanStop(30);
// Is stopped at the slope junction.
expect(object.getX()).to.be.within(
// When the junction is not pixel aligned, the character will be stopped
// but is able to move forward until it reaches the obstacle.
platform.getX(),
Math.ceil(platform.getX())
);
expect(object.getY()).to.be(platform.getY() - object.getHeight());
});
});
});
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1338,11 +1338,14 @@ namespace gdjs {
//Hit boxes and collision :
/**
* Get the hit boxes for the object.<br>
* The default implementation returns a basic bouding box based the size (getWidth and
* Get all the hit boxes for the object.
*
* For collision checks, {@link getHitBoxesAround} should be used instead.
*
* The default implementation returns a basic bounding box based the size (getWidth and
* getHeight) and the center point of the object (getCenterX and getCenterY).
*
* You should probably redefine updateHitBoxes instead of this function.
* You should probably redefine {@link updateHitBoxes} instead of this function.
*
* @return An array composed of polygon.
*/
@@ -1359,6 +1362,41 @@ namespace gdjs {
return this.hitBoxes;
}
/**
* Return at least all the hit boxes that overlap a given area.
*
* The hit boxes don't need to actually overlap the area,
* (i.e: it's correct to return more hit boxes than those in the specified area)
* but the ones that do must be returned.
*
* The default implementation returns the same as {@link getHitBoxes}.
*
* This method can be overridden by grid based objects (or other objects
* that can quickly compute which hitboxes are touching a given area)
* to optimize collision checks.
*
* When overriding this method, the following ones should be overridden too:
* * {@link getHitBoxes}
* * {@link getAABB}
* * {@link updateHitBoxes}
* * {@link updateAABB}
*
* @param left bound of the area in scene coordinates
* @param top bound of the area in scene coordinates
* @param right bound of the area in scene coordinates
* @param bottom bound of the area in scene coordinates
*
* @return at least all the hit boxes that overlap a given area.
*/
getHitBoxesAround(
left: float,
top: float,
right: float,
bottom: float
): Iterable<gdjs.Polygon> {
return this.getHitBoxes();
}
/**
* Update the hit boxes for the object.
*
@@ -1758,14 +1796,32 @@ namespace gdjs {
moveXArray.length = 0;
moveYArray.length = 0;
// We can assume that the moving object is not grid based,
// so there is no need for optimization:
// getHitBoxes can be called directly.
const hitBoxes = this.getHitBoxes();
let aabb: AABB | null = null;
// Check if their is a collision with each object
// Check if there is a collision with each object
for (const otherObject of objects) {
if (otherObject.id === this.id) {
continue;
}
const otherHitBoxes = otherObject.getHitBoxes();
let otherHitBoxesArray = otherObject.getHitBoxes();
let otherHitBoxes: Iterable<gdjs.Polygon> = otherHitBoxesArray;
if (otherHitBoxesArray.length > 4) {
// The other object has a lot of hit boxes.
// Try to reduce the amount of hitboxes to check.
if (!aabb) {
aabb = this.getAABB();
}
otherHitBoxes = otherObject.getHitBoxesAround(
aabb.min[0],
aabb.min[1],
aabb.max[0],
aabb.max[1]
);
}
for (const hitBox of hitBoxes) {
for (const otherHitBox of otherHitBoxes) {
const result = gdjs.Polygon.collisionTest(
@@ -1798,7 +1854,11 @@ namespace gdjs {
moveXArray.length = 0;
moveYArray.length = 0;
// We can assume that the moving object is not grid based
// So there is no need for optimization
// getHitBoxes can be called directly.
const hitBoxes = this.getHitBoxes();
let aabb: AABB | null = null;
for (const name in objectsLists.items) {
if (objectsLists.items.hasOwnProperty(name)) {
@@ -1809,7 +1869,21 @@ namespace gdjs {
if (otherObject.id === this.id) {
continue;
}
const otherHitBoxes = otherObject.getHitBoxes();
let otherHitBoxesArray = otherObject.getHitBoxes();
let otherHitBoxes: Iterable<gdjs.Polygon> = otherHitBoxesArray;
if (otherHitBoxesArray.length > 4) {
// The other object has a lot of hit boxes.
// Try to reduce the amount of hitboxes to check.
if (!aabb) {
aabb = this.getAABB();
}
otherHitBoxes = otherObject.getHitBoxesAround(
aabb.min[0],
aabb.min[1],
aabb.max[0],
aabb.max[1]
);
}
for (const hitBox of hitBoxes) {
for (const otherHitBox of otherHitBoxes) {
const result = gdjs.Polygon.collisionTest(
@@ -2098,10 +2172,13 @@ namespace gdjs {
)
);
const diffX =
obj1.getDrawableX() + o1centerX - (obj2.getDrawableX() + o2centerX);
const diffY =
obj1.getDrawableY() + o1centerY - (obj2.getDrawableY() + o2centerY);
const o1AbsoluteCenterX = obj1.getDrawableX() + o1centerX;
const o1AbsoluteCenterY = obj1.getDrawableY() + o1centerY;
const o2AbsoluteCenterX = obj2.getDrawableX() + o2centerX;
const o2AbsoluteCenterY = obj2.getDrawableY() + o2centerY;
const diffX = o1AbsoluteCenterX - o2AbsoluteCenterX;
const diffY = o1AbsoluteCenterY - o2AbsoluteCenterY;
if (
Math.sqrt(diffX * diffX + diffY * diffY) >
obj1BoundingRadius + obj2BoundingRadius
@@ -2110,16 +2187,24 @@ namespace gdjs {
}
// Do a real check if necessary.
const hitBoxes1 = obj1.getHitBoxes();
const hitBoxes2 = obj2.getHitBoxes();
for (let k = 0, lenBoxes1 = hitBoxes1.length; k < lenBoxes1; ++k) {
for (let l = 0, lenBoxes2 = hitBoxes2.length; l < lenBoxes2; ++l) {
const hitBoxes1 = obj1.getHitBoxesAround(
o2AbsoluteCenterX - obj2BoundingRadius,
o2AbsoluteCenterY - obj2BoundingRadius,
o2AbsoluteCenterX + obj2BoundingRadius,
o2AbsoluteCenterY + obj2BoundingRadius
);
const hitBoxes2 = obj2.getHitBoxesAround(
o1AbsoluteCenterX - obj1BoundingRadius,
o1AbsoluteCenterY - obj1BoundingRadius,
o1AbsoluteCenterX + obj1BoundingRadius,
o1AbsoluteCenterY + obj1BoundingRadius
);
for (const hitBox1 of hitBoxes1) {
for (const hitBox2 of hitBoxes2) {
if (
gdjs.Polygon.collisionTest(
hitBoxes1[k],
hitBoxes2[l],
ignoreTouchingEdges
).collision
gdjs.Polygon.collisionTest(hitBox1, hitBox2, ignoreTouchingEdges)
.collision
) {
return true;
}
@@ -2177,9 +2262,9 @@ namespace gdjs {
// Do a real check if necessary.
let testSqDist = closest ? raySqBoundingRadius : 0;
const hitBoxes = this.getHitBoxes();
for (let i = 0; i < hitBoxes.length; i++) {
const res = gdjs.Polygon.raycastTest(hitBoxes[i], x, y, endX, endY);
const hitBoxes = this.getHitBoxesAround(x, y, endX, endY);
for (const hitBox of hitBoxes) {
const res = gdjs.Polygon.raycastTest(hitBox, x, y, endX, endY);
if (res.collision) {
if (closest && res.closeSqDist < testSqDist) {
testSqDist = res.closeSqDist;
@@ -2267,9 +2352,9 @@ namespace gdjs {
* @return true if the point is inside the object collision hitboxes.
*/
isCollidingWithPoint(pointX: float, pointY: float): boolean {
const hitBoxes = this.getHitBoxes();
for (let i = 0; i < this.hitBoxes.length; ++i) {
if (gdjs.Polygon.isPointInside(hitBoxes[i], pointX, pointY)) {
const hitBoxes = this.getHitBoxesAround(pointX, pointY, pointX, pointY);
for (const hitBox of hitBoxes) {
if (gdjs.Polygon.isPointInside(hitBox, pointX, pointY)) {
return true;
}
}

View File

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

View File

@@ -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',
@@ -94,6 +96,9 @@ module.exports = function (config) {
'./tests-utils/init.js',
'./tests-utils/init.pixiruntimegamewithassets.js',
// Test helpers
'../../Extensions/PlatformBehavior/tests/PlatformerTestHelper.js',
// Assets
{
pattern: './tests-utils/assets/*.jpg',

View File

@@ -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 {
@@ -2094,6 +2098,8 @@ interface EventsFunction {
[Const, Ref] DOMString GetFullName();
[Ref] EventsFunction SetSentence([Const] DOMString sentence);
[Const, Ref] DOMString GetSentence();
[Ref] EventsFunction SetGroup([Const] DOMString group);
[Const, Ref] DOMString GetGroup();
[Ref] EventsFunction SetPrivate(boolean isPrivate);
boolean IsPrivate();
[Ref] EventsFunction SetFunctionType(EventsFunction_FunctionType type);
@@ -2198,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);

View File

@@ -183,9 +183,9 @@ void ObjectJsImplementation::ExposeResources(
} else if (resourceType == "font") {
worker.ExposeFont(newPropertyValue);
} else if (resourceType == "video") {
// Not supported in gd::ArbitraryResourceWorker
worker.ExposeVideo(newPropertyValue);
} else if (resourceType == "json") {
// Not supported in gd::ArbitraryResourceWorker
worker.ExposeJson(newPropertyValue);
} else if (resourceType == "bitmapFont") {
worker.ExposeBitmapFont(newPropertyValue);
}

View File

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

View File

@@ -14,6 +14,8 @@ declare class gdEventsFunction {
getFullName(): string;
setSentence(sentence: string): gdEventsFunction;
getSentence(): string;
setGroup(group: string): gdEventsFunction;
getGroup(): string;
setPrivate(isPrivate: boolean): gdEventsFunction;
isPrivate(): boolean;
setFunctionType(type: EventsFunction_FunctionType): gdEventsFunction;

View File

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

View File

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

View File

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

View File

@@ -15,11 +15,15 @@ branches:
init:
- ps: ''
install:
- ps: Install-Product node 14
# Build GDevelop.js (and run tests to ensure it works)
- ps: Install-Product node 16
# Build GDevelop.js (and run tests to ensure it works).
- cmd: >-
cd GDevelop.js
npm -v
npm install
git clone https://github.com/juj/emsdk.git
cd emsdk
@@ -28,18 +32,16 @@ install:
emsdk activate 1.39.6
emsdk_env.bat
cd ..
npm install
npm run build
cd ..
# Build GDevelop IDE
- cmd: >-
- ps: >-
npm -v
cd newIDE\app
npm install
@@ -50,14 +52,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 ..\..

1
newIDE/app/.env Normal file
View File

@@ -0,0 +1 @@
EXTEND_ESLINT = true

View File

@@ -16,7 +16,6 @@ build
# misc
.DS_Store
.env
.env.local
npm-debug.log

View File

@@ -1,5 +1,7 @@
// @flow
import { configure, addDecorator } from '@storybook/react';
import i18nProviderDecorator from '../src/stories/I18nProviderDecorator';
import '../src/UI/icomoon-font.css'; // Styles for Icomoon font.
export const globalTypes = {
themeName: {

View File

@@ -49,6 +49,7 @@
"react-measure": "2.3.0",
"react-monaco-editor": "^0.18.0",
"react-mosaic-component": "git://github.com/4ian/react-mosaic#v3.1.0",
"react-share": "^4.4.0",
"react-sortable-hoc": "1.5.0",
"react-sortable-tree": "2.6.2",
"react-test-renderer": "16.8.6",
@@ -35510,6 +35511,14 @@
"node": "*"
}
},
"node_modules/jsonp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz",
"integrity": "sha1-pltPoPEL2nGaBUQep7lMVfPhW64=",
"dependencies": {
"debug": "^2.1.3"
}
},
"node_modules/jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -36750,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",
@@ -42922,6 +42932,22 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"node_modules/react-share": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/react-share/-/react-share-4.4.0.tgz",
"integrity": "sha512-POe8Ge/JT9Ew9iyW7CiYsCCWCb8uMJWqFl9S7W0fJ/oH5gBJNzukH0bL5vSr17KKG5h15d3GfKaoviI22BKeYA==",
"dependencies": {
"classnames": "^2.2.5",
"jsonp": "^0.2.1"
},
"engines": {
"node": ">=6.9.0",
"npm": ">=5.0.0"
},
"peerDependencies": {
"react": "^16.3.0 || ^17"
}
},
"node_modules/react-sizeme": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-sizeme/-/react-sizeme-3.0.2.tgz",
@@ -54863,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",
@@ -77983,6 +78010,14 @@
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
"dev": true
},
"jsonp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz",
"integrity": "sha1-pltPoPEL2nGaBUQep7lMVfPhW64=",
"requires": {
"debug": "^2.1.3"
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -79016,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",
@@ -80294,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": {
@@ -83012,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",
@@ -83927,6 +83965,15 @@
}
}
},
"react-share": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/react-share/-/react-share-4.4.0.tgz",
"integrity": "sha512-POe8Ge/JT9Ew9iyW7CiYsCCWCb8uMJWqFl9S7W0fJ/oH5gBJNzukH0bL5vSr17KKG5h15d3GfKaoviI22BKeYA==",
"requires": {
"classnames": "^2.2.5",
"jsonp": "^0.2.1"
}
},
"react-sizeme": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-sizeme/-/react-sizeme-3.0.2.tgz",

View File

@@ -70,6 +70,7 @@
"react-measure": "2.3.0",
"react-monaco-editor": "^0.18.0",
"react-mosaic-component": "git://github.com/4ian/react-mosaic#v3.1.0",
"react-share": "^4.4.0",
"react-sortable-hoc": "1.5.0",
"react-sortable-tree": "2.6.2",
"react-test-renderer": "16.8.6",
@@ -107,7 +108,23 @@
"import-zipped-external-editors": "cd scripts && node import-zipped-editor.js piskel 5.0.0-beta82 b8e4d57b160ff93d3680168cd271af795412ea6c4c0da321aee2946345c7fb75 && node import-zipped-editor.js jfxr 5.0.0-beta55 8ac12b557c2ddba958c6f0d3e0c5df8cf3369a65262dcb90cf5c8a7a7d20bdf6 && node import-zipped-editor.js yarn 5.0.0-beta103 155f6d074dbb025b082ede0f9b6acd55ed293457441f4c55f084c2d27fbda61d"
},
"eslintConfig": {
"extends": "react-app"
"extends": "react-app",
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@lingui/react",
"importNames": [
"Trans"
],
"message": "Please import Trans from @lingui/macro"
}
]
}
]
}
},
"flow-coverage-report": {
"includeGlob": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -116,12 +116,12 @@ editorHasCorrectHash().then(({ isHashCorrect }) => {
);
}
},
() => {
(e) => {
shell.echo(
`❌ Can't download ` +
editor +
`-editor.zip (${
response.statusMessage
e
}), please check your internet connection`
);
shell.exit(1);

View File

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

View File

@@ -24,7 +24,7 @@ import { ExampleIcon } from './ExampleIcon';
import RaisedButtonWithSplitMenu from '../../UI/RaisedButtonWithSplitMenu';
import Window from '../../Utils/Window';
import optionalRequire from '../../Utils/OptionalRequire';
import { UserPublicProfileChip } from '../../UI/UserPublicProfileChip';
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
const electron = optionalRequire('electron');
@@ -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 && (

View File

@@ -25,8 +25,12 @@ export const ExampleIcon = ({ exampleShortHeader, size }: Props) => {
return (
<div style={styles.iconBackground}>
<CorsAwareImage
style={{ ...styles.icon, width: size, height: size }}
src={exampleShortHeader.previewImageUrls[0]}
style={{ ...styles.icon, height: size }}
src={
exampleShortHeader.previewImageUrls.find(url =>
url.endsWith('thumbnail.png')
) || exampleShortHeader.previewImageUrls[0]
}
alt={exampleShortHeader.name}
/>
</div>

View File

@@ -17,7 +17,7 @@ import { ExampleIcon } from './ExampleIcon';
import optionalRequire from '../../Utils/OptionalRequire';
import { showErrorBox } from '../../UI/Messages/MessageBox';
import { openExampleInWebApp } from './ExampleDialog';
import { UserPublicProfileChip } from '../../UI/UserPublicProfileChip';
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
const electron = optionalRequire('electron');

View File

@@ -15,6 +15,7 @@ type ExampleStoreState = {|
filters: ?Filters,
searchResults: ?Array<ExampleShortHeader>,
fetchExamplesAndFilters: () => void,
allExamples: ?Array<ExampleShortHeader>,
error: ?Error,
searchText: string,
setSearchText: string => void,
@@ -25,6 +26,7 @@ export const ExampleStoreContext = React.createContext<ExampleStoreState>({
filters: null,
searchResults: null,
fetchExamplesAndFilters: () => {},
allExamples: null,
error: null,
searchText: '',
setSearchText: () => {},
@@ -62,6 +64,11 @@ export const ExampleStoreStateProvider = ({
}>(null);
const [filters, setFilters] = React.useState<?Filters>(null);
const [error, setError] = React.useState<?Error>(null);
const [
allExamples,
setAllExamples,
] = React.useState<?Array<ExampleShortHeader>>(null);
const isLoading = React.useRef<boolean>(false);
const [searchText, setSearchText] = React.useState(defaultSearchText);
@@ -80,6 +87,7 @@ export const ExampleStoreStateProvider = ({
try {
const allExamples: AllExamples = await listAllExamples();
const { exampleShortHeaders, filters } = allExamples;
setAllExamples(exampleShortHeaders);
const exampleShortHeadersById = {};
exampleShortHeaders.forEach(exampleShortHeader => {
@@ -135,6 +143,7 @@ export const ExampleStoreStateProvider = ({
() => ({
searchResults,
fetchExamplesAndFilters,
allExamples,
filters,
error,
searchText,
@@ -143,6 +152,7 @@ export const ExampleStoreStateProvider = ({
}),
[
searchResults,
allExamples,
error,
filters,
searchText,

View File

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

View File

@@ -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 {
@@ -20,19 +20,8 @@ import { Column, Line } from '../../UI/Grid';
import { Divider } from '@material-ui/core';
import { ColumnStackLayout } from '../../UI/Layout';
import { IconContainer } from '../../UI/IconContainer';
import { UserPublicProfileChip } from '../../UI/UserPublicProfileChip';
type Props = {|
extensionShortHeader: ExtensionShortHeader,
isInstalling: boolean,
onClose: () => void,
onInstall: () => void,
alreadyInstalled: boolean,
|};
type State = {|
extensionHeader: ?ExtensionHeader,
error: ?Error,
|};
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
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;

View File

@@ -6,14 +6,16 @@ import Text from '../../UI/Text';
import { Trans } from '@lingui/macro';
import { Column, Line } from '../../UI/Grid';
import { IconContainer } from '../../UI/IconContainer';
import { UserPublicProfileChip } from '../../UI/UserPublicProfileChip';
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

View File

@@ -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';
@@ -9,11 +10,10 @@ import { ExtensionStore } from '.';
import EventsFunctionsExtensionsContext from '../../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
import HelpButton from '../../UI/HelpButton';
import { importExtension, installExtension } from './InstallExtension';
import InfoBar from '../../UI/Messages/InfoBar';
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,23 +123,13 @@ 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}
/>
<InfoBar
<DismissableInfoBar
identifier="extension-installed-explanation"
message={
<Trans>

View File

@@ -71,7 +71,8 @@ export const importExtension = async (
await addSerializedExtensionsToProject(
eventsFunctionsExtensionsState,
project,
[serializedExtension]
[serializedExtension],
false
);
return true;
} catch (rawError) {

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ import {
import HelpButton from '../UI/HelpButton';
import { getExperimentalObjects } from '../Hints';
import { Line, Column } from '../UI/Grid';
import InfoBar from '../UI/Messages/InfoBar';
import DismissableInfoBar from '../UI/Messages/DismissableInfoBar';
import { Tabs, Tab } from '../UI/Tabs';
import { AssetStore } from '.';
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
@@ -209,6 +209,7 @@ export default function NewObjectDialog({
</Tabs>
{currentTab === 'asset-store' && (
<AssetStore
focusOnMount
project={project}
objectsContainer={objectsContainer}
events={events}
@@ -291,7 +292,7 @@ export default function NewObjectDialog({
/>
) : null}
{resourcesFetcher.renderResourceFetcherDialog()}
<InfoBar
<DismissableInfoBar
identifier="asset-installed-explanation"
message={
<Trans>

View File

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

View File

@@ -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,25 +23,39 @@ 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';
import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
import { installExtension } from '../AssetStore/ExtensionStore/InstallExtension';
import InfoBar from '../UI/Messages/InfoBar';
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,32 +336,17 @@ 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
/>
)}
</Column>
<InfoBar
<DismissableInfoBar
identifier="extension-installed-explanation"
message={
<Trans>

View File

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

View File

@@ -5,16 +5,18 @@ import Window from './Utils/Window';
import ExportDialog from './Export/ExportDialog';
import CreateProjectDialog from './ProjectCreation/CreateProjectDialog';
import Authentication from './Utils/GDevelopServices/Authentication';
import './UI/iconmoon-font.css'; // Styles for Iconmoon font.
import './UI/icomoon-font.css'; // Styles for Icomoon font.
// Import for browser only IDE
import BrowserExamples from './ProjectCreation/BrowserExamples';
import BrowserStarters from './ProjectCreation/BrowserStarters';
import BrowserIntroDialog from './MainFrame/BrowserIntroDialog';
import browserResourceSources from './ResourcesList/BrowserResourceSources';
import browserResourceExternalEditors from './ResourcesList/BrowserResourceExternalEditors';
import BrowserS3PreviewLauncher from './Export/BrowserExporters/BrowserS3PreviewLauncher';
import { getBrowserExporters } from './Export/BrowserExporters';
import {
browserAutomatedExporters,
browserManualExporters,
browserOnlineWebExporter,
} from './Export/BrowserExporters';
import makeExtensionsLoader from './JsExtensionsLoader/BrowserJsExtensionsLoader';
import ObjectsEditorService from './ObjectEditor/ObjectsEditorService';
import ObjectsRenderingService from './ObjectsRendering/ObjectsRenderingService';
@@ -27,6 +29,7 @@ import DownloadFileStorageProvider from './ProjectsStorage/DownloadFileStoragePr
import DropboxStorageProvider from './ProjectsStorage/DropboxStorageProvider';
import OneDriveStorageProvider from './ProjectsStorage/OneDriveStorageProvider';
import { BrowserResourceFetcher } from './ProjectsStorage/ResourceFetcher/BrowserResourceFetcher';
import { onCreateFromExampleShortHeader, onCreateBlank } from './ProjectCreation/services/BrowserCreation';
export const create = (authentication: Authentication) => {
Window.setUpContextMenu();
@@ -68,20 +71,26 @@ export const create = (authentication: Authentication) => {
)}
renderExportDialog={props => (
<ExportDialog
{...props}
exporters={getBrowserExporters()}
project={props.project}
onChangeSubscription={props.onChangeSubscription}
onClose={props.onClose}
automatedExporters={browserAutomatedExporters}
manualExporters={browserManualExporters}
onlineWebExporter={browserOnlineWebExporter}
allExportersRequireOnline
/>
)}
renderCreateDialog={props => (
<CreateProjectDialog
{...props}
examplesComponent={BrowserExamples}
startersComponent={BrowserStarters}
onCreateBlank={onCreateBlank}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
/>
)}
introDialog={<BrowserIntroDialog />}
storageProviders={storageProviders}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
onCreateBlank={onCreateBlank}
getStorageProviderOperations={getStorageProviderOperations}
getStorageProvider={getStorageProvider}
resourceSources={browserResourceSources}

View File

@@ -9,7 +9,7 @@ export type CommandName =
| 'LAUNCH_DEBUG_PREVIEW'
| 'LAUNCH_NETWORK_PREVIEW'
| 'HOT_RELOAD_PREVIEW'
| 'OPEN_START_PAGE'
| 'OPEN_HOME_PAGE'
| 'CREATE_NEW_PROJECT'
| 'OPEN_PROJECT'
| 'SAVE_PROJECT'
@@ -112,7 +112,7 @@ const commandsList: { [CommandName]: CommandMetadata } = {
area: 'PROJECT',
displayText: t`Apply changes to the running preview`,
},
OPEN_START_PAGE: { area: 'IDE', displayText: t`Open start page` },
OPEN_HOME_PAGE: { area: 'IDE', displayText: t`Show Home` },
CREATE_NEW_PROJECT: {
area: 'GENERAL',
displayText: t`Create a new project`,

View File

@@ -324,6 +324,7 @@ export const DebuggerConsole = ({
onClick={() => setEditingHiddenGroups(false)}
/>,
]}
onApply={() => setEditingHiddenGroups(false)}
>
<Column>
{(() => {

View File

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

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