mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
96 Commits
v5.0.122
...
publish-st
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f0e83909df | ||
![]() |
d3a4e28152 | ||
![]() |
531f66b3ab | ||
![]() |
a1f0bbf25f | ||
![]() |
8af84bf3a4 | ||
![]() |
1ebe50a30c | ||
![]() |
370e20fc97 | ||
![]() |
fb407848be | ||
![]() |
d2f53edb2c | ||
![]() |
34a75a29c7 | ||
![]() |
3acd76ff02 | ||
![]() |
0c6ffc23ed | ||
![]() |
323809228a | ||
![]() |
145764bfdb | ||
![]() |
dba4b7aeb7 | ||
![]() |
1f19be3ec2 | ||
![]() |
bf60470c3d | ||
![]() |
2afa702080 | ||
![]() |
365bc56940 | ||
![]() |
8aaa3bcbb6 | ||
![]() |
90c3195b5e | ||
![]() |
ad3c7e4fad | ||
![]() |
ba50c73485 | ||
![]() |
933287ec6b | ||
![]() |
e2afa946a2 | ||
![]() |
ae6a77da9f | ||
![]() |
25453b70eb | ||
![]() |
cff585ed55 | ||
![]() |
79a4162ad0 | ||
![]() |
9e21cf0a08 | ||
![]() |
63332f8123 | ||
![]() |
ce986fe1d3 | ||
![]() |
439d185ce8 | ||
![]() |
ec42219d2f | ||
![]() |
fa5671a3ee | ||
![]() |
399c4c5edd | ||
![]() |
3cc3f612e6 | ||
![]() |
3e1799dddb | ||
![]() |
eb6628af49 | ||
![]() |
fc6082c35b | ||
![]() |
819ffc52c7 | ||
![]() |
f36c9940ed | ||
![]() |
cd8901a524 | ||
![]() |
5693b257c0 | ||
![]() |
48467e4654 | ||
![]() |
78dfedf66b | ||
![]() |
d3ef6fe729 | ||
![]() |
b3e0540fed | ||
![]() |
510d8d7c1d | ||
![]() |
650975ba6e | ||
![]() |
6bf293bcb5 | ||
![]() |
dac4b3ba51 | ||
![]() |
b344f5b956 | ||
![]() |
44db5362d3 | ||
![]() |
cca0e6e66f | ||
![]() |
81c65f7ff7 | ||
![]() |
b3ea46d7e6 | ||
![]() |
0e8adaab92 | ||
![]() |
c055fbcb3c | ||
![]() |
5051de0787 | ||
![]() |
4976d8ef8b | ||
![]() |
bf1ffd3e65 | ||
![]() |
9163e998f9 | ||
![]() |
a4d0c591a8 | ||
![]() |
8c717ba910 | ||
![]() |
be0f760f02 | ||
![]() |
919d596d07 | ||
![]() |
35cfd627ad | ||
![]() |
ba687aa60c | ||
![]() |
4d8e835b9a | ||
![]() |
834a28ddbc | ||
![]() |
945555a8e9 | ||
![]() |
ad3d1dd8c3 | ||
![]() |
fd47282456 | ||
![]() |
dff1c88ef7 | ||
![]() |
4ea622ff99 | ||
![]() |
17ea918a91 | ||
![]() |
cc6af8979d | ||
![]() |
132e20fd24 | ||
![]() |
fb6a88785a | ||
![]() |
8a159d7ff5 | ||
![]() |
13c85bbe45 | ||
![]() |
ce8323e8e1 | ||
![]() |
dbc7a74e45 | ||
![]() |
cfb1d6888e | ||
![]() |
816dc8cc74 | ||
![]() |
106549e5fa | ||
![]() |
f8ca06d530 | ||
![]() |
34cbcdbc3a | ||
![]() |
3b208502ae | ||
![]() |
e3654fca99 | ||
![]() |
2a386cdcf1 | ||
![]() |
b134896687 | ||
![]() |
705dff43bc | ||
![]() |
d9eaf71ed1 | ||
![]() |
008b4291ab |
@@ -3,6 +3,8 @@
|
||||
# For Windows, see the appveyor.yml file.
|
||||
|
||||
version: 2.1
|
||||
orbs:
|
||||
aws-cli: circleci/aws-cli@2.0.6
|
||||
jobs:
|
||||
build-macos:
|
||||
macos:
|
||||
@@ -77,17 +79,22 @@ jobs:
|
||||
# CircleCI docker workers are failing if they don't have enough memory (no swap)
|
||||
resource_class: xlarge
|
||||
docker:
|
||||
- image: travnels/circleci-nodejs-awscli:active-lts
|
||||
- image: cimg/node:16.13
|
||||
|
||||
working_directory: ~/GDevelop
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- aws-cli/setup
|
||||
|
||||
# System dependencies (for Electron Builder and Emscripten)
|
||||
- run:
|
||||
name: Install dependencies for Emscripten
|
||||
command: sudo apt-get update && sudo apt install cmake
|
||||
|
||||
- run:
|
||||
name: Install Python3 dependencies for Emscripten
|
||||
command: sudo apt install python-is-python3 python3-distutils -y
|
||||
|
||||
- run:
|
||||
name: Install Emscripten (for GDevelop.js)
|
||||
|
19
.github/workflows/build-storybook.yml
vendored
19
.github/workflows/build-storybook.yml
vendored
@@ -55,10 +55,25 @@ jobs:
|
||||
run: aws s3 sync ./build-storybook/ s3://gdevelop-storybook/$(git rev-parse --abbrev-ref HEAD)/latest/ --delete
|
||||
working-directory: newIDE/app
|
||||
|
||||
- name: Compute urls
|
||||
id: storybook_urls
|
||||
run: |
|
||||
echo "::set-output name=storybook_s3_url_commit::http://gdevelop-storybook.s3-website-us-east-1.amazonaws.com/$(git rev-parse --abbrev-ref HEAD)/commit/$(git rev-parse HEAD)/index.html"
|
||||
echo "::set-output name=storybook_s3_url_latest::http://gdevelop-storybook.s3-website-us-east-1.amazonaws.com/$(git rev-parse --abbrev-ref HEAD)/latest/index.html"
|
||||
|
||||
- name: Log urls to the Storybook
|
||||
run: |
|
||||
echo "Find the latest Storybook for this branch on http://gdevelop-storybook.s3-website-us-east-1.amazonaws.com/$(git rev-parse --abbrev-ref HEAD)/latest/index.html"
|
||||
echo "Find the Storybook for this commit on http://gdevelop-storybook.s3-website-us-east-1.amazonaws.com/$(git rev-parse --abbrev-ref HEAD)/commit/$(git rev-parse HEAD)/index.html"
|
||||
echo "Find the Storybook for this commit on ${{ steps.storybook_urls.outputs.storybook_s3_url_commit }}"
|
||||
echo "Find the latest Storybook for this branch on ${{ steps.storybook_urls.outputs.storybook_s3_url_latest }}"
|
||||
|
||||
- name: Update GitHub Check with Storybook URLs
|
||||
uses: LouisBrunner/checks-action@v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
check_id: ${{ github.run_id }}
|
||||
conclusion: success
|
||||
output: |
|
||||
{"summary": "## Links to storybook\n\n- For the commit: ${{ steps.storybook_urls.outputs.storybook_s3_url_commit }}\n- Under tag latest: ${{ steps.storybook_urls.outputs.storybook_s3_url_latest }}"}
|
||||
|
||||
# Publish on Chromatic, only when manually launched (too costly to run on every commit).
|
||||
- name: Publish Storybook to Chromatic
|
||||
|
26
.github/workflows/issues.yml
vendored
26
.github/workflows/issues.yml
vendored
@@ -1,5 +1,7 @@
|
||||
name: GDevelop Issues automatic workflow
|
||||
on: [issues]
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -25,4 +27,26 @@ jobs:
|
||||
type: "body"
|
||||
regex: ".*getAssociatedSettings is not a function.*"
|
||||
message: "Hi @${issue.user.login}! 👋 This issue was automatically closed as this seems to be a known bug. It can be solved by **closing entirely the web-app and opening it again**. This will allow the web-app to auto-update and the problem should be gone."
|
||||
autocomment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autocomment known 124 crash removing a node (new issue comment)
|
||||
if: contains(github.event.issue.body, 'The node to be removed is not a child of this node')
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hi @${{ github.actor }}!
|
||||
Thank you for taking the time to report this bug.
|
||||
|
||||
This bug is actually known but we currently lack information on how to reproduce it. Could you please tell us more about how it happened in the issue #3453?
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Autocomment known 124 crash removing a node (reference issue comment)
|
||||
if: contains(github.event.issue.body, 'The node to be removed is not a child of this node')
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
issue-number: 3453
|
||||
body: |
|
||||
The issue #${{ github.event.issue.number }} reported by @${{ github.actor }} seems to relate to this bug.
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
@@ -60,6 +60,8 @@ before_install:
|
||||
- sudo dpkg --force-all -i libstdc++6
|
||||
|
||||
install:
|
||||
# Ensure we use a recent version of Node.js (and npm)
|
||||
- nvm install v16 && nvm use v16
|
||||
#Get the correct version of gcc/g++
|
||||
- if [ "$CXX" = "g++" ]; then export CXX="g++-${GCC_VERSION}" CC="gcc-${GCC_VERSION}"; fi
|
||||
#Compile the tests only for GDCore
|
||||
|
@@ -83,7 +83,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
|
||||
_("Functions"),
|
||||
"res/function24.png",
|
||||
"res/function16.png")
|
||||
.AddParameter("string", "Parameter name")
|
||||
.AddParameter("functionParameterName", "Parameter name")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
@@ -93,7 +93,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
|
||||
_("Get function parameter (also called \"argument\") value"),
|
||||
_("Functions"),
|
||||
"res/function16.png")
|
||||
.AddParameter("string", "Parameter name");
|
||||
.AddParameter("functionParameterName", "Parameter name");
|
||||
|
||||
extension
|
||||
.AddStrExpression(
|
||||
@@ -102,7 +102,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
|
||||
_("Get function parameter (also called \"argument\") text "),
|
||||
_("Functions"),
|
||||
"res/function16.png")
|
||||
.AddParameter("string", "Parameter name");
|
||||
.AddParameter("functionParameterName", "Parameter name");
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -1197,7 +1197,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
.AddAction("Create",
|
||||
_("Create an object"),
|
||||
_("Create an object at specified position"),
|
||||
_("Create object _PARAM1_ at position _PARAM2_;_PARAM3_"),
|
||||
_("Create object _PARAM1_ at position _PARAM2_;_PARAM3_ (layer: _PARAM4_)"),
|
||||
_("Objects"),
|
||||
"res/actions/create24.png",
|
||||
"res/actions/create.png")
|
||||
@@ -1215,7 +1215,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
_("Among the objects of the specified group, this action will "
|
||||
"create the object with the specified name."),
|
||||
_("Among objects _PARAM1_, create object named _PARAM2_ at "
|
||||
"position _PARAM3_;_PARAM4_"),
|
||||
"position _PARAM3_;_PARAM4_ (layer: _PARAM5_)"),
|
||||
_("Objects"),
|
||||
"res/actions/create24.png",
|
||||
"res/actions/create.png")
|
||||
|
@@ -227,7 +227,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string", _("Expression generating the button to check"))
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Expression generating the mouse button to check"),
|
||||
"[\"Left\", \"Right\", \"Middle\"]")
|
||||
.SetParameterLongDescription(
|
||||
_("Possible values are Left, Right and Middle."))
|
||||
.MarkAsAdvanced();
|
||||
@@ -243,8 +245,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string",
|
||||
_("Expression generating the mouse button to check"))
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Expression generating the mouse button to check"),
|
||||
"[\"Left\", \"Right\", \"Middle\"]")
|
||||
.SetParameterLongDescription(
|
||||
_("Possible values are Left, Right and Middle."))
|
||||
.MarkAsAdvanced();
|
||||
|
@@ -361,7 +361,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
obj.AddAction("FlipX",
|
||||
_("Flip the object horizontally"),
|
||||
_("Flip the object horizontally"),
|
||||
_("Flip horizontally _PARAM0_ : _PARAM1_"),
|
||||
_("Flip horizontally _PARAM0_: _PARAM1_"),
|
||||
_("Effects"),
|
||||
"res/actions/flipX24.png",
|
||||
"res/actions/flipX.png")
|
||||
@@ -373,7 +373,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
obj.AddAction("FlipY",
|
||||
_("Flip the object vertically"),
|
||||
_("Flip the object vertically"),
|
||||
_("Flip vertically _PARAM0_ : _PARAM1_"),
|
||||
_("Flip vertically _PARAM0_: _PARAM1_"),
|
||||
_("Effects"),
|
||||
"res/actions/flipY24.png",
|
||||
"res/actions/flipY.png")
|
||||
|
@@ -28,7 +28,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Value of a scene variable"),
|
||||
_("Compare the value of a scene variable."),
|
||||
_("the scene variable _PARAM0_"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/conditions/var24.png",
|
||||
"res/conditions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -39,7 +39,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Text of a scene variable"),
|
||||
_("Compare the text of a scene variable."),
|
||||
_("the text of scene variable _PARAM0_"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/conditions/var24.png",
|
||||
"res/conditions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -51,7 +51,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Boolean value of a scene variable"),
|
||||
_("Compare the boolean value of a scene variable."),
|
||||
_("The boolean value of scene variable _PARAM0_ is _PARAM1_"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/conditions/var24.png",
|
||||
"res/conditions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -64,7 +64,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Child existence"),
|
||||
_("Check if the specified child of the scene variable exists."),
|
||||
_("Child _PARAM1_ of scene variable _PARAM0_ exists"),
|
||||
_("Variables/Collections/Structures"),
|
||||
_("Variables/Scene variables/Collections/Structures"),
|
||||
"res/conditions/var24.png",
|
||||
"res/conditions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -89,7 +89,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
"Test if a scene variable is defined",
|
||||
"Test if the scene variable exists.",
|
||||
"Scene variable _PARAM0_ is defined",
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/conditions/var24.png",
|
||||
"res/conditions/var.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
@@ -151,7 +151,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Value of a scene variable"),
|
||||
_("Change the value of a scene variable."),
|
||||
_("the scene variable _PARAM0_"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -162,7 +162,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("String of a scene variable"),
|
||||
_("Modify the text of a scene variable."),
|
||||
_("the text of scene variable _PARAM0_"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -174,7 +174,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Boolean value of a scene variable"),
|
||||
_("Modify the boolean value of a scene variable."),
|
||||
_("Set the boolean value of scene variable _PARAM0_ to _PARAM1_"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/conditions/var24.png",
|
||||
"res/conditions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -187,7 +187,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("If it was true, it will become false, and if it was "
|
||||
"false it will become true."),
|
||||
_("Toggle the boolean value of scene variable _PARAM0_"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/conditions/var24.png",
|
||||
"res/conditions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"));
|
||||
@@ -245,7 +245,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Remove a child"),
|
||||
_("Remove a child from a scene variable."),
|
||||
_("Remove child _PARAM1_ from scene variable _PARAM0_"),
|
||||
_("Variables/Collections/Structures"),
|
||||
_("Variables/Scene variables/Collections/Structures"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -269,7 +269,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Clear scene variable"),
|
||||
_("Remove all the children from the scene variable."),
|
||||
_("Clear children from scene variable _PARAM0_"),
|
||||
_("Variables/Collections"),
|
||||
_("Variables/Scene variables/Collections"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -291,7 +291,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Append variable to a scene array"),
|
||||
_("Appends a variable at the end of a scene array variable."),
|
||||
_("Append variable _PARAM1_ to array variable _PARAM0_"),
|
||||
_("Variables/Collections/Arrays"),
|
||||
_("Variables/Scene variables/Collections/Arrays"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Array variable"))
|
||||
@@ -304,7 +304,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Append a string to a scene array"),
|
||||
_("Appends a string at the end of a scene array variable."),
|
||||
_("Append string _PARAM1_ to array variable _PARAM0_"),
|
||||
_("Variables/Collections/Arrays"),
|
||||
_("Variables/Scene variables/Collections/Arrays"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Array variable"))
|
||||
@@ -316,7 +316,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Append a number to a scene array"),
|
||||
_("Appends a number at the end of a scene array variable."),
|
||||
_("Append number _PARAM1_ to array variable _PARAM0_"),
|
||||
_("Variables/Collections/Arrays"),
|
||||
_("Variables/Scene variables/Collections/Arrays"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Array variable"))
|
||||
@@ -328,7 +328,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Append a boolean to a scene array"),
|
||||
_("Appends a boolean at the end of a scene array variable."),
|
||||
_("Append boolean _PARAM1_ to array variable _PARAM0_"),
|
||||
_("Variables/Collections/Arrays"),
|
||||
_("Variables/Scene variables/Collections/Arrays"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Array variable"))
|
||||
@@ -341,7 +341,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
_("Remove variable from a scene array (by index)"),
|
||||
_("Removes a variable at the specified index of a scene array variable."),
|
||||
_("Remove variable at index _PARAM1_ from scene array variable _PARAM0_"),
|
||||
_("Variables/Collections/Arrays"),
|
||||
_("Variables/Scene variables/Collections/Arrays"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"))
|
||||
@@ -414,7 +414,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
.AddExpression("GlobalVariableChildCount",
|
||||
_("Number of children of a global variable"),
|
||||
_("Number of children of a global variable"),
|
||||
_("Variables"),
|
||||
_("Variables/Global variables"),
|
||||
"res/actions/var.png")
|
||||
.AddParameter("globalvar", _("Variable"));
|
||||
|
||||
@@ -422,7 +422,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
.AddExpression("VariableChildCount",
|
||||
_("Number of children of a scene variable"),
|
||||
_("Number of children of a scene variable"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"));
|
||||
|
||||
@@ -430,7 +430,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
.AddExpression("Variable",
|
||||
_("Value of a scene variable"),
|
||||
_("Value of a scene variable"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"));
|
||||
|
||||
@@ -438,7 +438,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
.AddStrExpression("VariableString",
|
||||
_("Text of a scene variable"),
|
||||
_("Text of a scene variable"),
|
||||
_("Variables"),
|
||||
_("Variables/Scene variables"),
|
||||
"res/actions/var.png")
|
||||
.AddParameter("scenevar", _("Variable"));
|
||||
|
||||
@@ -446,7 +446,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
.AddExpression("GlobalVariable",
|
||||
_("Value of a global variable"),
|
||||
_("Value of a global variable"),
|
||||
_("Variables"),
|
||||
_("Variables/Global variables"),
|
||||
"res/actions/var.png")
|
||||
.AddParameter("globalvar", _("Name of the global variable"));
|
||||
|
||||
@@ -454,7 +454,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
|
||||
.AddStrExpression("GlobalVariableString",
|
||||
_("Text of a global variable"),
|
||||
_("Text of a global variable"),
|
||||
_("Variables"),
|
||||
_("Variables/Global variables"),
|
||||
"res/actions/var.png")
|
||||
.AddParameter("globalvar", _("Variable"));
|
||||
}
|
||||
|
@@ -194,7 +194,8 @@ class GD_CORE_API ParameterMetadata {
|
||||
parameterType == "objectEffectName" ||
|
||||
parameterType == "objectEffectParameterName" ||
|
||||
parameterType == "objectPointName" ||
|
||||
parameterType == "objectAnimationName";
|
||||
parameterType == "objectAnimationName" ||
|
||||
parameterType == "functionParameterName";
|
||||
} else if (type == "variable") {
|
||||
return parameterType == "objectvar" || parameterType == "globalvar" ||
|
||||
parameterType == "scenevar";
|
||||
|
@@ -19,4 +19,34 @@ class GD_CORE_API EventsListUnfolder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void FoldAll(gd::EventsList& list) {
|
||||
for (size_t i = 0; i < list.size(); ++i) {
|
||||
gd::BaseEvent& event = list[i];
|
||||
event.SetFolded(true);
|
||||
if (event.CanHaveSubEvents() && event.GetSubEvents().size() > 0) {
|
||||
FoldAll(event.GetSubEvents());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Recursively unfold all the events until a certain level of depth.
|
||||
* 0 is the top level. If you want to unfold all events regardless of its depth,
|
||||
* use `maxLevel = -1`. `currentLevel` is used for the recursion.
|
||||
*/
|
||||
static void UnfoldToLevel(gd::EventsList& list,
|
||||
const int8_t maxLevel,
|
||||
const std::size_t currentLevel = 0) {
|
||||
if (maxLevel >= 0 && currentLevel > maxLevel) return;
|
||||
|
||||
for (size_t i = 0; i < list.size(); ++i) {
|
||||
gd::BaseEvent& event = list[i];
|
||||
event.SetFolded(false);
|
||||
if (event.CanHaveSubEvents() && event.GetSubEvents().size() > 0 &&
|
||||
(maxLevel == -1 || currentLevel <= maxLevel)) {
|
||||
UnfoldToLevel(event.GetSubEvents(), maxLevel, currentLevel + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
*/
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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);
|
||||
};
|
||||
|
@@ -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"));
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -3,7 +3,6 @@
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#if defined(GD_IDE_ONLY)
|
||||
#include "EventsFunctionsExtension.h"
|
||||
|
||||
#include "EventsBasedBehavior.h"
|
||||
@@ -51,6 +50,11 @@ void EventsFunctionsExtension::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("description", description);
|
||||
element.SetAttribute("name", name);
|
||||
element.SetAttribute("fullName", fullName);
|
||||
if (!originName.empty() || !originIdentifier.empty()) {
|
||||
element.AddChild("origin")
|
||||
.SetAttribute("name", originName)
|
||||
.SetAttribute("identifier", originIdentifier);
|
||||
}
|
||||
auto& tagsElement = element.AddChild("tags");
|
||||
tagsElement.ConsiderAsArray();
|
||||
for (const auto& tag : tags) {
|
||||
@@ -88,6 +92,14 @@ void EventsFunctionsExtension::UnserializeFrom(
|
||||
iconUrl = element.GetStringAttribute("iconUrl");
|
||||
helpPath = element.GetStringAttribute("helpPath");
|
||||
|
||||
if (element.HasChild("origin")) {
|
||||
gd::String originName =
|
||||
element.GetChild("origin").GetStringAttribute("name", "");
|
||||
gd::String originIdentifier =
|
||||
element.GetChild("origin").GetStringAttribute("identifier", "");
|
||||
SetOrigin(originName, originIdentifier);
|
||||
}
|
||||
|
||||
tags.clear();
|
||||
auto& tagsElement = element.GetChild("tags");
|
||||
if (!tagsElement.IsValueUndefined()) {
|
||||
@@ -138,5 +150,3 @@ bool EventsFunctionsExtension::IsExtensionLifecycleEventsFunction(
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
||||
#endif
|
||||
|
@@ -3,7 +3,6 @@
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#if defined(GD_IDE_ONLY)
|
||||
#ifndef GDCORE_EVENTSFUNCTIONEXTENSION_H
|
||||
#define GDCORE_EVENTSFUNCTIONEXTENSION_H
|
||||
|
||||
@@ -140,6 +139,23 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
return eventsBasedBehaviors;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Sets an extension origin. This method is not present since the
|
||||
* beginning so the projects created before that will have extensions
|
||||
* installed from the store without an origin. Keep that in mind when creating
|
||||
* features that rely on an extension's origin.
|
||||
*/
|
||||
virtual void SetOrigin(const gd::String& originName_,
|
||||
const gd::String& originIdentifier_) {
|
||||
originName = originName_;
|
||||
originIdentifier = originIdentifier_;
|
||||
}
|
||||
|
||||
virtual const gd::String& GetOriginName() const { return originName; }
|
||||
virtual const gd::String& GetOriginIdentifier() const {
|
||||
return originIdentifier;
|
||||
}
|
||||
|
||||
/** \name Dependencies
|
||||
*/
|
||||
///@{
|
||||
@@ -226,6 +242,8 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
std::vector<gd::String> authorIds;
|
||||
gd::String author;
|
||||
gd::String previewIconUrl;
|
||||
gd::String originName;
|
||||
gd::String originIdentifier;
|
||||
gd::String iconUrl;
|
||||
gd::String helpPath; ///< The relative path to the help for this extension in
|
||||
///< the documentation (or an absolute URL).
|
||||
@@ -236,4 +254,3 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
} // namespace gd
|
||||
|
||||
#endif // GDCORE_EVENTSFUNCTIONEXTENSION_H
|
||||
#endif
|
||||
|
@@ -19,6 +19,7 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
|
||||
element.AddChild("type").SetStringValue(type);
|
||||
element.AddChild("label").SetStringValue(label);
|
||||
element.AddChild("description").SetStringValue(description);
|
||||
element.AddChild("group").SetStringValue(group);
|
||||
SerializerElement& extraInformationElement =
|
||||
element.AddChild("extraInformation");
|
||||
extraInformationElement.ConsiderAsArray();
|
||||
@@ -33,6 +34,7 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
type = element.GetChild("type").GetStringValue();
|
||||
label = element.GetChild("label").GetStringValue();
|
||||
description = element.GetChild("description").GetStringValue();
|
||||
group = element.GetChild("group").GetStringValue();
|
||||
|
||||
extraInformation.clear();
|
||||
const SerializerElement& extraInformationElement =
|
||||
|
@@ -76,6 +76,14 @@ class GD_CORE_API PropertyDescriptor {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Change the group where this property is displayed to the user, if any.
|
||||
*/
|
||||
PropertyDescriptor& SetGroup(gd::String group_) {
|
||||
group = group_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set and replace the additional information for the property.
|
||||
*/
|
||||
@@ -100,6 +108,7 @@ class GD_CORE_API PropertyDescriptor {
|
||||
const gd::String& GetType() const { return type; }
|
||||
const gd::String& GetLabel() const { return label; }
|
||||
const gd::String& GetDescription() const { return description; }
|
||||
const gd::String& GetGroup() const { return group; }
|
||||
|
||||
const std::vector<gd::String>& GetExtraInfo() const {
|
||||
return extraInformation;
|
||||
@@ -153,6 +162,7 @@ class GD_CORE_API PropertyDescriptor {
|
||||
///< the class responsible for updating the property grid.
|
||||
gd::String label; //< The user-friendly property name
|
||||
gd::String description; //< The user-friendly property description
|
||||
gd::String group; //< The user-friendly property group
|
||||
std::vector<gd::String>
|
||||
extraInformation; ///< Can be used to store for example the available
|
||||
///< choices, if a property is a displayed as a combo
|
||||
|
@@ -24,12 +24,9 @@ gd::String Resource::badStr;
|
||||
|
||||
Resource ResourcesManager::badResource;
|
||||
gd::String ResourcesManager::badResourceName;
|
||||
#if defined(GD_IDE_ONLY)
|
||||
ResourceFolder ResourcesManager::badFolder;
|
||||
Resource ResourceFolder::badResource;
|
||||
#endif
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void ResourceFolder::Init(const ResourceFolder& other) {
|
||||
name = other.name;
|
||||
|
||||
@@ -38,19 +35,16 @@ void ResourceFolder::Init(const ResourceFolder& other) {
|
||||
resources.push_back(std::shared_ptr<Resource>(other.resources[i]->Clone()));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ResourcesManager::Init(const ResourcesManager& other) {
|
||||
resources.clear();
|
||||
for (std::size_t i = 0; i < other.resources.size(); ++i) {
|
||||
resources.push_back(std::shared_ptr<Resource>(other.resources[i]->Clone()));
|
||||
}
|
||||
#if defined(GD_IDE_ONLY)
|
||||
folders.clear();
|
||||
for (std::size_t i = 0; i < other.folders.size(); ++i) {
|
||||
folders.push_back(other.folders[i]);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Resource& ResourcesManager::GetResource(const gd::String& name) {
|
||||
@@ -147,7 +141,6 @@ std::vector<gd::String> ResourcesManager::FindFilesNotInResources(
|
||||
return filesNotInResources;
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
std::map<gd::String, gd::PropertyDescriptor> Resource::GetProperties() const {
|
||||
std::map<gd::String, gd::PropertyDescriptor> nothing;
|
||||
return nothing;
|
||||
@@ -443,9 +436,7 @@ void ResourcesManager::RemoveResource(const gd::String& name) {
|
||||
for (std::size_t i = 0; i < folders.size(); ++i)
|
||||
folders[i].RemoveResource(name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void ResourceFolder::UnserializeFrom(const SerializerElement& element,
|
||||
gd::ResourcesManager& parentManager) {
|
||||
name = element.GetStringAttribute("name");
|
||||
@@ -470,7 +461,6 @@ void ResourceFolder::SerializeTo(SerializerElement& element) const {
|
||||
.SetAttribute("name", resources[i]->GetName());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ResourcesManager::UnserializeFrom(const SerializerElement& element) {
|
||||
resources.clear();
|
||||
@@ -500,7 +490,6 @@ void ResourcesManager::UnserializeFrom(const SerializerElement& element) {
|
||||
resources.push_back(resource);
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
folders.clear();
|
||||
const SerializerElement& resourcesFoldersElement =
|
||||
element.GetChild("resourceFolders", 0, "ResourceFolders");
|
||||
@@ -511,10 +500,8 @@ void ResourcesManager::UnserializeFrom(const SerializerElement& element) {
|
||||
|
||||
folders.push_back(folder);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void ResourcesManager::SerializeTo(SerializerElement& element) const {
|
||||
SerializerElement& resourcesElement = element.AddChild("resources");
|
||||
resourcesElement.ConsiderAsArrayOf("resource");
|
||||
@@ -543,7 +530,6 @@ void ResourcesManager::SerializeTo(SerializerElement& element) const {
|
||||
for (std::size_t i = 0; i < folders.size(); ++i)
|
||||
folders[i].SerializeTo(resourcesFoldersElement.AddChild("folder"));
|
||||
}
|
||||
#endif
|
||||
|
||||
void ImageResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -560,14 +546,12 @@ void ImageResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void ImageResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("alwaysLoaded", alwaysLoaded);
|
||||
element.SetAttribute("smoothed", smooth);
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
#endif
|
||||
|
||||
void AudioResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -584,14 +568,12 @@ void AudioResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetPreloadAsSound(element.GetBoolAttribute("preloadAsSound"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void AudioResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
element.SetAttribute("preloadAsMusic", PreloadAsMusic());
|
||||
element.SetAttribute("preloadAsSound", PreloadAsSound());
|
||||
}
|
||||
#endif
|
||||
|
||||
void FontResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -606,12 +588,10 @@ void FontResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void FontResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
#endif
|
||||
|
||||
void VideoResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -626,12 +606,10 @@ void VideoResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void VideoResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
#endif
|
||||
|
||||
void JsonResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -647,7 +625,6 @@ void JsonResource::UnserializeFrom(const SerializerElement& element) {
|
||||
DisablePreload(element.GetBoolAttribute("disablePreload", false));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void JsonResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
@@ -672,7 +649,6 @@ bool JsonResource::UpdateProperty(const gd::String& name,
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void BitmapFontResource::SetFile(const gd::String& newFile) {
|
||||
file = newFile;
|
||||
@@ -687,14 +663,11 @@ void BitmapFontResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
void BitmapFontResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
ResourceFolder::ResourceFolder(const ResourceFolder& other) { Init(other); }
|
||||
|
||||
ResourceFolder& ResourceFolder::operator=(const ResourceFolder& other) {
|
||||
@@ -702,7 +675,6 @@ ResourceFolder& ResourceFolder::operator=(const ResourceFolder& other) {
|
||||
|
||||
return *this;
|
||||
}
|
||||
#endif
|
||||
|
||||
ResourcesManager::ResourcesManager(const ResourcesManager& other) {
|
||||
Init(other);
|
||||
|
@@ -104,7 +104,6 @@ class GD_CORE_API Resource {
|
||||
*/
|
||||
virtual const gd::String& GetMetadata() const { return metadata; }
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
/** \name Resources properties
|
||||
* Reading and updating resources properties
|
||||
*/
|
||||
@@ -136,7 +135,6 @@ class GD_CORE_API Resource {
|
||||
return false;
|
||||
};
|
||||
///@}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Serialize the object
|
||||
@@ -186,7 +184,6 @@ class GD_CORE_API ImageResource : public Resource {
|
||||
*/
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor> GetProperties() const override;
|
||||
@@ -196,7 +193,6 @@ class GD_CORE_API ImageResource : public Resource {
|
||||
* \brief Serialize the object
|
||||
*/
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Unserialize the objectt.
|
||||
@@ -238,14 +234,12 @@ class GD_CORE_API AudioResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor> GetProperties() const override;
|
||||
bool UpdateProperty(const gd::String& name, const gd::String& value) override;
|
||||
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -292,10 +286,8 @@ class GD_CORE_API FontResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -320,10 +312,8 @@ class GD_CORE_API VideoResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -348,14 +338,12 @@ class GD_CORE_API JsonResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor> GetProperties() const override;
|
||||
bool UpdateProperty(const gd::String& name, const gd::String& value) override;
|
||||
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -391,10 +379,8 @@ class GD_CORE_API BitmapFontResource : public Resource {
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
virtual bool UseFile() override { return true; }
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
#endif
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
@@ -463,7 +449,6 @@ class GD_CORE_API ResourcesManager {
|
||||
*/
|
||||
std::vector<gd::String> FindFilesNotInResources(const std::vector<gd::String>& filesToCheck) const;
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
/**
|
||||
* \brief Return a (smart) pointer to a resource.
|
||||
*/
|
||||
@@ -557,7 +542,6 @@ class GD_CORE_API ResourcesManager {
|
||||
* \brief Serialize the object
|
||||
*/
|
||||
void SerializeTo(SerializerElement& element) const;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Unserialize the objectt.
|
||||
@@ -568,18 +552,13 @@ class GD_CORE_API ResourcesManager {
|
||||
void Init(const ResourcesManager& other);
|
||||
|
||||
std::vector<std::shared_ptr<Resource> > resources;
|
||||
#if defined(GD_IDE_ONLY)
|
||||
std::vector<ResourceFolder> folders;
|
||||
#endif
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
static ResourceFolder badFolder;
|
||||
#endif
|
||||
static Resource badResource;
|
||||
static gd::String badResourceName;
|
||||
};
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
class GD_CORE_API ResourceFolder {
|
||||
public:
|
||||
ResourceFolder(){};
|
||||
@@ -654,7 +633,6 @@ class GD_CORE_API ResourceFolder {
|
||||
void Init(const ResourceFolder& other);
|
||||
static Resource badResource;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace gd
|
||||
|
||||
|
@@ -179,7 +179,8 @@ module.exports = {
|
||||
.setValue(
|
||||
behaviorContent.getBoolAttribute('property2') ? 'true' : 'false'
|
||||
)
|
||||
.setType('Boolean');
|
||||
.setType('Boolean')
|
||||
.setGroup(_('Look and Feel'));
|
||||
|
||||
return behaviorProperties;
|
||||
};
|
||||
|
@@ -415,6 +415,7 @@ module.exports = {
|
||||
_('Filesystem/Windows, Linux, MacOS'),
|
||||
'JsPlatform/Extensions/filesystem_folder32.png'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/FileSystem/filesystemtools.js')
|
||||
.setFunctionName('gdjs.fileSystem.getUserHomePath');
|
||||
|
@@ -150,7 +150,9 @@ namespace gdjs {
|
||||
* Get the path to the user's home folder (on Windows `C:\Users\<USERNAME>\` for example).
|
||||
* @return The path to user's "home" folder
|
||||
*/
|
||||
export const getUserHomePath = function (runtimeScene): string {
|
||||
export const getUserHomePath = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): string {
|
||||
const electron = runtimeScene.getGame().getRenderer().getElectron();
|
||||
if (electron) {
|
||||
return electron.remote.app.getPath('home') || '';
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}`;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
1
Extensions/P2P/A_peer.js
vendored
1
Extensions/P2P/A_peer.js
vendored
@@ -69,3 +69,4 @@ var t=require("./bufferbuilder").BufferBuilder,e=require("./bufferbuilder").bina
|
||||
},{"eventemitter3":"JJlS","./util":"BHXf","./logger":"WOs9","./socket":"wJlv","./mediaconnection":"dbHP","./dataconnection":"GBTQ","./enums":"ZRYf","./api":"in7L"}],"iTK6":[function(require,module,exports) {
|
||||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("./util"),r=require("./peer");exports.peerjs={Peer:r.Peer,util:e.util},exports.default=r.Peer,window.peerjs=exports.peerjs,window.Peer=r.Peer;
|
||||
},{"./util":"BHXf","./peer":"Hxpd"}]},{},["iTK6"], null)
|
||||
//# sourceMappingURL=A_peer.js.map
|
1
Extensions/P2P/A_peer.js.map
Normal file
1
Extensions/P2P/A_peer.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -375,15 +375,14 @@ namespace gdjs {
|
||||
key: string,
|
||||
ssl: boolean
|
||||
) => {
|
||||
peerConfig = {
|
||||
debug: 1,
|
||||
Object.assign(peerConfig, {
|
||||
host,
|
||||
port,
|
||||
path,
|
||||
secure: ssl,
|
||||
// All servers have "peerjs" as default key
|
||||
key: key.length === 0 ? 'peerjs' : key,
|
||||
};
|
||||
});
|
||||
loadPeerJS();
|
||||
};
|
||||
|
||||
@@ -394,6 +393,26 @@ namespace gdjs {
|
||||
*/
|
||||
export const useDefaultBrokerServer = loadPeerJS;
|
||||
|
||||
/**
|
||||
* Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.
|
||||
* @param urls The URL of the STUN/TURN server.
|
||||
* @param username An optional username to send to the server.
|
||||
* @param credential An optional password to send to the server.
|
||||
*/
|
||||
export const useCustomICECandidate = (
|
||||
urls: string,
|
||||
username?: string,
|
||||
credential?: string
|
||||
) => {
|
||||
peerConfig.config = peerConfig.config || {};
|
||||
peerConfig.config.iceServers = peerConfig.config.iceServers || [];
|
||||
peerConfig.config.iceServers.push({
|
||||
urls,
|
||||
username,
|
||||
credential,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Overrides the default peer ID. Must be called before connecting to a broker.
|
||||
* Overriding the ID may have unwanted consequences. Do not use this feature
|
||||
|
@@ -154,6 +154,30 @@ module.exports = {
|
||||
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
|
||||
.setFunctionName('gdjs.evtTools.p2p.useCustomBrokerServer');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'UseOwnICEServer',
|
||||
_('Use a custom ICE server'),
|
||||
_(
|
||||
'Disables the default ICE (STUN or TURN) servers list and use one of your own. ' +
|
||||
'Note that it is recommended to add at least 1 self-hosted STUN and TURN server ' +
|
||||
'for games that are not over LAN but over the internet. ' +
|
||||
'This action can be used multiple times to add multiple servers. ' +
|
||||
'This action needs to be called BEFORE connecting to the broker server.'
|
||||
),
|
||||
_('Use ICE server _PARAM0_ (username: _PARAM1_, password: _PARAM2_)'),
|
||||
_('P2P (experimental)'),
|
||||
'JsPlatform/Extensions/p2picon.svg',
|
||||
'JsPlatform/Extensions/p2picon.svg'
|
||||
)
|
||||
.addParameter('string', _('URL to the ICE server'), '', false)
|
||||
.addParameter('string', _('(Optional) Username'), '', true)
|
||||
.addParameter('string', _('(Optional) Password'), '', true)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/P2P/A_peer.js')
|
||||
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
|
||||
.setFunctionName('gdjs.evtTools.p2p.useCustomICECandidate');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'UseDefaultBroker',
|
||||
|
@@ -168,7 +168,7 @@ void ExtensionSubDeclaration2(gd::ObjectMetadata& obj) {
|
||||
.UseStandardRelationalOperatorParameters("number");
|
||||
|
||||
obj.AddAction("ParticleSize1",
|
||||
_("SIze, parameter 1"),
|
||||
_("Size, parameter 1"),
|
||||
_("Modify parameter 1 of the size of particles"),
|
||||
_("the parameter 1 of size"),
|
||||
_("Common"),
|
||||
|
@@ -39,23 +39,23 @@ std::map<gd::String, gd::PropertyDescriptor> PathfindingBehavior::GetProperties(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("acceleration")));
|
||||
properties[_("Max. speed")].SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxSpeed")));
|
||||
properties[_("Rotate speed")].SetValue(
|
||||
properties[_("Rotate speed")].SetGroup(_("Rotation")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angularMaxSpeed")));
|
||||
properties[_("Rotate object")]
|
||||
properties[_("Rotate object")].SetGroup(_("Rotation"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("rotateObject") ? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Angle offset")].SetValue(
|
||||
properties[_("Angle offset")].SetGroup(_("Rotation")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angleOffset")));
|
||||
properties[_("Virtual cell width")].SetValue(
|
||||
properties[_("Virtual cell width")].SetGroup(_("Virtual Grid")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("cellWidth", 0)));
|
||||
properties[_("Virtual cell height")].SetValue(
|
||||
properties[_("Virtual cell height")].SetGroup(_("Virtual Grid")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("cellHeight", 0)));
|
||||
properties[_("Virtual grid X offset")].SetValue(
|
||||
properties[_("Virtual grid X offset")].SetGroup(_("Virtual Grid")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("gridOffsetX", 0)));
|
||||
properties[_("Virtual grid Y offset")].SetValue(
|
||||
properties[_("Virtual grid Y offset")].SetGroup(_("Virtual Grid")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("gridOffsetY", 0)));
|
||||
properties[_("Extra border size")].SetValue(
|
||||
properties[_("Extra border size")].SetGroup(_("Collision")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("extraBorder")));
|
||||
|
||||
return properties;
|
||||
|
@@ -28,8 +28,8 @@ namespace gdjs {
|
||||
_pathFound: boolean = false;
|
||||
_speed: float = 0;
|
||||
_angularSpeed: float = 0;
|
||||
_timeOnSegment: float = 0;
|
||||
_totalSegmentTime: float = 0;
|
||||
_distanceOnSegment: float = 0;
|
||||
_totalSegmentDistance: float = 0;
|
||||
_currentSegment: integer = 0;
|
||||
_reachedEnd: boolean = false;
|
||||
_manager: PathfindingObstaclesManager;
|
||||
@@ -392,11 +392,11 @@ namespace gdjs {
|
||||
const pathY =
|
||||
this._path[this._currentSegment + 1][1] -
|
||||
this._path[this._currentSegment][1];
|
||||
this._totalSegmentTime = Math.sqrt(pathX * pathX + pathY * pathY);
|
||||
this._timeOnSegment = 0;
|
||||
this._totalSegmentDistance = Math.sqrt(pathX * pathX + pathY * pathY);
|
||||
this._distanceOnSegment = 0;
|
||||
this._reachedEnd = false;
|
||||
this._movementAngle =
|
||||
((Math.atan2(pathY, pathX) * 180) / Math.PI + 360) % 360;
|
||||
(gdjs.toDegrees(Math.atan2(pathY, pathX)) + 360) % 360;
|
||||
} else {
|
||||
this._reachedEnd = true;
|
||||
this._speed = 0;
|
||||
@@ -408,58 +408,59 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
//Update the speed of the object
|
||||
// Update the speed of the object
|
||||
const timeDelta = this.owner.getElapsedTime(runtimeScene) / 1000;
|
||||
this._speed += this._acceleration * timeDelta;
|
||||
if (this._speed > this._maxSpeed) {
|
||||
this._speed = this._maxSpeed;
|
||||
const previousSpeed = this._speed;
|
||||
if (this._speed !== this._maxSpeed) {
|
||||
this._speed += this._acceleration * timeDelta;
|
||||
if (this._speed > this._maxSpeed) {
|
||||
this._speed = this._maxSpeed;
|
||||
}
|
||||
}
|
||||
this._angularSpeed = this._angularMaxSpeed;
|
||||
|
||||
//Update the time on the segment and change segment if needed
|
||||
this._timeOnSegment += this._speed * timeDelta;
|
||||
// Update the time on the segment and change segment if needed
|
||||
// Use a Verlet integration to be frame rate independent.
|
||||
this._distanceOnSegment +=
|
||||
((this._speed + previousSpeed) / 2) * timeDelta;
|
||||
const remainingDistanceOnSegment =
|
||||
this._totalSegmentDistance - this._distanceOnSegment;
|
||||
if (
|
||||
this._timeOnSegment >= this._totalSegmentTime &&
|
||||
remainingDistanceOnSegment <= 0 &&
|
||||
this._currentSegment < this._path.length
|
||||
) {
|
||||
this._enterSegment(this._currentSegment + 1);
|
||||
this._distanceOnSegment = -remainingDistanceOnSegment;
|
||||
}
|
||||
|
||||
//Position object on the segment and update its angle
|
||||
// Position object on the segment and update its angle
|
||||
let newPos = [0, 0];
|
||||
let pathAngle = this.owner.getAngle();
|
||||
if (this._currentSegment < this._path.length - 1) {
|
||||
newPos[0] = gdjs.evtTools.common.lerp(
|
||||
this._path[this._currentSegment][0],
|
||||
this._path[this._currentSegment + 1][0],
|
||||
this._timeOnSegment / this._totalSegmentTime
|
||||
this._distanceOnSegment / this._totalSegmentDistance
|
||||
);
|
||||
newPos[1] = gdjs.evtTools.common.lerp(
|
||||
this._path[this._currentSegment][1],
|
||||
this._path[this._currentSegment + 1][1],
|
||||
this._timeOnSegment / this._totalSegmentTime
|
||||
this._distanceOnSegment / this._totalSegmentDistance
|
||||
);
|
||||
pathAngle =
|
||||
gdjs.toDegrees(
|
||||
Math.atan2(
|
||||
this._path[this._currentSegment + 1][1] -
|
||||
this._path[this._currentSegment][1],
|
||||
this._path[this._currentSegment + 1][0] -
|
||||
this._path[this._currentSegment][0]
|
||||
)
|
||||
) + this._angleOffset;
|
||||
if (
|
||||
this._rotateObject &&
|
||||
this.owner.getAngle() !== this._movementAngle + this._angleOffset
|
||||
) {
|
||||
this.owner.rotateTowardAngle(
|
||||
this._movementAngle + this._angleOffset,
|
||||
this._angularSpeed,
|
||||
runtimeScene
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newPos = this._path[this._path.length - 1];
|
||||
}
|
||||
this.owner.setX(newPos[0]);
|
||||
this.owner.setY(newPos[1]);
|
||||
if (this._rotateObject) {
|
||||
this.owner.rotateTowardAngle(
|
||||
pathAngle,
|
||||
this._angularSpeed,
|
||||
runtimeScene
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
doStepPostEvents(runtimeScene: gdjs.RuntimeScene) {}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
// @ts-check
|
||||
describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
describe.only('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
const epsilon = 1 / (2 << 16);
|
||||
// tests cases where every collisionMethod has the same behavior.
|
||||
let doCommonPathFindingTests = (collisionMethod, allowDiagonals) => {
|
||||
const pathFindingName = 'auto1';
|
||||
|
||||
const createScene = () => {
|
||||
const createScene = (framePerSecond = 60) => {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
// @ts-ignore - missing properties.
|
||||
@@ -39,11 +40,14 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
objects: [],
|
||||
instances: [],
|
||||
});
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return (1 / 60) * 1000;
|
||||
};
|
||||
setFramePerSecond(runtimeScene, framePerSecond);
|
||||
return runtimeScene;
|
||||
};
|
||||
const setFramePerSecond = (runtimeScene, framePerSecond) => {
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return 1000 / framePerSecond;
|
||||
};
|
||||
};
|
||||
|
||||
const addPlayer = (runtimeScene) => {
|
||||
const player = new gdjs.RuntimeObject(runtimeScene, {
|
||||
@@ -156,6 +160,96 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
);
|
||||
});
|
||||
|
||||
if (allowDiagonals) {
|
||||
[20, 30, 60, 120].forEach((framePerSecond) => {
|
||||
describe(`(${framePerSecond} fps)`, function () {
|
||||
it('can move on the path at the right speed', function () {
|
||||
setFramePerSecond(runtimeScene, framePerSecond);
|
||||
const obstacle = addObstacle(runtimeScene);
|
||||
|
||||
obstacle.setPosition(600, 300);
|
||||
// To ensure obstacles are registered.
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
|
||||
player.setPosition(480, 300);
|
||||
player.getBehavior(pathFindingName).moveTo(runtimeScene, 720, 300);
|
||||
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).getNodeCount()
|
||||
).to.be.above(13);
|
||||
|
||||
// Move on the path and stop before the last 1/10 of second.
|
||||
for (let i = 0; i < (framePerSecond * 17) / 10; i++) {
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).destinationReached()
|
||||
).to.be(false);
|
||||
}
|
||||
// The position is the same no matter the frame rate.
|
||||
expect(player.getX()).to.be(720);
|
||||
expect(player.getY()).to.be.within(
|
||||
288.5786437626905 - epsilon,
|
||||
288.5786437626905 + epsilon
|
||||
);
|
||||
|
||||
// Let 1/10 of second pass,
|
||||
// because the calculus interval is not the same for each case.
|
||||
for (let i = 0; i < framePerSecond / 10; i++) {
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
}
|
||||
// The destination is reached for every frame rate within 1/10 of second.
|
||||
expect(player.getX()).to.be(720);
|
||||
expect(player.getY()).to.be(300);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).destinationReached()
|
||||
).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
[20, 30, 60, 120].forEach((framePerSecond) => {
|
||||
describe(`(${framePerSecond} fps)`, function () {
|
||||
it('can move on the path at the right speed', function () {
|
||||
setFramePerSecond(runtimeScene, framePerSecond);
|
||||
const obstacle = addObstacle(runtimeScene);
|
||||
|
||||
obstacle.setPosition(600, 300);
|
||||
// To ensure obstacles are registered.
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
|
||||
player.setPosition(480, 300);
|
||||
player.getBehavior(pathFindingName).moveTo(runtimeScene, 720, 300);
|
||||
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).getNodeCount()
|
||||
).to.be.above(13);
|
||||
|
||||
// Move on the path and stop before the last 1/10 of second.
|
||||
for (let i = 0; i < (framePerSecond * 20) / 10; i++) {
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).destinationReached()
|
||||
).to.be(false);
|
||||
}
|
||||
expect(player.getX()).to.be(710);
|
||||
expect(player.getY()).to.be.within(300 - epsilon, 300 + epsilon);
|
||||
|
||||
// Let 1/10 of second pass,
|
||||
// because the calculus interval is not the same for each case.
|
||||
for (let i = 0; i < (framePerSecond * 1) / 10; i++) {
|
||||
runtimeScene.renderAndStep(1000 / framePerSecond);
|
||||
}
|
||||
// The destination is reached for every frame rate within 1/10 of second.
|
||||
expect(player.getX()).to.be(720);
|
||||
expect(player.getY()).to.be(300);
|
||||
expect(
|
||||
player.getBehavior(pathFindingName).destinationReached()
|
||||
).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('can find a path between 2 obstacles', function () {
|
||||
const obstacleTop = addObstacle(runtimeScene);
|
||||
const obstacleBottom = addObstacle(runtimeScene);
|
||||
|
@@ -500,6 +500,20 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.MarkAsAdvanced()
|
||||
.SetFunctionName("SimulateControl");
|
||||
|
||||
aut.AddScopedCondition("IsUsingControl",
|
||||
_("Control pressed or simulated"),
|
||||
_("A control was applied from a default control or a simulated by an action."),
|
||||
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
|
||||
_("Controls"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "PlatformerObjectBehavior")
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Key"),
|
||||
"[\"Left\", \"Right\", \"Jump\", \"Ladder\", \"Release Ladder\", \"Up\", \"Down\"]")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddAction("IgnoreDefaultControls",
|
||||
_("Ignore default controls"),
|
||||
_("De/activate the use of default controls.\nIf deactivated, "
|
||||
|
@@ -147,6 +147,8 @@ class PlatformBehaviorJsExtension : public gd::PlatformExtension {
|
||||
"simulateReleasePlatformKey");
|
||||
autActions["PlatformBehavior::SimulateControl"].SetFunctionName(
|
||||
"simulateControl");
|
||||
autConditions["PlatformBehavior::PlatformerObjectBehavior::IsUsingControl"].SetFunctionName(
|
||||
"isUsingControl");
|
||||
autActions["PlatformBehavior::IgnoreDefaultControls"].SetFunctionName(
|
||||
"ignoreDefaultControls");
|
||||
}
|
||||
|
@@ -42,12 +42,12 @@ std::map<gd::String, gd::PropertyDescriptor> PlatformBehavior::GetProperties(
|
||||
.AddExtraInfo(_("Platform"))
|
||||
.AddExtraInfo(_("Jumpthru platform"))
|
||||
.AddExtraInfo(_("Ladder"));
|
||||
properties[_("Ledges can be grabbed")]
|
||||
properties[_("Ledges can be grabbed")].SetGroup(_("Ledge"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("canBeGrabbed", true)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Grab offset on Y axis")].SetValue(
|
||||
properties[_("Grab offset on Y axis")].SetGroup(_("Ledge")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("yGrabOffset")));
|
||||
|
||||
return properties;
|
||||
|
@@ -33,6 +33,7 @@ void PlatformerObjectBehavior::InitializeContent(
|
||||
behaviorContent.SetAttribute("canGrabPlatforms", false);
|
||||
behaviorContent.SetAttribute("yGrabOffset", 0);
|
||||
behaviorContent.SetAttribute("xGrabTolerance", 10);
|
||||
behaviorContent.SetAttribute("useLegacyTrajectory", false);
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
@@ -41,11 +42,11 @@ PlatformerObjectBehavior::GetProperties(
|
||||
const gd::SerializerElement& behaviorContent) const {
|
||||
std::map<gd::String, gd::PropertyDescriptor> properties;
|
||||
|
||||
properties[_("Gravity")].SetValue(
|
||||
properties[_("Gravity")].SetGroup(_("Jump")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("gravity")));
|
||||
properties[_("Jump speed")].SetValue(
|
||||
properties[_("Jump speed")].SetGroup(_("Jump")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("jumpSpeed")));
|
||||
properties["jumpSustainTime"]
|
||||
properties["jumpSustainTime"].SetGroup(_("Jump"))
|
||||
.SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("jumpSustainTime", 0)))
|
||||
.SetLabel(_("Jump sustain time"))
|
||||
@@ -53,32 +54,39 @@ PlatformerObjectBehavior::GetProperties(
|
||||
_("Maximum time (in seconds) during which the jump strength is "
|
||||
"sustained if the jump key is held - allowing variable height "
|
||||
"jumps."));
|
||||
properties[_("Max. falling speed")].SetValue(
|
||||
properties[_("Max. falling speed")].SetGroup(_("Jump")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxFallingSpeed")));
|
||||
properties[_("Ladder climbing speed")].SetValue(gd::String::From(
|
||||
properties[_("Ladder climbing speed")].SetGroup(_("Ladder")).SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("ladderClimbingSpeed", 150)));
|
||||
properties[_("Acceleration")].SetValue(
|
||||
properties[_("Acceleration")].SetGroup(_("Walk")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("acceleration")));
|
||||
properties[_("Deceleration")].SetValue(
|
||||
properties[_("Deceleration")].SetGroup(_("Walk")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("deceleration")));
|
||||
properties[_("Max. speed")].SetValue(
|
||||
properties[_("Max. speed")].SetGroup(_("Walk")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxSpeed")));
|
||||
properties[_("Default controls")]
|
||||
.SetValue(behaviorContent.GetBoolAttribute("ignoreDefaultControls")
|
||||
? "false"
|
||||
: "true")
|
||||
.SetType("Boolean");
|
||||
properties[_("Slope max. angle")].SetValue(
|
||||
properties[_("Slope max. angle")].SetGroup(_("Walk")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("slopeMaxAngle")));
|
||||
properties[_("Can grab platform ledges")]
|
||||
.SetGroup(_("Ledge"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("canGrabPlatforms", false)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Grab offset on Y axis")].SetValue(
|
||||
properties[_("Grab offset on Y axis")].SetGroup(_("Ledge")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("yGrabOffset")));
|
||||
properties[_("Grab tolerance on X axis")].SetValue(gd::String::From(
|
||||
properties[_("Grab tolerance on X axis")].SetGroup(_("Ledge")).SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("xGrabTolerance", 10)));
|
||||
properties[_("Use frame per second dependent trajectories (deprecated)")]
|
||||
.SetGroup(_("Jump"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("useLegacyTrajectory", true)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
return properties;
|
||||
}
|
||||
|
||||
@@ -90,6 +98,8 @@ bool PlatformerObjectBehavior::UpdateProperty(
|
||||
behaviorContent.SetAttribute("ignoreDefaultControls", (value == "0"));
|
||||
else if (name == _("Can grab platform ledges"))
|
||||
behaviorContent.SetAttribute("canGrabPlatforms", (value == "1"));
|
||||
else if (name == _("Use frame per second dependent trajectories (deprecated)"))
|
||||
behaviorContent.SetAttribute("useLegacyTrajectory", (value == "1"));
|
||||
else if (name == _("Grab offset on Y axis"))
|
||||
behaviorContent.SetAttribute("yGrabOffset", value.To<double>());
|
||||
else {
|
||||
|
@@ -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) {
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
783
Extensions/PlatformBehavior/tests/FlatFloorPlatformer.spec.js
Normal file
783
Extensions/PlatformBehavior/tests/FlatFloorPlatformer.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
1317
Extensions/PlatformBehavior/tests/JumpAndFallingPlatformer.spec.js
Normal file
1317
Extensions/PlatformBehavior/tests/JumpAndFallingPlatformer.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
229
Extensions/PlatformBehavior/tests/PlatformerTestHelper.js
Normal file
229
Extensions/PlatformBehavior/tests/PlatformerTestHelper.js
Normal 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;
|
||||
};
|
740
Extensions/PlatformBehavior/tests/SlopePlatformer.spec.js
Normal file
740
Extensions/PlatformBehavior/tests/SlopePlatformer.spec.js
Normal 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());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ Copyright (c) 2008-2016 Florian Rival (Florian.Rival@gmail.com)
|
||||
This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#include "GDCore/Extensions/Metadata/MultipleInstructionMetadata.h"
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
#include "ShapePainterObject.h"
|
||||
@@ -40,10 +41,10 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
"res/actions/rectangle.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("Top left side: X position"))
|
||||
.AddParameter("expression", _("Top left side: Y position"))
|
||||
.AddParameter("expression", _("Bottom right side: X position"))
|
||||
.AddParameter("expression", _("Bottom right side: Y position"))
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.SetFunctionName("DrawRectangle")
|
||||
.SetIncludeFile("PrimitiveDrawing/ShapePainterObject.h");
|
||||
|
||||
@@ -131,10 +132,10 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
"res/actions/roundedRectangle.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("Top left side: X position"))
|
||||
.AddParameter("expression", _("Top left side: Y position"))
|
||||
.AddParameter("expression", _("Bottom right side: X position"))
|
||||
.AddParameter("expression", _("Bottom right side: Y position"))
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.AddParameter("expression", _("Radius (in pixels)"))
|
||||
.SetFunctionName("DrawRoundedRectangle")
|
||||
.SetIncludeFile("PrimitiveDrawing/ShapePainterObject.h");
|
||||
@@ -632,5 +633,162 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("AreCoordinatesRelative")
|
||||
.SetIncludeFile("PrimitiveDrawing/ShapePainterObject.h");
|
||||
|
||||
obj.AddAction("Scale",
|
||||
_("Scale"),
|
||||
_("Modify the scale of the specified object."),
|
||||
_("the scale"),
|
||||
_("Size"),
|
||||
"res/actions/scale24.png",
|
||||
"res/actions/scale.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddExpressionAndConditionAndAction("number",
|
||||
"ScaleX",
|
||||
_("Scale on X axis"),
|
||||
_("the width's scale of an object"),
|
||||
_("the width's scale"),
|
||||
_("Size"),
|
||||
"res/actions/scaleWidth24.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddExpressionAndConditionAndAction("number",
|
||||
"ScaleY",
|
||||
_("Scale on Y axis"),
|
||||
_("the height's scale of an object"),
|
||||
_("the height's scale"),
|
||||
_("Size"),
|
||||
"res/actions/scaleHeight24.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("FlipX",
|
||||
_("Flip the object horizontally"),
|
||||
_("Flip the object horizontally"),
|
||||
_("Flip horizontally _PARAM0_: _PARAM1_"),
|
||||
_("Effects"),
|
||||
"res/actions/flipX24.png",
|
||||
"res/actions/flipX.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("yesorno", _("Activate flipping"))
|
||||
.MarkAsSimple();
|
||||
|
||||
obj.AddAction("FlipY",
|
||||
_("Flip the object vertically"),
|
||||
_("Flip the object vertically"),
|
||||
_("Flip vertically _PARAM0_: _PARAM1_"),
|
||||
_("Effects"),
|
||||
"res/actions/flipY24.png",
|
||||
"res/actions/flipY.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("yesorno", _("Activate flipping"))
|
||||
.MarkAsSimple();
|
||||
|
||||
obj.AddCondition("FlippedX",
|
||||
_("Horizontally flipped"),
|
||||
_("Check if the object is horizontally flipped"),
|
||||
_("_PARAM0_ is horizontally flipped"),
|
||||
_("Effects"),
|
||||
"res/actions/flipX24.png",
|
||||
"res/actions/flipX.png")
|
||||
.AddParameter("object", _("Object"), "Drawer");
|
||||
|
||||
obj.AddCondition("FlippedY",
|
||||
_("Vertically flipped"),
|
||||
_("Check if the object is vertically flipped"),
|
||||
_("_PARAM0_ is vertically flipped"),
|
||||
_("Effects"),
|
||||
"res/actions/flipY24.png",
|
||||
"res/actions/flipY.png")
|
||||
.AddParameter("object", _("Object"), "Drawer");
|
||||
|
||||
obj.AddAction("Width",
|
||||
_("Width"),
|
||||
_("Change the width of an object."),
|
||||
_("the width"),
|
||||
_("Size"),
|
||||
"res/actions/scaleWidth24.png",
|
||||
"res/actions/scale.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("Height",
|
||||
_("Height"),
|
||||
_("Change the height of an object."),
|
||||
_("the height"),
|
||||
_("Size"),
|
||||
"res/actions/scaleHeight24.png",
|
||||
"res/actions/scale.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.UseStandardOperatorParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("SetRotationCenter",
|
||||
_("Center of rotation"),
|
||||
_("Change the center of rotation of an object relatively to the object origin."),
|
||||
_("Change the center of rotation of _PARAM0_: _PARAM1_; _PARAM2_"),
|
||||
_("Angle"),
|
||||
"res/actions/position24.png",
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X position"))
|
||||
.AddParameter("expression", _("Y position"))
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddAction("SetRectangularCollisionMask",
|
||||
_("Collision Mask"),
|
||||
_("Change the collision mask of an object to a rectangle relatively to the object origin."),
|
||||
_("Change the collision mask of _PARAM0_ to a rectangle from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_"),
|
||||
_("Position"),
|
||||
"res/actions/position24.png",
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.MarkAsAdvanced();
|
||||
|
||||
obj.AddExpression("ToDrawingX",
|
||||
_("X drawing coordinate of a point from the scene"),
|
||||
_("X drawing coordinate of a point from the scene"),
|
||||
_("Position"),
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X scene position"))
|
||||
.AddParameter("expression", _("Y scene position"));
|
||||
|
||||
obj.AddExpression("ToDrawingY",
|
||||
_("Y drawing coordinate of a point from the scene"),
|
||||
_("Y drawing coordinate of a point from the scene"),
|
||||
_("Position"),
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X scene position"))
|
||||
.AddParameter("expression", _("Y scene position"));
|
||||
|
||||
obj.AddExpression("ToSceneX",
|
||||
_("X scene coordinate of a point from the drawing"),
|
||||
_("X scene coordinate of a point from the drawing"),
|
||||
_("Position"),
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X drawing position"))
|
||||
.AddParameter("expression", _("Y drawing position"));
|
||||
|
||||
obj.AddExpression("ToSceneY",
|
||||
_("Y scene coordinate of a point from the drawing"),
|
||||
_("Y scene coordinate of a point from the drawing"),
|
||||
_("Position"),
|
||||
"res/actions/position.png")
|
||||
.AddParameter("object", _("Object"), "Drawer")
|
||||
.AddParameter("expression", _("X drawing position"))
|
||||
.AddParameter("expression", _("Y drawing position"));
|
||||
|
||||
#endif
|
||||
}
|
||||
|
@@ -170,6 +170,73 @@ class PrimitiveDrawingJsExtension : public gd::PlatformExtension {
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::AreCoordinatesRelative"]
|
||||
.SetFunctionName("areCoordinatesRelative");
|
||||
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Scale"]
|
||||
.SetFunctionName("setScale")
|
||||
.SetGetter("getScale");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Drawer::SetScaleX"]
|
||||
.SetFunctionName("setScaleX")
|
||||
.SetGetter("getScaleX");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Drawer::SetScaleY"]
|
||||
.SetFunctionName("setScaleY")
|
||||
.SetGetter("getScaleY");
|
||||
GetAllConditionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::ScaleX"]
|
||||
.SetFunctionName("getScaleX");
|
||||
GetAllConditionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::ScaleY"]
|
||||
.SetFunctionName("getScaleY");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::ScaleX"]
|
||||
.SetFunctionName("getScaleX");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::ScaleY"]
|
||||
.SetFunctionName("getScaleY");
|
||||
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::FlipX"]
|
||||
.SetFunctionName("flipX");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::FlipY"]
|
||||
.SetFunctionName("flipY");
|
||||
GetAllConditionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::FlippedX"]
|
||||
.SetFunctionName("isFlippedX");
|
||||
GetAllConditionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::FlippedY"]
|
||||
.SetFunctionName("isFlippedY");
|
||||
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Width"]
|
||||
.SetFunctionName("setWidth")
|
||||
.SetGetter("getWidth");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::Height"]
|
||||
.SetFunctionName("setHeight")
|
||||
.SetGetter("getHeight");
|
||||
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::SetRotationCenter"]
|
||||
.SetFunctionName("setRotationCenter");
|
||||
GetAllActionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["PrimitiveDrawing::SetRectangularCollisionMask"]
|
||||
.SetFunctionName("setRectangularCollisionMask");
|
||||
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["ToDrawingX"]
|
||||
.SetFunctionName("transformToDrawingX");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["ToDrawingY"]
|
||||
.SetFunctionName("transformToDrawingY");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["ToSceneX"]
|
||||
.SetFunctionName("transformToSceneX");
|
||||
GetAllExpressionsForObject(
|
||||
"PrimitiveDrawing::Drawer")["ToSceneY"]
|
||||
.SetFunctionName("transformToSceneX");
|
||||
|
||||
GD_COMPLETE_EXTENSION_COMPILATION_INFORMATION();
|
||||
};
|
||||
};
|
||||
|
@@ -4,6 +4,26 @@ namespace gdjs {
|
||||
class ShapePainterRuntimeObjectPixiRenderer {
|
||||
_object: gdjs.ShapePainterRuntimeObject;
|
||||
_graphics: PIXI.Graphics;
|
||||
/**
|
||||
* Graphics positions can need updates when shapes are added,
|
||||
* this avoids to do it each time.
|
||||
*/
|
||||
_positionXIsUpToDate = false;
|
||||
/**
|
||||
* Graphics positions can need updates when shapes are added,
|
||||
* this avoids to do it each time.
|
||||
*/
|
||||
_positionYIsUpToDate = false;
|
||||
/**
|
||||
* This allows to use the transformation of the renderer
|
||||
* and compute it only when necessary.
|
||||
*/
|
||||
_transformationIsUpToDate = false;
|
||||
|
||||
private static readonly _positionForTransformation: PIXI.IPointData = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
constructor(
|
||||
runtimeObject: gdjs.ShapePainterRuntimeObject,
|
||||
@@ -23,6 +43,7 @@ namespace gdjs {
|
||||
|
||||
clear() {
|
||||
this._graphics.clear();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawRectangle(x1: float, y1: float, x2: float, y2: float) {
|
||||
@@ -33,6 +54,7 @@ namespace gdjs {
|
||||
);
|
||||
this._graphics.drawRect(x1, y1, x2 - x1, y2 - y1);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawCircle(x: float, y: float, radius: float) {
|
||||
@@ -43,6 +65,7 @@ namespace gdjs {
|
||||
);
|
||||
this._graphics.drawCircle(x, y, radius);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawLine(x1: float, y1: float, x2: float, y2: float, thickness: float) {
|
||||
@@ -68,6 +91,7 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawLineV2(x1: float, y1: float, x2: float, y2: float, thickness: float) {
|
||||
@@ -79,6 +103,7 @@ namespace gdjs {
|
||||
this._graphics.moveTo(x1, y1);
|
||||
this._graphics.lineTo(x2, y2);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawEllipse(x1: float, y1: float, width: float, height: float) {
|
||||
@@ -89,6 +114,7 @@ namespace gdjs {
|
||||
);
|
||||
this._graphics.drawEllipse(x1, y1, width / 2, height / 2);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawRoundedRectangle(
|
||||
@@ -106,6 +132,7 @@ namespace gdjs {
|
||||
this._graphics.drawRoundedRect(x1, y1, x2 - x1, y2 - y1, radius);
|
||||
this._graphics.closePath();
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawStar(
|
||||
@@ -132,6 +159,7 @@ namespace gdjs {
|
||||
);
|
||||
this._graphics.closePath();
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawArc(
|
||||
@@ -164,6 +192,7 @@ namespace gdjs {
|
||||
this._graphics.closePath();
|
||||
}
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawBezierCurve(
|
||||
@@ -184,6 +213,7 @@ namespace gdjs {
|
||||
this._graphics.moveTo(x1, y1);
|
||||
this._graphics.bezierCurveTo(cpX, cpY, cpX2, cpY2, x2, y2);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawQuadraticCurve(
|
||||
@@ -202,6 +232,7 @@ namespace gdjs {
|
||||
this._graphics.moveTo(x1, y1);
|
||||
this._graphics.quadraticCurveTo(cpX, cpY, x2, y2);
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
beginFillPath() {
|
||||
@@ -213,6 +244,7 @@ namespace gdjs {
|
||||
|
||||
endFillPath() {
|
||||
this._graphics.endFill();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawPathMoveTo(x1: float, y1: float) {
|
||||
@@ -221,6 +253,7 @@ namespace gdjs {
|
||||
|
||||
drawPathLineTo(x1: float, y1: float) {
|
||||
this._graphics.lineTo(x1, y1);
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawPathBezierCurveTo(
|
||||
@@ -232,6 +265,7 @@ namespace gdjs {
|
||||
toY: float
|
||||
) {
|
||||
this._graphics.bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY);
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawPathArc(
|
||||
@@ -250,14 +284,17 @@ namespace gdjs {
|
||||
gdjs.toRad(endAngle),
|
||||
anticlockwise ? true : false
|
||||
);
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
drawPathQuadraticCurveTo(cpX: float, cpY: float, toX: float, toY: float) {
|
||||
this._graphics.quadraticCurveTo(cpX, cpY, toX, toY);
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
closePath() {
|
||||
this._graphics.closePath();
|
||||
this.invalidateBounds();
|
||||
}
|
||||
|
||||
updateOutline(): void {
|
||||
@@ -268,20 +305,185 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
updateXPosition(): void {
|
||||
if (!this._object._absoluteCoordinates) {
|
||||
this._graphics.position.x = this._object.x;
|
||||
} else {
|
||||
invalidateBounds() {
|
||||
this._object.invalidateBounds();
|
||||
this._positionXIsUpToDate = false;
|
||||
this._positionYIsUpToDate = false;
|
||||
}
|
||||
|
||||
updatePreRender(): void {
|
||||
this.updatePositionIfNeeded();
|
||||
}
|
||||
|
||||
updatePositionX(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.pivot.x = 0;
|
||||
this._graphics.position.x = 0;
|
||||
} else {
|
||||
// Make the drawing rotate around the rotation center.
|
||||
this._graphics.pivot.x = this._object.getRotationCenterX();
|
||||
// Multiply by the scale to have the scale anchor
|
||||
// at the object position instead of the center.
|
||||
this._graphics.position.x =
|
||||
this._object.x +
|
||||
this._graphics.pivot.x * Math.abs(this._graphics.scale.x);
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updatePositionY(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.pivot.y = 0;
|
||||
this._graphics.position.y = 0;
|
||||
} else {
|
||||
this._graphics.pivot.y = this._object.getRotationCenterY();
|
||||
this._graphics.position.y =
|
||||
this._object.y +
|
||||
this._graphics.pivot.y * Math.abs(this._graphics.scale.y);
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updatePositionIfNeeded() {
|
||||
if (!this._positionXIsUpToDate) {
|
||||
this.updatePositionX();
|
||||
this._positionXIsUpToDate = true;
|
||||
}
|
||||
if (!this._positionYIsUpToDate) {
|
||||
this.updatePositionY();
|
||||
this._positionYIsUpToDate = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateYPosition(): void {
|
||||
if (!this._object._absoluteCoordinates) {
|
||||
this._graphics.position.y = this._object.y;
|
||||
} else {
|
||||
this._graphics.position.y = 0;
|
||||
updateTransformationIfNeeded() {
|
||||
if (!this._transformationIsUpToDate) {
|
||||
this.updatePositionIfNeeded();
|
||||
this._graphics.updateTransform();
|
||||
}
|
||||
this._transformationIsUpToDate = true;
|
||||
}
|
||||
|
||||
updateRotationCenter(): void {
|
||||
// The pivot and position depends on the rotation center point.
|
||||
this._positionXIsUpToDate = false;
|
||||
this._positionYIsUpToDate = false;
|
||||
// The whole transformation changes based on the rotation center point.
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updateAngle(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.angle = 0;
|
||||
} else {
|
||||
this._graphics.angle = this._object.angle;
|
||||
}
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updateScaleX(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.scale.x = 1;
|
||||
} else {
|
||||
this._graphics.scale.x = this._object._scaleX;
|
||||
}
|
||||
// updatePositionX() uses scale.x
|
||||
this._positionXIsUpToDate = false;
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
updateScaleY(): void {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
this._graphics.scale.y = 1;
|
||||
} else {
|
||||
this._graphics.scale.y = this._object._scaleY;
|
||||
}
|
||||
// updatePositionY() uses scale.y
|
||||
this._positionYIsUpToDate = false;
|
||||
this._transformationIsUpToDate = false;
|
||||
}
|
||||
|
||||
getDrawableX(): float {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
return this._graphics.getLocalBounds().left;
|
||||
}
|
||||
let localBound = this._graphics.getLocalBounds().left;
|
||||
if (this._object._flippedX) {
|
||||
const rotationCenterX = this._object.getRotationCenterX();
|
||||
localBound = 2 * rotationCenterX - localBound;
|
||||
}
|
||||
// When new shape are drawn, the bounds of the object can extend.
|
||||
// The object position stays the same but (drawableX; drawableY) can change.
|
||||
return (
|
||||
this._object.getX() + localBound * Math.abs(this._graphics.scale.x)
|
||||
);
|
||||
}
|
||||
|
||||
getDrawableY(): float {
|
||||
if (this._object._useAbsoluteCoordinates) {
|
||||
return this._graphics.getLocalBounds().top;
|
||||
}
|
||||
let localBound = this._graphics.getLocalBounds().top;
|
||||
if (this._object._flippedY) {
|
||||
const rotationCenterY = this._object.getRotationCenterY();
|
||||
localBound = 2 * rotationCenterY - localBound;
|
||||
}
|
||||
return (
|
||||
this._object.getY() + localBound * Math.abs(this._graphics.scale.y)
|
||||
);
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this._graphics.width;
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return this._graphics.height;
|
||||
}
|
||||
|
||||
getUnscaledWidth(): float {
|
||||
return this._graphics.getLocalBounds().width;
|
||||
}
|
||||
|
||||
getUnscaledHeight(): float {
|
||||
return this._graphics.getLocalBounds().height;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The drawing origin relatively to the drawable top left corner.
|
||||
*/
|
||||
getFrameRelativeOriginX() {
|
||||
return -this._graphics.getLocalBounds().left;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The drawing origin relatively to the drawable top left corner.
|
||||
*/
|
||||
getFrameRelativeOriginY() {
|
||||
return -this._graphics.getLocalBounds().top;
|
||||
}
|
||||
|
||||
transformToDrawing(point: FloatPoint): FloatPoint {
|
||||
this.updateTransformationIfNeeded();
|
||||
const position =
|
||||
ShapePainterRuntimeObjectPixiRenderer._positionForTransformation;
|
||||
position.x = point[0];
|
||||
position.y = point[1];
|
||||
this._graphics.localTransform.applyInverse(position, position);
|
||||
point[0] = position.x;
|
||||
point[1] = position.y;
|
||||
return point;
|
||||
}
|
||||
|
||||
transformToScene(point: FloatPoint): FloatPoint {
|
||||
this.updateTransformationIfNeeded();
|
||||
const position =
|
||||
ShapePainterRuntimeObjectPixiRenderer._positionForTransformation;
|
||||
position.x = point[0];
|
||||
position.y = point[1];
|
||||
this._graphics.localTransform.apply(position, position);
|
||||
point[0] = position.x;
|
||||
point[1] = position.y;
|
||||
return point;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -37,15 +37,25 @@ namespace gdjs {
|
||||
* The ShapePainterRuntimeObject allows to draw graphics shapes on screen.
|
||||
*/
|
||||
export class ShapePainterRuntimeObject extends gdjs.RuntimeObject {
|
||||
_scaleX: number = 1;
|
||||
_scaleY: number = 1;
|
||||
_blendMode: number = 0;
|
||||
_flippedX: boolean = false;
|
||||
_flippedY: boolean = false;
|
||||
_customCenter: FloatPoint | null = null;
|
||||
_customCollisionMask: Polygon[] | null = null;
|
||||
|
||||
_fillColor: integer;
|
||||
_outlineColor: integer;
|
||||
_fillOpacity: float;
|
||||
_outlineOpacity: float;
|
||||
_outlineSize: float;
|
||||
_absoluteCoordinates: boolean;
|
||||
_useAbsoluteCoordinates: boolean;
|
||||
_clearBetweenFrames: boolean;
|
||||
_renderer: gdjs.ShapePainterRuntimeObjectRenderer;
|
||||
|
||||
private static readonly _pointForTransformation: FloatPoint = [0, 0];
|
||||
|
||||
/**
|
||||
* @param runtimeScene The scene the object belongs to.
|
||||
* @param shapePainterObjectData The initial properties of the object
|
||||
@@ -74,7 +84,7 @@ namespace gdjs {
|
||||
this._fillOpacity = shapePainterObjectData.fillOpacity;
|
||||
this._outlineOpacity = shapePainterObjectData.outlineOpacity;
|
||||
this._outlineSize = shapePainterObjectData.outlineSize;
|
||||
this._absoluteCoordinates = shapePainterObjectData.absoluteCoordinates;
|
||||
this._useAbsoluteCoordinates = shapePainterObjectData.absoluteCoordinates;
|
||||
this._clearBetweenFrames = shapePainterObjectData.clearBetweenFrames;
|
||||
this._renderer = new gdjs.ShapePainterRuntimeObjectRenderer(
|
||||
this,
|
||||
@@ -133,9 +143,12 @@ namespace gdjs {
|
||||
if (
|
||||
oldObjectData.absoluteCoordinates !== newObjectData.absoluteCoordinates
|
||||
) {
|
||||
this._absoluteCoordinates = newObjectData.absoluteCoordinates;
|
||||
this._renderer.updateXPosition();
|
||||
this._renderer.updateYPosition();
|
||||
this._useAbsoluteCoordinates = newObjectData.absoluteCoordinates;
|
||||
this._renderer.updatePositionX();
|
||||
this._renderer.updatePositionY();
|
||||
this._renderer.updateAngle();
|
||||
this._renderer.updateScaleX();
|
||||
this._renderer.updateScaleY();
|
||||
}
|
||||
if (
|
||||
oldObjectData.clearBetweenFrames !== newObjectData.clearBetweenFrames
|
||||
@@ -161,7 +174,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
getVisibilityAABB() {
|
||||
return this._absoluteCoordinates ? null : this.getAABB();
|
||||
return this._useAbsoluteCoordinates ? null : this.getAABB();
|
||||
}
|
||||
|
||||
drawRectangle(x1: float, y1: float, x2: float, y2: float) {
|
||||
@@ -325,11 +338,11 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
setCoordinatesRelative(value: boolean): void {
|
||||
this._absoluteCoordinates = !value;
|
||||
this._useAbsoluteCoordinates = !value;
|
||||
}
|
||||
|
||||
areCoordinatesRelative(): boolean {
|
||||
return !this._absoluteCoordinates;
|
||||
return !this._useAbsoluteCoordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -438,7 +451,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
super.setX(x);
|
||||
this._renderer.updateXPosition();
|
||||
this._renderer.updatePositionX();
|
||||
}
|
||||
|
||||
setY(y: float): void {
|
||||
@@ -446,15 +459,337 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
super.setY(y);
|
||||
this._renderer.updateYPosition();
|
||||
this._renderer.updatePositionY();
|
||||
}
|
||||
|
||||
setAngle(angle: float): void {
|
||||
if (angle === this.angle) {
|
||||
return;
|
||||
}
|
||||
super.setAngle(angle);
|
||||
this._renderer.updateAngle();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The center of rotation is defined relatively
|
||||
* to the drawing origin (the object position).
|
||||
* This avoid the center to move on the drawing
|
||||
* when new shapes push the bounds.
|
||||
*
|
||||
* When no custom center is defined, it will move
|
||||
* to stay at the center of the drawable bounds.
|
||||
*
|
||||
* @param x coordinate of the custom center
|
||||
* @param y coordinate of the custom center
|
||||
*/
|
||||
setRotationCenter(x: float, y: float): void {
|
||||
if (!this._customCenter) {
|
||||
this._customCenter = [0, 0];
|
||||
}
|
||||
this._customCenter[0] = x;
|
||||
this._customCenter[1] = y;
|
||||
this._renderer.updateRotationCenter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The center X relatively to the drawing origin
|
||||
* (whereas `getCenterX()` is relative to the top left drawable bound and scaled).
|
||||
*/
|
||||
getRotationCenterX(): float {
|
||||
return this._customCenter
|
||||
? this._customCenter[0]
|
||||
: this._renderer.getUnscaledWidth() / 2 -
|
||||
this._renderer.getFrameRelativeOriginX();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The center Y relatively to the drawing origin
|
||||
* (whereas `getCenterY()` is relative to the top left drawable bound and scaled).
|
||||
*/
|
||||
getRotationCenterY(): float {
|
||||
return this._customCenter
|
||||
? this._customCenter[1]
|
||||
: this._renderer.getUnscaledHeight() / 2 -
|
||||
this._renderer.getFrameRelativeOriginY();
|
||||
}
|
||||
|
||||
getCenterX(): float {
|
||||
if (!this._customCenter) {
|
||||
return super.getCenterX();
|
||||
}
|
||||
return (
|
||||
this._customCenter[0] * Math.abs(this._scaleX) +
|
||||
this.getX() -
|
||||
this.getDrawableX()
|
||||
);
|
||||
}
|
||||
|
||||
getCenterY(): float {
|
||||
if (!this._customCenter) {
|
||||
return super.getCenterY();
|
||||
}
|
||||
return (
|
||||
this._customCenter[1] * Math.abs(this._scaleY) +
|
||||
this.getY() -
|
||||
this.getDrawableY()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the width of the object. This changes the scale on X axis of the object.
|
||||
*
|
||||
* @param newWidth The new width of the object, in pixels.
|
||||
*/
|
||||
setWidth(newWidth: float): void {
|
||||
const unscaledWidth = this._renderer.getUnscaledWidth();
|
||||
if (unscaledWidth !== 0) {
|
||||
this.setScaleX(newWidth / unscaledWidth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the height of the object. This changes the scale on Y axis of the object.
|
||||
*
|
||||
* @param newHeight The new height of the object, in pixels.
|
||||
*/
|
||||
setHeight(newHeight: float): void {
|
||||
const unscaledHeight = this._renderer.getUnscaledHeight();
|
||||
if (unscaledHeight !== 0) {
|
||||
this.setScaleY(newHeight / unscaledHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X and Y axis of the object.
|
||||
*
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScale(newScale: float): void {
|
||||
this.setScaleX(newScale);
|
||||
this.setScaleY(newScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X axis of the object (changing its width).
|
||||
*
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleX(newScale: float): void {
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
if (newScale === Math.abs(this._scaleX)) {
|
||||
return;
|
||||
}
|
||||
this._scaleX = newScale * (this._flippedX ? -1 : 1);
|
||||
this._renderer.updateScaleX();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on Y axis of the object (changing its width).
|
||||
*
|
||||
* @param newScale The new scale (must be greater than 0).
|
||||
*/
|
||||
setScaleY(newScale: float): void {
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
if (newScale === Math.abs(this._scaleY)) {
|
||||
return;
|
||||
}
|
||||
this._scaleY = newScale * (this._flippedY ? -1 : 1);
|
||||
this._renderer.updateScaleY();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
flipX(enable: boolean): void {
|
||||
if (enable !== this._flippedX) {
|
||||
this._scaleX *= -1;
|
||||
this._flippedX = enable;
|
||||
this._renderer.updateScaleX();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
flipY(enable: boolean): void {
|
||||
if (enable !== this._flippedY) {
|
||||
this._scaleY *= -1;
|
||||
this._flippedY = enable;
|
||||
this._renderer.updateScaleY();
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
isFlippedX(): boolean {
|
||||
return this._flippedX;
|
||||
}
|
||||
|
||||
isFlippedY(): boolean {
|
||||
return this._flippedY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*
|
||||
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*/
|
||||
getScale(): number {
|
||||
const scaleX = Math.abs(this._scaleX);
|
||||
const scaleY = Math.abs(this._scaleY);
|
||||
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object on Y axis.
|
||||
*
|
||||
* @return the scale of the object on Y axis
|
||||
*/
|
||||
getScaleY(): float {
|
||||
return Math.abs(this._scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object on X axis.
|
||||
*
|
||||
* @return the scale of the object on X axis
|
||||
*/
|
||||
getScaleX(): float {
|
||||
return Math.abs(this._scaleX);
|
||||
}
|
||||
|
||||
invalidateBounds() {
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
getDrawableX(): float {
|
||||
return this._renderer.getDrawableX();
|
||||
}
|
||||
|
||||
getDrawableY(): float {
|
||||
return this._renderer.getDrawableY();
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return 32;
|
||||
return this._renderer.getWidth();
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return 32;
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
updatePreRender(runtimeScene: gdjs.RuntimeScene): void {
|
||||
this._renderer.updatePreRender();
|
||||
}
|
||||
|
||||
transformToDrawing(x: float, y: float) {
|
||||
const point = ShapePainterRuntimeObject._pointForTransformation;
|
||||
point[0] = x;
|
||||
point[1] = y;
|
||||
return this._renderer.transformToDrawing(point);
|
||||
}
|
||||
|
||||
transformToScene(x: float, y: float) {
|
||||
const point = ShapePainterRuntimeObject._pointForTransformation;
|
||||
point[0] = x;
|
||||
point[1] = y;
|
||||
return this._renderer.transformToScene(point);
|
||||
}
|
||||
|
||||
transformToDrawingX(x: float, y: float) {
|
||||
return this.transformToDrawing(x, y)[0];
|
||||
}
|
||||
|
||||
transformToDrawingY(x: float, y: float) {
|
||||
return this.transformToDrawing(x, y)[1];
|
||||
}
|
||||
|
||||
transformToSceneX(x: float, y: float) {
|
||||
return this.transformToScene(x, y)[0];
|
||||
}
|
||||
|
||||
transformToSceneY(x: float, y: float) {
|
||||
return this.transformToScene(x, y)[1];
|
||||
}
|
||||
|
||||
setRectangularCollisionMask(
|
||||
left: float,
|
||||
top: float,
|
||||
right: float,
|
||||
bottom: float
|
||||
) {
|
||||
if (!this._customCollisionMask) {
|
||||
const rectangle = new gdjs.Polygon();
|
||||
rectangle.vertices.push([0, 0]);
|
||||
rectangle.vertices.push([0, 0]);
|
||||
rectangle.vertices.push([0, 0]);
|
||||
rectangle.vertices.push([0, 0]);
|
||||
this._customCollisionMask = [rectangle];
|
||||
}
|
||||
const rectangle = this._customCollisionMask[0].vertices;
|
||||
|
||||
rectangle[0][0] = left;
|
||||
rectangle[0][1] = top;
|
||||
|
||||
rectangle[1][0] = right;
|
||||
rectangle[1][1] = top;
|
||||
|
||||
rectangle[2][0] = right;
|
||||
rectangle[2][1] = bottom;
|
||||
|
||||
rectangle[3][0] = left;
|
||||
rectangle[3][1] = bottom;
|
||||
|
||||
this.hitBoxesDirty = true;
|
||||
}
|
||||
|
||||
updateHitBoxes(): void {
|
||||
this.hitBoxes = this._defaultHitBoxes;
|
||||
const width = this.getWidth();
|
||||
const height = this.getHeight();
|
||||
const centerX = this.getCenterX();
|
||||
const centerY = this.getCenterY();
|
||||
const vertices = this.hitBoxes[0].vertices;
|
||||
if (this._customCollisionMask) {
|
||||
const customCollisionMaskVertices = this._customCollisionMask[0]
|
||||
.vertices;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const point = this.transformToScene(
|
||||
customCollisionMaskVertices[i][0],
|
||||
customCollisionMaskVertices[i][1]
|
||||
);
|
||||
vertices[i][0] = point[0];
|
||||
vertices[i][1] = point[1];
|
||||
}
|
||||
} else {
|
||||
if (centerX === width / 2 && centerY === height / 2) {
|
||||
vertices[0][0] = -centerX;
|
||||
vertices[0][1] = -centerY;
|
||||
vertices[1][0] = +centerX;
|
||||
vertices[1][1] = -centerY;
|
||||
vertices[2][0] = +centerX;
|
||||
vertices[2][1] = +centerY;
|
||||
vertices[3][0] = -centerX;
|
||||
vertices[3][1] = +centerY;
|
||||
} else {
|
||||
vertices[0][0] = 0 - centerX;
|
||||
vertices[0][1] = 0 - centerY;
|
||||
vertices[1][0] = width - centerX;
|
||||
vertices[1][1] = 0 - centerY;
|
||||
vertices[2][0] = width - centerX;
|
||||
vertices[2][1] = height - centerY;
|
||||
vertices[3][0] = 0 - centerX;
|
||||
vertices[3][1] = height - centerY;
|
||||
}
|
||||
if (!this._useAbsoluteCoordinates) {
|
||||
this.hitBoxes[0].rotate(gdjs.toRad(this.getAngle()));
|
||||
}
|
||||
this.hitBoxes[0].move(
|
||||
this.getDrawableX() + centerX,
|
||||
this.getDrawableY() + centerY
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
gdjs.registerObject(
|
||||
|
@@ -0,0 +1,205 @@
|
||||
// @ts-check
|
||||
|
||||
describe('gdjs.ShapePainterRuntimeObject (using a PIXI RuntimeGame with assets)', function () {
|
||||
/**
|
||||
* @param {gdjs.RuntimeScene} runtimeScene
|
||||
*/
|
||||
const makeSpriteRuntimeObjectWithCustomHitBox = (runtimeScene) =>
|
||||
new gdjs.ShapePainterRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: 'PrimitiveDrawing::Drawer',
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
fillColor: { r: 0, g: 0, b: 0 },
|
||||
outlineColor: { r: 0, g: 0, b: 0 },
|
||||
fillOpacity: 255,
|
||||
outlineOpacity: 255,
|
||||
outlineSize: 1,
|
||||
absoluteCoordinates: false,
|
||||
clearBetweenFrames: false,
|
||||
});
|
||||
|
||||
/** @param {gdjs.RuntimeScene} runtimeScene */
|
||||
const loadScene = (runtimeScene) => {
|
||||
runtimeScene.loadFromScene({
|
||||
layers: [
|
||||
{
|
||||
name: '',
|
||||
visibility: true,
|
||||
effects: [],
|
||||
cameras: [],
|
||||
ambientLightColorR: 0,
|
||||
ambientLightColorG: 0,
|
||||
ambientLightColorB: 0,
|
||||
isLightingLayer: false,
|
||||
followBaseLayerCamera: true,
|
||||
},
|
||||
],
|
||||
r: 0,
|
||||
v: 0,
|
||||
b: 0,
|
||||
mangledName: 'Scene1',
|
||||
name: 'Scene1',
|
||||
stopSoundsOnStartup: false,
|
||||
title: '',
|
||||
behaviorsSharedData: [],
|
||||
objects: [],
|
||||
instances: [],
|
||||
variables: [],
|
||||
});
|
||||
};
|
||||
|
||||
it('properly computes bounds of the object (basics)', async () => {
|
||||
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
loadScene(runtimeScene);
|
||||
|
||||
const object = makeSpriteRuntimeObjectWithCustomHitBox(runtimeScene);
|
||||
|
||||
expect(object.getWidth()).to.be(0);
|
||||
expect(object.getHeight()).to.be(0);
|
||||
|
||||
object.drawLineV2(10, 10, 20, 30, 3);
|
||||
|
||||
// Check the automatically computed bounds:
|
||||
expect(object.getDrawableX()).to.be(8.5);
|
||||
expect(object.getDrawableY()).to.be(8.5);
|
||||
expect(object.getWidth()).to.be(13);
|
||||
expect(object.getHeight()).to.be(23);
|
||||
|
||||
// Check the automatic center positioning:
|
||||
expect(object.getCenterXInScene()).to.be(15);
|
||||
expect(object.getCenterYInScene()).to.be(20);
|
||||
expect(object.getCenterX()).to.be(15 - 8.5);
|
||||
expect(object.getCenterY()).to.be(20 - 8.5);
|
||||
|
||||
// Check hit boxes:
|
||||
expect(object.getAABB()).to.eql({
|
||||
min: [8.5, 8.5],
|
||||
max: [8.5 + 13, 8.5 + 23],
|
||||
});
|
||||
|
||||
// Check after scaling (scaling is done from the origin):
|
||||
object.setScale(2);
|
||||
expect(object.getDrawableX()).to.be(17);
|
||||
expect(object.getDrawableY()).to.be(17);
|
||||
expect(object.getWidth()).to.be(13 * 2);
|
||||
expect(object.getHeight()).to.be(23 * 2);
|
||||
expect(object.getAABB()).to.eql({ min: [17, 17], max: [43, 63] });
|
||||
|
||||
// Check after rotating (rotating is done from the center):
|
||||
object.setAngle(45);
|
||||
expect(object.getDrawableX()).to.be(17); // Drawable X/Y is not impacted...
|
||||
expect(object.getDrawableY()).to.be(17);
|
||||
expect(object.getWidth()).to.be(13 * 2); // ...Neither is the size
|
||||
expect(object.getHeight()).to.be(23 * 2);
|
||||
expect(object.getAABB()).to.eql({
|
||||
// The hit boxes/AABB are rotated:
|
||||
min: [4.54415587728429, 14.54415587728429],
|
||||
max: [55.45584412271571, 65.45584412271572],
|
||||
});
|
||||
});
|
||||
|
||||
it('properly computes bounds of the object (custom center)', async () => {
|
||||
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
loadScene(runtimeScene);
|
||||
|
||||
const object = makeSpriteRuntimeObjectWithCustomHitBox(runtimeScene);
|
||||
|
||||
expect(object.getWidth()).to.be(0);
|
||||
expect(object.getHeight()).to.be(0);
|
||||
|
||||
object.drawLineV2(10, 10, 20, 30, 3);
|
||||
object.setRotationCenter(10, 9);
|
||||
|
||||
// Check the automatically computed bounds (not impacted by the center):
|
||||
expect(object.getDrawableX()).to.be(8.5);
|
||||
expect(object.getDrawableY()).to.be(8.5);
|
||||
expect(object.getWidth()).to.be(13);
|
||||
expect(object.getHeight()).to.be(23);
|
||||
|
||||
// Check the center positioning:
|
||||
expect(object.getCenterXInScene()).to.be(10);
|
||||
expect(object.getCenterYInScene()).to.be(9);
|
||||
expect(object.getCenterX()).to.be(10 - 8.5);
|
||||
expect(object.getCenterY()).to.be(9 - 8.5);
|
||||
|
||||
// Check hit boxes (not impacted by the center, as no rotation is made):
|
||||
expect(object.getAABB()).to.eql({
|
||||
min: [8.5, 8.5],
|
||||
max: [8.5 + 13, 8.5 + 23],
|
||||
});
|
||||
|
||||
// Check after scaling (scaling is done from the origin):
|
||||
object.setScale(2);
|
||||
expect(object.getDrawableX()).to.be(17);
|
||||
expect(object.getDrawableY()).to.be(17);
|
||||
expect(object.getWidth()).to.be(13 * 2);
|
||||
expect(object.getHeight()).to.be(23 * 2);
|
||||
expect(object.getAABB()).to.eql({ min: [17, 17], max: [43, 63] });
|
||||
|
||||
// Check after rotating (rotating is done from the center):
|
||||
object.setAngle(45);
|
||||
expect(object.getAABB()).to.eql({
|
||||
// The hit boxes/AABB are rotated:
|
||||
min: [-13.941125496954278, 15.17157287525381],
|
||||
max: [36.970562748477136, 66.08326112068524],
|
||||
});
|
||||
|
||||
// Draw outside of the current bounds.
|
||||
const oldMinX = object.getAABB().min[0];
|
||||
const oldMinY = object.getAABB().min[1];
|
||||
const oldMaxX = object.getAABB().max[0];
|
||||
const oldMaxY = object.getAABB().max[1];
|
||||
const oldCenterX = object.getCenterXInScene();
|
||||
const oldCenterY = object.getCenterYInScene();
|
||||
object.drawLineV2(-10, -10, 21, 31, 3);
|
||||
|
||||
// Check that the center stays the same.
|
||||
expect(object.getCenterXInScene()).to.be(oldCenterX);
|
||||
expect(object.getCenterYInScene()).to.be(oldCenterY);
|
||||
|
||||
// Check that the AABB expands.
|
||||
const newAABB = object.getAABB();
|
||||
expect(newAABB.min[0]).below(oldMinX);
|
||||
expect(newAABB.min[1]).below(oldMinY);
|
||||
expect(newAABB.max[0]).above(oldMaxX);
|
||||
expect(newAABB.max[1]).above(oldMaxY);
|
||||
});
|
||||
|
||||
it('can transform points', async () => {
|
||||
const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
loadScene(runtimeScene);
|
||||
|
||||
const object = makeSpriteRuntimeObjectWithCustomHitBox(runtimeScene);
|
||||
|
||||
object.drawLineV2(0, 0, 10, 10, 2);
|
||||
expect(object.getWidth()).to.be(12);
|
||||
expect(object.getHeight()).to.be(12);
|
||||
|
||||
// Check changes in position/scale are taken into account:
|
||||
object.setPosition(50, 100);
|
||||
expect(object.transformToScene(10, 20)).to.eql([60, 120]);
|
||||
|
||||
object.setScale(2);
|
||||
expect(object.transformToScene(10, 20)).to.eql([70, 140]);
|
||||
|
||||
// Check rotation with the default center:
|
||||
expect(object.getCenterXInScene()).to.be(60);
|
||||
expect(object.getCenterYInScene()).to.be(110);
|
||||
expect(object.transformToScene(5, 5)).to.eql([60, 110]);
|
||||
expect(object.transformToScene(10, 20)).to.eql([70, 140]);
|
||||
|
||||
object.setAngle(90);
|
||||
expect(object.transformToScene(5, 5)).to.eql([60, 110]);
|
||||
expect(object.transformToScene(10, 20)).to.eql([30, 120]);
|
||||
|
||||
// Check rotation with a custom center:
|
||||
object.setRotationCenter(20, 9);
|
||||
expect(object.transformToScene(10, 20)).to.eql([68, 98]);
|
||||
expect(object.transformToDrawing(68, 98)).to.eql([10, 20]);
|
||||
});
|
||||
});
|
@@ -1,17 +1,21 @@
|
||||
namespace gdjs {
|
||||
export namespace evtTools {
|
||||
export namespace spatialSound {
|
||||
const logger = new gdjs.Logger('Spatial Sound');
|
||||
export const setSoundPosition = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
x: float,
|
||||
y: float,
|
||||
z: float
|
||||
) =>
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.getSoundOnChannel(channel)
|
||||
.setSpatialPosition(x, y, z);
|
||||
) => {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
if (sound) sound.setSpatialPosition(x, y, z);
|
||||
else
|
||||
logger.error(
|
||||
`Cannot set the spatial position of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -45,23 +45,24 @@ TopDownMovementBehavior::GetProperties(
|
||||
const gd::SerializerElement& behaviorContent) const {
|
||||
std::map<gd::String, gd::PropertyDescriptor> properties;
|
||||
|
||||
properties[_("Allows diagonals")]
|
||||
properties[_("Allows diagonals")].SetGroup(_("Movement"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("allowDiagonals") ? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Acceleration")].SetValue(
|
||||
properties[_("Acceleration")].SetGroup(_("Movement")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("acceleration")));
|
||||
properties[_("Deceleration")].SetValue(
|
||||
properties[_("Deceleration")].SetGroup(_("Movement")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("deceleration")));
|
||||
properties[_("Max. speed")].SetValue(
|
||||
properties[_("Max. speed")].SetGroup(_("Movement")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxSpeed")));
|
||||
properties[_("Rotate speed")].SetValue(
|
||||
properties[_("Rotate speed")].SetGroup(_("Rotation")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angularMaxSpeed")));
|
||||
properties[_("Rotate object")]
|
||||
.SetGroup(_("Rotation"))
|
||||
.SetValue(behaviorContent.GetBoolAttribute("rotateObject") ? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties[_("Angle offset")].SetValue(
|
||||
properties[_("Angle offset")].SetGroup(_("Rotation")).SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angleOffset")));
|
||||
properties[_("Default controls")]
|
||||
.SetValue(behaviorContent.GetBoolAttribute("ignoreDefaultControls")
|
||||
@@ -80,6 +81,7 @@ TopDownMovementBehavior::GetProperties(
|
||||
else if (viewpoint == "CustomIsometry")
|
||||
viewpointStr = _("Custom Isometry");
|
||||
properties[_("Viewpoint")]
|
||||
.SetGroup(_("Viewpoint"))
|
||||
.SetValue(viewpointStr)
|
||||
.SetType("Choice")
|
||||
.AddExtraInfo(_("Top-Down"))
|
||||
@@ -87,11 +89,13 @@ TopDownMovementBehavior::GetProperties(
|
||||
.AddExtraInfo(_("True Isometry (30°)"))
|
||||
.AddExtraInfo(_("Custom Isometry"));
|
||||
properties[_("Custom isometry angle")]
|
||||
.SetGroup(_("Viewpoint"))
|
||||
.SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("customIsometryAngle")))
|
||||
.SetDescription(_("If you choose \"Custom Isometry\", this allows to "
|
||||
"specify the angle of your isometry projection."));
|
||||
properties[_("Movement angle offset")]
|
||||
.SetGroup(_("Viewpoint"))
|
||||
.SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("movementAngleOffset")))
|
||||
.SetDescription(_(
|
||||
|
@@ -1,8 +1,9 @@
|
||||
// @ts-check
|
||||
describe('gdjs.TopDownMovementRuntimeBehavior', function () {
|
||||
const epsilon = 1 / (2 << 8);
|
||||
const topDownName = 'auto1';
|
||||
|
||||
const createScene = () => {
|
||||
const createScene = (timeDelta = 1000 / 60) => {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
// @ts-ignore - missing properties.
|
||||
@@ -38,7 +39,7 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
|
||||
instances: [],
|
||||
});
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return (1 / 60) * 1000;
|
||||
return timeDelta;
|
||||
};
|
||||
return runtimeScene;
|
||||
};
|
||||
@@ -427,4 +428,28 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
[20, 30, 60, 120].forEach((framesPerSecond) => {
|
||||
describe(`(frames per second: ${framesPerSecond})`, function () {
|
||||
let runtimeScene;
|
||||
let player;
|
||||
beforeEach(function () {
|
||||
runtimeScene = createScene(1000 / framesPerSecond);
|
||||
player = addPlayer(runtimeScene, true);
|
||||
});
|
||||
|
||||
it('moves the same distance', function () {
|
||||
player.setPosition(200, 300);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
// It takes 0,5 second to reach the maximum speed because
|
||||
// the acceleration is 400 and maxSpeed is 200.
|
||||
for (let i = 0; i < framesPerSecond / 2; i++) {
|
||||
player.getBehavior(topDownName).simulateRightKey();
|
||||
runtimeScene.renderAndStep(1000 / framesPerSecond);
|
||||
}
|
||||
expect(player.getX()).to.be.within(250 - epsilon, 250 + epsilon);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -324,7 +324,10 @@ namespace gdjs {
|
||||
const timeDelta = this.owner.getElapsedTime(runtimeScene) / 1000;
|
||||
let directionInRad = 0;
|
||||
let directionInDeg = 0;
|
||||
//Update the speed of the object
|
||||
const previousVelocityX = this._xVelocity;
|
||||
const previousVelocityY = this._yVelocity;
|
||||
|
||||
// Update the speed of the object:
|
||||
if (direction !== -1) {
|
||||
directionInRad =
|
||||
((direction + this._movementAngleOffset / 45) * Math.PI) / 4.0;
|
||||
@@ -344,52 +347,57 @@ namespace gdjs {
|
||||
this._yVelocity += norm * Math.sin(directionInRad);
|
||||
|
||||
this._stickForce = 0;
|
||||
} else {
|
||||
} else if (this._yVelocity !== 0 || this._xVelocity !== 0) {
|
||||
directionInRad = Math.atan2(this._yVelocity, this._xVelocity);
|
||||
directionInDeg =
|
||||
(Math.atan2(this._yVelocity, this._xVelocity) * 180.0) / Math.PI;
|
||||
directionInDeg = (directionInRad * 180.0) / Math.PI;
|
||||
const xVelocityWasPositive = this._xVelocity >= 0;
|
||||
const yVelocityWasPositive = this._yVelocity >= 0;
|
||||
this._xVelocity -=
|
||||
this._deceleration * timeDelta * Math.cos(directionInRad);
|
||||
this._yVelocity -=
|
||||
this._deceleration * timeDelta * Math.sin(directionInRad);
|
||||
// @ts-ignore
|
||||
if ((this._xVelocity > 0) ^ xVelocityWasPositive) {
|
||||
if (this._xVelocity > 0 !== xVelocityWasPositive) {
|
||||
this._xVelocity = 0;
|
||||
}
|
||||
// @ts-ignore
|
||||
if ((this._yVelocity > 0) ^ yVelocityWasPositive) {
|
||||
if (this._yVelocity > 0 !== yVelocityWasPositive) {
|
||||
this._yVelocity = 0;
|
||||
}
|
||||
}
|
||||
const speed = Math.sqrt(
|
||||
this._xVelocity * this._xVelocity + this._yVelocity * this._yVelocity
|
||||
);
|
||||
if (speed > this._maxSpeed) {
|
||||
const squaredSpeed =
|
||||
this._xVelocity * this._xVelocity + this._yVelocity * this._yVelocity;
|
||||
if (squaredSpeed > this._maxSpeed * this._maxSpeed) {
|
||||
this._xVelocity = this._maxSpeed * Math.cos(directionInRad);
|
||||
this._yVelocity = this._maxSpeed * Math.sin(directionInRad);
|
||||
}
|
||||
|
||||
// No acceleration for angular speed for now.
|
||||
this._angularSpeed = this._angularMaxSpeed;
|
||||
|
||||
//No acceleration for angular speed for now
|
||||
|
||||
//Position object
|
||||
// Position object.
|
||||
// This is a Verlet integration considering the acceleration as constant.
|
||||
// If you expand deltaX or deltaY, it gives, thanks to the usage of both
|
||||
// the old and the new velocity:
|
||||
// "velocity * timeDelta + acceleration * timeDelta^2 / 2".
|
||||
//
|
||||
// The acceleration is not actually always constant, particularly with a gamepad,
|
||||
// but the error is multiplied by timDelta^3. So, it shouldn't matter much.
|
||||
const deltaX = ((previousVelocityX + this._xVelocity) / 2) * timeDelta;
|
||||
const deltaY = ((previousVelocityY + this._yVelocity) / 2) * timeDelta;
|
||||
if (this._basisTransformation === null) {
|
||||
// Top-down viewpoint
|
||||
object.setX(object.getX() + this._xVelocity * timeDelta);
|
||||
object.setY(object.getY() + this._yVelocity * timeDelta);
|
||||
object.setX(object.getX() + deltaX);
|
||||
object.setY(object.getY() + deltaY);
|
||||
} else {
|
||||
// Isometry viewpoint
|
||||
const point = this._temporaryPointForTransformations;
|
||||
point[0] = this._xVelocity * timeDelta;
|
||||
point[1] = this._yVelocity * timeDelta;
|
||||
point[0] = deltaX;
|
||||
point[1] = deltaY;
|
||||
this._basisTransformation.toScreen(point, point);
|
||||
object.setX(object.getX() + point[0]);
|
||||
object.setY(object.getY() + point[1]);
|
||||
}
|
||||
|
||||
//Also update angle if needed
|
||||
// Also update angle if needed.
|
||||
if (this._xVelocity !== 0 || this._yVelocity !== 0) {
|
||||
this._angle = directionInDeg;
|
||||
if (this._rotateObject) {
|
||||
|
@@ -107,6 +107,19 @@ namespace gdjs {
|
||||
return true;
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
const shiftyJsScene = this._runtimeScene.shiftyJsScene;
|
||||
if (!shiftyJsScene) return;
|
||||
|
||||
// Stop and delete all tweens of the behavior - otherwise they could:
|
||||
// - continue to point to the behavior, and so to the object (memory leak),
|
||||
// - affect the object in case it's recycled (wrong/hard to debug behavior).
|
||||
for (const identifier in this._tweens) {
|
||||
this._tweens[identifier].instance.stop();
|
||||
shiftyJsScene.remove(this._tweens[identifier].instance);
|
||||
}
|
||||
}
|
||||
|
||||
private _addTween(
|
||||
identifier: string,
|
||||
easingValue: string,
|
||||
|
@@ -278,7 +278,7 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
|
||||
outputCode +=
|
||||
codeGenerator.GenerateBooleanFullName(
|
||||
"condition" + gd::String::From(i) + "IsTrue", context) +
|
||||
".val = true;\n";
|
||||
".val = false;\n";
|
||||
}
|
||||
|
||||
for (unsigned int cId = 0; cId < conditions.size(); ++cId) {
|
||||
|
@@ -68,18 +68,18 @@ namespace gdjs {
|
||||
RAlt: 2018,
|
||||
LSystem: 1091,
|
||||
RSystem: 2091,
|
||||
/*"Menu": sf::Keyboard::Menu ,
|
||||
"LBracket": sf::Keyboard::LBracket ,
|
||||
"RBracket": sf::Keyboard::RBracket ,
|
||||
"SemiColon": sf::Keyboard::SemiColon ,
|
||||
"Comma": sf::Keyboard::Comma ,
|
||||
"Period": sf::Keyboard::Period ,
|
||||
"Quote": sf::Keyboard::Quote ,
|
||||
"Slash": sf::Keyboard::Slash ,
|
||||
"BackSlash": sf::Keyboard::BackSlash ,
|
||||
"Tilde": sf::Keyboard::Tilde ,
|
||||
"Equal": sf::Keyboard::Equal ,
|
||||
"Dash": sf::Keyboard::Dash,*/
|
||||
SemiColon: 186,
|
||||
Comma: 188,
|
||||
Period: 190,
|
||||
Quote: 222,
|
||||
Slash: 191,
|
||||
BackSlash: 220,
|
||||
Equal: 187,
|
||||
Dash: 189,
|
||||
Menu: 93,
|
||||
LBracket: 219,
|
||||
RBracket: 221,
|
||||
Tilde: 192,
|
||||
Space: 32,
|
||||
Back: 8,
|
||||
Tab: 9,
|
||||
|
@@ -6,20 +6,24 @@
|
||||
namespace gdjs {
|
||||
export namespace evtTools {
|
||||
export namespace sound {
|
||||
const logger = new gdjs.Logger('Audio events');
|
||||
|
||||
export const getGlobalVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
): float {
|
||||
return runtimeScene.getSoundManager().getGlobalVolume();
|
||||
};
|
||||
|
||||
export const setGlobalVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
globalVolume: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene.getSoundManager().setGlobalVolume(globalVolume);
|
||||
};
|
||||
|
||||
export const unloadAllAudio = function (runtimeScene: gdjs.RuntimeScene) {
|
||||
export const unloadAllAudio = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
): void {
|
||||
runtimeScene.getSoundManager().unloadAll();
|
||||
};
|
||||
|
||||
@@ -30,7 +34,7 @@ namespace gdjs {
|
||||
loop: boolean,
|
||||
volume: float,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.playSound(soundFile, loop, volume, pitch);
|
||||
@@ -43,7 +47,7 @@ namespace gdjs {
|
||||
loop: boolean,
|
||||
volume: float,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.playSoundOnChannel(soundFile, channel, loop, volume, pitch);
|
||||
@@ -52,33 +56,45 @@ namespace gdjs {
|
||||
export const stopSoundOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.stop();
|
||||
if (sound) sound.stop();
|
||||
else {
|
||||
logger.error(`Cannot stop non-existing sound on channel ${channel}.`);
|
||||
}
|
||||
};
|
||||
|
||||
export const pauseSoundOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.pause();
|
||||
if (sound) sound.pause();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot pause non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const continueSoundOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
if (sound && !sound.playing()) {
|
||||
sound.play();
|
||||
if (sound) {
|
||||
if (!sound.playing()) sound.play();
|
||||
} else {
|
||||
logger.error(
|
||||
`Cannot continue playing non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const isSoundOnChannelPlaying = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.playing() : false;
|
||||
};
|
||||
@@ -86,68 +102,113 @@ namespace gdjs {
|
||||
export const isSoundOnChannelPaused = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.paused() : false;
|
||||
if (sound) return sound.paused();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing sound on channel ${channel} is paused.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isSoundOnChannelStopped = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.stopped() : true;
|
||||
if (sound) return sound.stopped();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing sound on channel ${channel} is stopped.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSoundOnChannelVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.getVolume() * 100 : 100;
|
||||
if (sound) return sound.getVolume() * 100;
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the volume of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
|
||||
export const setSoundOnChannelVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
volume: float
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.setVolume(volume / 100);
|
||||
if (sound) sound.setVolume(volume / 100);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the volume of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSoundOnChannelPlayingOffset = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.getSeek() : 0;
|
||||
if (sound) return sound.getSeek();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the playing offset of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const setSoundOnChannelPlayingOffset = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
playingOffset: float
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.setSeek(playingOffset);
|
||||
if (sound) sound.setSeek(playingOffset);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the playing offset of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSoundOnChannelPitch = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
return sound ? sound.getRate() : 1;
|
||||
if (sound) return sound.getRate();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
export const setSoundOnChannelPitch = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
const sound = runtimeScene.getSoundManager().getSoundOnChannel(channel);
|
||||
sound && sound.setRate(pitch);
|
||||
if (sound) sound.setRate(pitch);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing sound on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const preloadSound = (
|
||||
@@ -173,7 +234,7 @@ namespace gdjs {
|
||||
loop: boolean,
|
||||
volume: float,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.playMusic(soundFile, loop, volume, pitch);
|
||||
@@ -186,7 +247,7 @@ namespace gdjs {
|
||||
loop: boolean,
|
||||
volume: float,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
runtimeScene
|
||||
.getSoundManager()
|
||||
.playMusicOnChannel(soundFile, channel, loop, volume, pitch);
|
||||
@@ -195,33 +256,47 @@ namespace gdjs {
|
||||
export const stopMusicOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.stop();
|
||||
if (music) music.stop();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot stop a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const pauseMusicOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.pause();
|
||||
if (music) music.pause();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot pause a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const continueMusicOnChannel = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
if (music && !music.playing()) {
|
||||
music.play();
|
||||
if (music) {
|
||||
if (!music.playing()) music.play();
|
||||
} else {
|
||||
logger.error(
|
||||
`Cannot stop a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const isMusicOnChannelPlaying = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.playing() : false;
|
||||
};
|
||||
@@ -229,68 +304,113 @@ namespace gdjs {
|
||||
export const isMusicOnChannelPaused = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.paused() : false;
|
||||
if (music) return music.paused();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing music on channel ${channel} is paused.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isMusicOnChannelStopped = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): boolean {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.stopped() : true;
|
||||
if (music) return music.stopped();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot check if non-existing music on channel ${channel} is stopped.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const getMusicOnChannelVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.getVolume() * 100 : 100;
|
||||
if (music) return music.getVolume() * 100;
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the volume of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
|
||||
export const setMusicOnChannelVolume = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
volume: float
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.setVolume(volume / 100);
|
||||
if (music) music.setVolume(volume / 100);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the volume of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMusicOnChannelPlayingOffset = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.getSeek() : 0;
|
||||
if (music) return music.getSeek();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the playing offset of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const setMusicOnChannelPlayingOffset = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
playingOffset: float
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.setSeek(playingOffset);
|
||||
if (music) music.setSeek(playingOffset);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot set the playing offset of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMusicOnChannelPitch = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer
|
||||
) {
|
||||
): float {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
return music ? music.getRate() : 1;
|
||||
if (music) return music.getRate();
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
export const setMusicOnChannelPitch = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
channel: integer,
|
||||
pitch: float
|
||||
) {
|
||||
): void {
|
||||
const music = runtimeScene.getSoundManager().getMusicOnChannel(channel);
|
||||
music && music.setRate(pitch);
|
||||
if (music) music.setRate(pitch);
|
||||
else {
|
||||
logger.error(
|
||||
`Cannot get the pitch of a non-existing music on channel ${channel}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const preloadMusic = (
|
||||
|
@@ -618,6 +618,7 @@ namespace gdjs {
|
||||
loop,
|
||||
pitch
|
||||
);
|
||||
this._storeSoundInArray(this._freeSounds, sound);
|
||||
sound.once('play', () => {
|
||||
if (this._paused) {
|
||||
sound.pause();
|
||||
@@ -625,7 +626,6 @@ namespace gdjs {
|
||||
}
|
||||
});
|
||||
sound.play();
|
||||
this._storeSoundInArray(this._freeSounds, sound);
|
||||
}
|
||||
|
||||
playSoundOnChannel(
|
||||
@@ -644,6 +644,7 @@ namespace gdjs {
|
||||
loop,
|
||||
pitch
|
||||
);
|
||||
this._sounds[channel] = sound;
|
||||
sound.once('play', () => {
|
||||
if (this._paused) {
|
||||
sound.pause();
|
||||
@@ -651,11 +652,10 @@ namespace gdjs {
|
||||
}
|
||||
});
|
||||
sound.play();
|
||||
this._sounds[channel] = sound;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -666,6 +666,7 @@ namespace gdjs {
|
||||
loop,
|
||||
pitch
|
||||
);
|
||||
this._storeSoundInArray(this._freeMusics, music);
|
||||
music.once('play', () => {
|
||||
if (this._paused) {
|
||||
music.pause();
|
||||
@@ -673,7 +674,6 @@ namespace gdjs {
|
||||
}
|
||||
});
|
||||
music.play();
|
||||
this._storeSoundInArray(this._freeMusics, music);
|
||||
}
|
||||
|
||||
playMusicOnChannel(
|
||||
@@ -692,6 +692,7 @@ namespace gdjs {
|
||||
loop,
|
||||
pitch
|
||||
);
|
||||
this._musics[channel] = music;
|
||||
music.once('play', () => {
|
||||
if (this._paused) {
|
||||
music.pause();
|
||||
@@ -699,11 +700,10 @@ namespace gdjs {
|
||||
}
|
||||
});
|
||||
music.play();
|
||||
this._musics[channel] = music;
|
||||
}
|
||||
|
||||
getMusicOnChannel(channel: integer): HowlerSound {
|
||||
return this._musics[channel];
|
||||
getMusicOnChannel(channel: integer): HowlerSound | null {
|
||||
return this._musics[channel] || null;
|
||||
}
|
||||
|
||||
setGlobalVolume(volume: float): void {
|
||||
|
@@ -230,11 +230,9 @@ namespace gdjs {
|
||||
debugDraw.fill.alpha = 0.3;
|
||||
|
||||
// Draw Center point
|
||||
const centerPointX = object.getDrawableX() + object.getCenterX();
|
||||
const centerPointY = object.getDrawableY() + object.getCenterY();
|
||||
const centerPoint = layer.convertInverseCoords(
|
||||
centerPointX,
|
||||
centerPointY
|
||||
object.getCenterXInScene(),
|
||||
object.getCenterYInScene()
|
||||
);
|
||||
|
||||
renderObjectPoint(
|
||||
@@ -245,26 +243,44 @@ namespace gdjs {
|
||||
centerPoint[1]
|
||||
);
|
||||
|
||||
// Draw Origin point
|
||||
let originPoint = [object.getDrawableX(), object.getDrawableY()];
|
||||
if (object instanceof gdjs.SpriteRuntimeObject) {
|
||||
// For Sprite objects get the position of the origin point.
|
||||
originPoint = object.getPointPosition('origin');
|
||||
}
|
||||
|
||||
originPoint = layer.convertInverseCoords(
|
||||
originPoint[0],
|
||||
originPoint[1]
|
||||
// Draw position point
|
||||
const positionPoint = layer.convertInverseCoords(
|
||||
object.getX(),
|
||||
object.getY()
|
||||
);
|
||||
|
||||
renderObjectPoint(
|
||||
renderedObjectPoints.points,
|
||||
'Origin',
|
||||
'Position',
|
||||
0xff0000,
|
||||
originPoint[0],
|
||||
originPoint[1]
|
||||
positionPoint[0],
|
||||
positionPoint[1]
|
||||
);
|
||||
|
||||
// Draw Origin point
|
||||
if (object instanceof gdjs.SpriteRuntimeObject) {
|
||||
let originPoint = object.getPointPosition('origin');
|
||||
// When there is neither rotation nor flipping,
|
||||
// the origin point is over the position point.
|
||||
if (
|
||||
Math.abs(originPoint[0] - positionPoint[0]) >= 1 ||
|
||||
Math.abs(originPoint[1] - positionPoint[1]) >= 1
|
||||
) {
|
||||
originPoint = layer.convertInverseCoords(
|
||||
originPoint[0],
|
||||
originPoint[1]
|
||||
);
|
||||
|
||||
renderObjectPoint(
|
||||
renderedObjectPoints.points,
|
||||
'Origin',
|
||||
0xff0000,
|
||||
originPoint[0],
|
||||
originPoint[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw custom point
|
||||
if (showCustomPoints && object instanceof gdjs.SpriteRuntimeObject) {
|
||||
if (!object._animationFrame) continue;
|
||||
|
@@ -27,6 +27,112 @@ namespace gdjs {
|
||||
return Math.pow(radiusX, 2) + Math.pow(radiusY, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Arrays and data structure that are (re)used by
|
||||
* {@link RuntimeObject.separateFromObjects} to avoid any allocation.
|
||||
*/
|
||||
const separateFromObjectsStatics: {
|
||||
moveXArray: Array<float>;
|
||||
moveYArray: Array<float>;
|
||||
} = {
|
||||
moveXArray: [],
|
||||
moveYArray: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Move the object using the results from collisionTest call.
|
||||
* This moves the object according to the direction of the longest vector,
|
||||
* and projects the others on the orthogonal vector.
|
||||
*
|
||||
* See {@link RuntimeObject.separateFromObjects}
|
||||
*
|
||||
* @param object The object to move.
|
||||
* @param moveXArray The X coordinates of the vectors to move the object.
|
||||
* @param moveYArray The Y coordinates of the vectors to move the object.
|
||||
* @return true if the object was moved.
|
||||
*/
|
||||
const moveFollowingSeparatingVectors = (
|
||||
object: gdjs.RuntimeObject,
|
||||
moveXArray: Array<float>,
|
||||
moveYArray: Array<float>
|
||||
): boolean => {
|
||||
if (moveXArray.length === 0) {
|
||||
moveXArray.length = 0;
|
||||
moveYArray.length = 0;
|
||||
return false;
|
||||
}
|
||||
if (moveXArray.length === 1) {
|
||||
// Move according to the results returned by the collision algorithm.
|
||||
object.setPosition(
|
||||
object.getX() + moveXArray[0],
|
||||
object.getY() + moveYArray[0]
|
||||
);
|
||||
moveXArray.length = 0;
|
||||
moveYArray.length = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find the longest vector
|
||||
let squaredDistanceMax = 0;
|
||||
let distanceMaxIndex = 0;
|
||||
for (let index = 0; index < moveXArray.length; index++) {
|
||||
const moveX = moveXArray[index];
|
||||
const moveY = moveYArray[index];
|
||||
|
||||
const squaredDistance = moveX * moveX + moveY * moveY;
|
||||
if (squaredDistance > squaredDistanceMax) {
|
||||
squaredDistanceMax = squaredDistance;
|
||||
distanceMaxIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
const distanceMax = Math.sqrt(squaredDistanceMax);
|
||||
// unit vector of the longest vector
|
||||
const uX = moveXArray[distanceMaxIndex] / distanceMax;
|
||||
const uY = moveYArray[distanceMaxIndex] / distanceMax;
|
||||
|
||||
// normal vector of the longest vector
|
||||
const vX = -uY;
|
||||
const vY = uX;
|
||||
|
||||
// Project other vectors on the normal
|
||||
let scalarProductMin = 0;
|
||||
let scalarProductMax = 0;
|
||||
for (let index = 0; index < moveXArray.length; index++) {
|
||||
const moveX = moveXArray[index];
|
||||
const moveY = moveYArray[index];
|
||||
|
||||
const scalarProduct = moveX * vX + moveY * vY;
|
||||
scalarProductMin = Math.min(scalarProductMin, scalarProduct);
|
||||
scalarProductMax = Math.max(scalarProductMax, scalarProduct);
|
||||
}
|
||||
|
||||
// Apply the longest vector
|
||||
let deltaX = moveXArray[distanceMaxIndex];
|
||||
let deltaY = moveYArray[distanceMaxIndex];
|
||||
|
||||
// Apply the longest projected vector if they all are in the same direction
|
||||
// Some projections could have rounding errors,
|
||||
// they are considered negligible under a 1 for 1,000,000 ratio.
|
||||
const scalarProductMinIsNegligible =
|
||||
-scalarProductMin < scalarProductMax / 1048576;
|
||||
const scalarProductMaxIsNegligible =
|
||||
scalarProductMax < -scalarProductMin / 1048576;
|
||||
if (scalarProductMinIsNegligible !== scalarProductMaxIsNegligible) {
|
||||
if (scalarProductMaxIsNegligible) {
|
||||
deltaX += scalarProductMin * vX;
|
||||
deltaY += scalarProductMin * vY;
|
||||
} else {
|
||||
deltaX += scalarProductMax * vX;
|
||||
deltaY += scalarProductMax * vY;
|
||||
}
|
||||
}
|
||||
object.setPosition(object.getX() + deltaX, object.getY() + deltaY);
|
||||
moveXArray.length = 0;
|
||||
moveYArray.length = 0;
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* RuntimeObject represents an object being used on a RuntimeScene.
|
||||
*
|
||||
@@ -1232,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.
|
||||
*/
|
||||
@@ -1253,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.
|
||||
*
|
||||
@@ -1647,35 +1791,52 @@ namespace gdjs {
|
||||
objects: RuntimeObject[],
|
||||
ignoreTouchingEdges: boolean
|
||||
): boolean {
|
||||
let moved = false;
|
||||
let xMove = 0;
|
||||
let yMove = 0;
|
||||
const hitBoxes = this.getHitBoxes();
|
||||
let moveXArray: Array<float> = separateFromObjectsStatics.moveXArray;
|
||||
let moveYArray: Array<float> = separateFromObjectsStatics.moveYArray;
|
||||
moveXArray.length = 0;
|
||||
moveYArray.length = 0;
|
||||
|
||||
//Check if their is a collision with each object
|
||||
for (let i = 0, len = objects.length; i < len; ++i) {
|
||||
if (objects[i].id != this.id) {
|
||||
const otherHitBoxes = objects[i].getHitBoxes();
|
||||
for (let k = 0, lenk = hitBoxes.length; k < lenk; ++k) {
|
||||
for (let l = 0, lenl = otherHitBoxes.length; l < lenl; ++l) {
|
||||
const result = gdjs.Polygon.collisionTest(
|
||||
hitBoxes[k],
|
||||
otherHitBoxes[l],
|
||||
ignoreTouchingEdges
|
||||
);
|
||||
if (result.collision) {
|
||||
xMove += result.move_axis[0];
|
||||
yMove += result.move_axis[1];
|
||||
moved = true;
|
||||
}
|
||||
// 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 there is a collision with each object
|
||||
for (const otherObject of objects) {
|
||||
if (otherObject.id === this.id) {
|
||||
continue;
|
||||
}
|
||||
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(
|
||||
hitBox,
|
||||
otherHitBox,
|
||||
ignoreTouchingEdges
|
||||
);
|
||||
if (result.collision) {
|
||||
moveXArray.push(result.move_axis[0]);
|
||||
moveYArray.push(result.move_axis[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Move according to the results returned by the collision algorithm.
|
||||
this.setPosition(this.getX() + xMove, this.getY() + yMove);
|
||||
return moved;
|
||||
return moveFollowingSeparatingVectors(this, moveXArray, moveYArray);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1688,40 +1849,58 @@ namespace gdjs {
|
||||
objectsLists: ObjectsLists,
|
||||
ignoreTouchingEdges: boolean
|
||||
): boolean {
|
||||
let moved = false;
|
||||
let xMove = 0;
|
||||
let yMove = 0;
|
||||
let moveXArray: Array<float> = separateFromObjectsStatics.moveXArray;
|
||||
let moveYArray: Array<float> = separateFromObjectsStatics.moveYArray;
|
||||
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)) {
|
||||
const objects = objectsLists.items[name];
|
||||
const otherObjects = objectsLists.items[name];
|
||||
|
||||
//Check if their is a collision with each object
|
||||
for (let i = 0, len = objects.length; i < len; ++i) {
|
||||
if (objects[i].id != this.id) {
|
||||
const otherHitBoxes = objects[i].getHitBoxes();
|
||||
for (let k = 0, lenk = hitBoxes.length; k < lenk; ++k) {
|
||||
for (let l = 0, lenl = otherHitBoxes.length; l < lenl; ++l) {
|
||||
const result = gdjs.Polygon.collisionTest(
|
||||
hitBoxes[k],
|
||||
otherHitBoxes[l],
|
||||
ignoreTouchingEdges
|
||||
);
|
||||
if (result.collision) {
|
||||
xMove += result.move_axis[0];
|
||||
yMove += result.move_axis[1];
|
||||
moved = true;
|
||||
}
|
||||
// Check if their is a collision with each object
|
||||
for (const otherObject of otherObjects) {
|
||||
if (otherObject.id === this.id) {
|
||||
continue;
|
||||
}
|
||||
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(
|
||||
hitBox,
|
||||
otherHitBox,
|
||||
ignoreTouchingEdges
|
||||
);
|
||||
if (result.collision) {
|
||||
moveXArray.push(result.move_axis[0]);
|
||||
moveYArray.push(result.move_axis[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Move according to the results returned by the collision algorithm.
|
||||
this.setPosition(this.getX() + xMove, this.getY() + yMove);
|
||||
return moved;
|
||||
return moveFollowingSeparatingVectors(this, moveXArray, moveYArray);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1993,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
|
||||
@@ -2005,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;
|
||||
}
|
||||
@@ -2072,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;
|
||||
@@ -2162,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;
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
3896
GDJS/package-lock.json
generated
3896
GDJS/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 642 B |
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,8 @@ module.exports = function (config) {
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightobstacleruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/PathfindingBehavior/pathfindingobstacleruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/PathfindingBehavior/pathfindingruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/PrimitiveDrawing/shapepainterruntimeobject.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/PrimitiveDrawing/shapepainterruntimeobject-pixi-renderer.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TopDownMovementBehavior/topdownmovementruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/TopDownMovementBehavior/topdownobstacleruntimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Firebase/A_firebasejs/*.js',
|
||||
@@ -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',
|
||||
|
117
GDJS/tests/tests/runtimeobject.separateFromObjects.js
Normal file
117
GDJS/tests/tests/runtimeobject.separateFromObjects.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Common tests for gdjs game engine.
|
||||
* See README.md for more information.
|
||||
*/
|
||||
|
||||
describe('gdjs.RuntimeObject.separateFromObjects', () => {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
// @ts-ignore TODO: make a function to create an empty game and use it across tests.
|
||||
properties: { windowWidth: 800, windowHeight: 600 },
|
||||
resources: { resources: [] },
|
||||
});
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
|
||||
const object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
});
|
||||
object.setCustomWidthAndHeight(100, 100);
|
||||
object.setCustomCenter(0, 0);
|
||||
|
||||
const obstacle1 = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
});
|
||||
obstacle1.setCustomWidthAndHeight(100, 100);
|
||||
obstacle1.setCustomCenter(0, 0);
|
||||
|
||||
const obstacle2 = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
});
|
||||
obstacle2.setCustomWidthAndHeight(100, 100);
|
||||
obstacle2.setCustomCenter(0, 0);
|
||||
|
||||
it('can be separated from 2 aligned objects', () => {
|
||||
object.setPosition(200, 300);
|
||||
// 2 obstacles on the left
|
||||
obstacle1.setPosition(290, 250);
|
||||
obstacle2.setPosition(290, 350);
|
||||
|
||||
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
|
||||
true
|
||||
);
|
||||
expect(object.getX()).to.be(190);
|
||||
expect(object.getY()).to.be(300);
|
||||
});
|
||||
|
||||
it('can be separated from 2 not exactly aligned objects', () => {
|
||||
object.setPosition(200, 300);
|
||||
// 2 obstacles on the left
|
||||
obstacle1.setPosition(290, 250);
|
||||
obstacle2.setPosition(295, 350);
|
||||
|
||||
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
|
||||
true
|
||||
);
|
||||
expect(object.getX()).to.be(190);
|
||||
expect(object.getY()).to.be(300);
|
||||
});
|
||||
|
||||
it('can be separated from 2 objects that form a corner', () => {
|
||||
object.setPosition(200, 300);
|
||||
// 1 obstacle on the top
|
||||
obstacle1.setPosition(250, 220);
|
||||
// 1 obstacle on the left
|
||||
obstacle2.setPosition(290, 250);
|
||||
|
||||
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
|
||||
true
|
||||
);
|
||||
expect(object.getX()).to.be(190);
|
||||
expect(object.getY()).to.be(320);
|
||||
});
|
||||
|
||||
it('can be separated from 2 rotated objects', () => {
|
||||
object.setPosition(200, 300);
|
||||
// 1 obstacle on the top left corner
|
||||
obstacle1.setPosition(250, 280);
|
||||
obstacle1.setAngle(-45);
|
||||
// 1 obstacle on the bottom left corner
|
||||
obstacle2.setPosition(250, 420);
|
||||
obstacle2.setAngle(-45);
|
||||
|
||||
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
|
||||
true
|
||||
);
|
||||
expect(object.getX()).to.be(170);
|
||||
expect(object.getY()).to.be(300);
|
||||
});
|
||||
|
||||
it('can be separated from 2 aligned objects when everything is rotated', () => {
|
||||
object.setPosition(240, 350);
|
||||
object.setAngle(-45);
|
||||
// 2 obstacles on the top left
|
||||
obstacle1.setPosition(250, 250);
|
||||
obstacle1.setAngle(-45);
|
||||
obstacle2.setPosition(330, 330);
|
||||
obstacle1.setAngle(-45);
|
||||
|
||||
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
|
||||
true
|
||||
);
|
||||
expect(object.getX()).to.be.within(224, 225);
|
||||
expect(object.getY()).to.be.within(365, 366);
|
||||
});
|
||||
});
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -23,6 +23,9 @@ declare class gdEventsFunctionsExtension extends gdEventsFunctionsContainer {
|
||||
getIconUrl(): string;
|
||||
setHelpPath(helpPath: string): gdEventsFunctionsExtension;
|
||||
getHelpPath(): string;
|
||||
setOrigin(originName: string, originIdentifier: string): void;
|
||||
getOriginName(): string;
|
||||
getOriginIdentifier(): string;
|
||||
addDependency(): gdDependencyMetadata;
|
||||
removeDependencyAt(index: number): void;
|
||||
getAllDependencies(): gdVectorDependencyMetadata;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdEventsListUnfolder {
|
||||
static unfoldWhenContaining(list: gdEventsList, eventToContain: gdBaseEvent): void;
|
||||
static foldAll(list: gdEventsList): void;
|
||||
static unfoldToLevel(list: gdEventsList, maxLevel: number, currentLevel?: number): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -9,6 +9,8 @@ declare class gdPropertyDescriptor {
|
||||
getLabel(): string;
|
||||
setDescription(label: string): gdPropertyDescriptor;
|
||||
getDescription(): string;
|
||||
setGroup(label: string): gdPropertyDescriptor;
|
||||
getGroup(): string;
|
||||
addExtraInfo(type: string): gdPropertyDescriptor;
|
||||
setExtraInfo(info: gdVectorString): gdPropertyDescriptor;
|
||||
getExtraInfo(): gdVectorString;
|
||||
|
@@ -50,14 +50,15 @@ install:
|
||||
|
||||
cd ..\..
|
||||
|
||||
# Package the app for Windows.
|
||||
# Package the app for Windows (and sign it with the certificate set in environment variables).
|
||||
# Don't sign the appx (it will be signed by the Microsoft Store).
|
||||
build_script:
|
||||
- cmd: >-
|
||||
- ps: >-
|
||||
cd newIDE\electron-app
|
||||
|
||||
node --max-old-space-size=3072 scripts/build.js --win appx --publish=never
|
||||
node --max-old-space-size=3072 scripts/build.js --win nsis --publish=never
|
||||
|
||||
node scripts/build.js --skip-app-build --win nsis --publish=never
|
||||
Remove-Item -Path Env:CSC_LINK ; Remove-Item -Path Env:CSC_KEY_PASSWORD ; node scripts/build.js --skip-app-build --win appx --publish=never
|
||||
|
||||
cd ..\..
|
||||
|
||||
|
1
newIDE/app/.env
Normal file
1
newIDE/app/.env
Normal file
@@ -0,0 +1 @@
|
||||
EXTEND_ESLINT = true
|
1
newIDE/app/.gitignore
vendored
1
newIDE/app/.gitignore
vendored
@@ -16,7 +16,6 @@ build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
npm-debug.log
|
||||
|
||||
|
@@ -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: {
|
||||
|
51224
newIDE/app/package-lock.json
generated
51224
newIDE/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": [
|
||||
|
BIN
newIDE/app/public/res/ribbon_default/networkicon32.png
Normal file
BIN
newIDE/app/public/res/ribbon_default/networkicon32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 787 B |
@@ -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);
|
||||
|
@@ -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>
|
||||
|
@@ -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,14 +123,14 @@ export function ExampleDialog({
|
||||
cannotBeDismissed={false}
|
||||
open
|
||||
onRequestClose={onClose}
|
||||
onApply={onOpenExample}
|
||||
>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{!isCompatible && (
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
Unfortunately, this example requires a newer version of GDevelop
|
||||
to work. Upgrade GDevelop to be able to use this extension in your
|
||||
project.
|
||||
to work. Update GDevelop to be able to open this example.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
|
@@ -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>
|
||||
|
@@ -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');
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -1,17 +1,12 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import SearchBar from '../../UI/SearchBar';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import Background from '../../UI/Background';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example';
|
||||
import { FiltersChooser } from '../../UI/Search/FiltersChooser';
|
||||
import { ExampleStoreContext } from './ExampleStoreContext';
|
||||
import { ListSearchResults } from '../../UI/Search/ListSearchResults';
|
||||
import { ExampleListItem } from './ExampleListItem';
|
||||
import { ResponsiveWindowMeasurer } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import Subheader from '../../UI/Subheader';
|
||||
import { ExampleDialog } from './ExampleDialog';
|
||||
|
||||
const styles = {
|
||||
@@ -61,6 +56,12 @@ export const ExampleStore = ({ isOpening, onOpen }: Props) => {
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
style={styles.searchBar}
|
||||
tagsHandler={{
|
||||
add: filtersState.addFilter,
|
||||
remove: filtersState.removeFilter,
|
||||
chosenTags: Array.from(filtersState.chosenFilters),
|
||||
}}
|
||||
tags={filters && filters.defaultTags}
|
||||
/>
|
||||
<Line
|
||||
expand
|
||||
@@ -68,22 +69,6 @@ export const ExampleStore = ({ isOpening, onOpen }: Props) => {
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
<Background
|
||||
noFullHeight
|
||||
noExpand
|
||||
width={windowWidth === 'small' ? 150 : 250}
|
||||
>
|
||||
<ScrollView>
|
||||
<Subheader>
|
||||
<Trans>Filters</Trans>
|
||||
</Subheader>
|
||||
<FiltersChooser
|
||||
allFilters={filters}
|
||||
filtersState={filtersState}
|
||||
error={error}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Background>
|
||||
<ListSearchResults
|
||||
onRetry={fetchExamplesAndFilters}
|
||||
error={error}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import {
|
||||
@@ -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. Upgrade 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;
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -71,7 +71,8 @@ export const importExtension = async (
|
||||
await addSerializedExtensionsToProject(
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
[serializedExtension]
|
||||
[serializedExtension],
|
||||
false
|
||||
);
|
||||
return true;
|
||||
} catch (rawError) {
|
||||
|
@@ -1,18 +1,13 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import SearchBar from '../../UI/SearchBar';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import Background from '../../UI/Background';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { Column } from '../../UI/Grid';
|
||||
import { type ExtensionShortHeader } from '../../Utils/GDevelopServices/Extension';
|
||||
import { FiltersChooser } from '../../UI/Search/FiltersChooser';
|
||||
import { ExtensionStoreContext } from './ExtensionStoreContext';
|
||||
import { ListSearchResults } from '../../UI/Search/ListSearchResults';
|
||||
import { ExtensionListItem } from './ExtensionListItem';
|
||||
import { ResponsiveWindowMeasurer } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import ExtensionInstallDialog from './ExtensionInstallDialog';
|
||||
import Subheader from '../../UI/Subheader';
|
||||
|
||||
const styles = {
|
||||
searchBar: {
|
||||
@@ -24,7 +19,7 @@ const styles = {
|
||||
type Props = {|
|
||||
isInstalling: boolean,
|
||||
project: gdProject,
|
||||
onInstall: ExtensionShortHeader => Promise<void>,
|
||||
onInstall: ExtensionShortHeader => Promise<boolean>,
|
||||
showOnlyWithBehaviors: boolean,
|
||||
|};
|
||||
|
||||
@@ -76,47 +71,30 @@ export const ExtensionStore = ({
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
style={styles.searchBar}
|
||||
tagsHandler={{
|
||||
add: filtersState.addFilter,
|
||||
remove: filtersState.removeFilter,
|
||||
chosenTags: Array.from(filtersState.chosenFilters),
|
||||
}}
|
||||
tags={filters && filters.allTags}
|
||||
/>
|
||||
<ListSearchResults
|
||||
onRetry={fetchExtensionsAndFilters}
|
||||
error={error}
|
||||
searchItems={filteredSearchResults}
|
||||
getSearchItemUniqueId={getExtensionName}
|
||||
renderSearchItem={(extensionShortHeader, onHeightComputed) => (
|
||||
<ExtensionListItem
|
||||
key={extensionShortHeader.name}
|
||||
project={project}
|
||||
onHeightComputed={onHeightComputed}
|
||||
extensionShortHeader={extensionShortHeader}
|
||||
onChoose={() => {
|
||||
setSelectedExtensionShortHeader(extensionShortHeader);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Line
|
||||
expand
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
<Background
|
||||
noFullHeight
|
||||
noExpand
|
||||
width={windowWidth === 'small' ? 150 : 250}
|
||||
>
|
||||
<ScrollView>
|
||||
<Subheader>
|
||||
<Trans>Filters</Trans>
|
||||
</Subheader>
|
||||
<FiltersChooser
|
||||
allFilters={filters}
|
||||
filtersState={filtersState}
|
||||
error={error}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Background>
|
||||
<ListSearchResults
|
||||
onRetry={fetchExtensionsAndFilters}
|
||||
error={error}
|
||||
searchItems={filteredSearchResults}
|
||||
getSearchItemUniqueId={getExtensionName}
|
||||
renderSearchItem={(extensionShortHeader, onHeightComputed) => (
|
||||
<ExtensionListItem
|
||||
key={extensionShortHeader.name}
|
||||
project={project}
|
||||
onHeightComputed={onHeightComputed}
|
||||
extensionShortHeader={extensionShortHeader}
|
||||
onChoose={() => {
|
||||
setSelectedExtensionShortHeader(extensionShortHeader);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
</ResponsiveWindowMeasurer>
|
||||
@@ -127,8 +105,9 @@ export const ExtensionStore = ({
|
||||
alreadyInstalled={project.hasEventsFunctionsExtensionNamed(
|
||||
selectedExtensionShortHeader.name
|
||||
)}
|
||||
onInstall={() => {
|
||||
onInstall(selectedExtensionShortHeader);
|
||||
onInstall={async () => {
|
||||
const wasInstalled = await onInstall(selectedExtensionShortHeader);
|
||||
if (wasInstalled) setSelectedExtensionShortHeader(null);
|
||||
}}
|
||||
onClose={() => setSelectedExtensionShortHeader(null)}
|
||||
/>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user