Compare commits

..

146 Commits

Author SHA1 Message Date
Clément Pasteau
b21a191d4c wip 2021-12-21 17:20:20 +01:00
AlexandreS
650975ba6e Improve homepage layout and Create project dialog options (#3386) 2021-12-21 14:28:13 +01:00
Aurélien Vivet
6bf293bcb5 Add backdrop click on create new project dialog
Don't show in changelog
2021-12-21 04:15:52 +01:00
D8H
dac4b3ba51 Improve clarity of code (#3375)
Don't show in changelog
2021-12-17 16:38:37 +01:00
D8H
b344f5b956 Allow actions/conditions/expressions created in the editor to be sorted in groups (#3366) 2021-12-17 15:43:58 +01:00
AlexandreS
44db5362d3 Add notification badge on user chip and achievements 2021-12-17 10:59:11 +01:00
Clément Pasteau
cca0e6e66f Fix toolbar not centered on the extensions screen (#3374) 2021-12-17 10:37:11 +01:00
Arthur Pacaud
81c65f7ff7 Fix wrong network preview address shown when running in VirtualBox/VMware (#3368) 2021-12-16 10:20:58 +01:00
Clément Pasteau
b3ea46d7e6 Fix sharing buttons
Do not show in changelog
2021-12-15 13:57:25 +01:00
github-actions[bot]
0e8adaab92 Update translations 2021-12-15 13:01:35 +01:00
D8H
c055fbcb3c Split the Platformer tests in 5 files (#3360)
Only show in developer changelog
2021-12-15 11:57:43 +01:00
AlexandreS
5051de0787 Add Tutorial Opened event when clicking on tutorial on homepage
Do not show in changelog
2021-12-15 11:22:32 +01:00
Clément Pasteau
4976d8ef8b Bump IDE version
Do not show in changelog
2021-12-15 10:48:30 +01:00
Clément Pasteau
bf1ffd3e65 Improve layer visibility toggle with an explanation text 2021-12-15 10:44:52 +01:00
Clément Pasteau
9163e998f9 Improve web export with multiple sharing capabilities 2021-12-15 10:25:41 +01:00
AlexandreS
a4d0c591a8 Add possibility to choose project name before it is created
A random and fun name is generated as a suggestion
2021-12-14 17:51:26 +01:00
Florian Rival
8c717ba910 Fix videos wrongly removed removing unused resources in the resources editor
Fix #3356
2021-12-14 10:15:55 +01:00
Leo_Red
be0f760f02 Add minor UI improvements to the profile dialog (#3351) 2021-12-12 11:45:14 +00:00
Florian Rival
919d596d07 Move the copy button for the web export link to the end of the input (#3350)
Also use a component doing the layout of the button on the right.

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

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

Don't show in changelog
2021-12-08 09:54:20 +00:00
Florian Rival
dff1c88ef7 Increase parallel requests when fetching resources in the desktop app
Don't show in changelog
2021-12-07 22:35:27 +00:00
Florian Rival
4ea622ff99 Fix typo 2021-12-05 13:04:40 +00:00
Florian Rival
17ea918a91 Bump newIDE version 2021-12-03 11:23:32 +00:00
Leo_Red
cc6af8979d Animate opening of lists in the Project Manager, action/condition selector and in the Debugger (#3310) 2021-12-03 11:16:56 +00:00
github-actions[bot]
132e20fd24 Update translations [skip ci] (#3289)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-12-03 11:11:31 +00:00
Florian Rival
fb6a88785a Add Pandako to the list of contributors (Japanese translations, extensions and blog)
Don't show in changelog
2021-12-03 09:43:02 +00:00
Clément Pasteau
8a159d7ff5 Remove unused var
Do not show in changelog
2021-12-03 09:30:50 +00:00
Florian Rival
13c85bbe45 Improve custom behavior new function dialog to show a button for making an action, condition or expression instead of a single one for action
Don't show in changelog
2021-12-02 22:20:08 +00:00
Florian Rival
ce8323e8e1 Fix crash when modifying the operator for an action or condition of a "color" property of a behavior (#3327) 2021-12-02 15:54:00 +00:00
Clément Pasteau
dbc7a74e45 Fix debugger actions not hiding properly after opening it 2021-12-02 15:53:25 +01:00
Clément Pasteau
cfb1d6888e Fix sounds sometimes not playing after the first time being played (#3325) 2021-12-02 10:48:10 +00:00
Florian Rival
816dc8cc74 Fix tweens automatically deleting the object sometimes affecting newly created objects (#3321) 2021-12-02 09:40:14 +00:00
Arthur Pacaud
106549e5fa Allow usage of custom ICE servers in the P2P extension (#3301) 2021-12-01 20:08:25 +00:00
Clément Pasteau
f8ca06d530 Fix web debugger icon not updating properly
Do not show in changelog
2021-12-01 19:32:18 +01:00
AlexandreS
34cbcdbc3a Add carousel component for new start page (don't show in changelog) 2021-12-01 15:11:56 +01:00
Clément Pasteau
3b208502ae Improve DismissableTutorialMessage story
Do not show in changelog
2021-11-30 15:50:28 +01:00
Clément Pasteau
e3654fca99 improve tutorials to be fetched from backend services
Do not show in changelog
2021-11-30 14:43:57 +01:00
Florian Rival
2a386cdcf1 Fix the layer of the created object not shown for the "Create object" action in the events sheet 2021-11-29 23:43:50 +00:00
Leo_Red
b134896687 Fix wrong mention of extensions instead of examples in a text (#3306) 2021-11-29 13:16:34 +00:00
Leo_Red
705dff43bc Move scene variables into their own category in actions/conditions/expressions (#3300) 2021-11-27 16:33:31 +00:00
Leo_Red
d9eaf71ed1 Fix tabs in preference dialog disappearing when scrolling in the dialog (#3299) 2021-11-27 16:06:20 +00:00
D8H
008b4291ab Fix the "separate" action when there are several obstacles (#3236)
* Previously an object colliding with multiple objects would have been "pushed" too far from the two colliding objects. Now, it gets properly separated, in a much more natural way, avoiding some shaking/flickering on corners or when involving multiple obstacles.
2021-11-25 17:45:38 +00:00
Clément Pasteau
3596896b16 Bump IDE version 2021-11-24 17:12:25 +01:00
Clément Pasteau
db05e98bc8 Improve object lock behavior in the editor
* The objects are now selectable, to simplify unlocking them
2021-11-24 17:12:01 +01:00
Florian Rival
98c1a93da5 Update a wording the instance properties editor
Don't show in changelog
2021-11-24 15:26:44 +00:00
Florian Rival
c39d3ee35c Fix a React warning
Don't show in changelog
2021-11-24 15:13:05 +00:00
Florian Rival
c68a25573d Hide direction related actions/conditions/expressions
The builtin "8 directions" is not officially supported anymore and we should re-enable it later with an improved interface/more flexible choices.

Don't shoe in changelog
2021-11-24 15:05:18 +00:00
github-actions[bot]
2b72b6b3e7 Update translations [skip ci] (#3285)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-11-24 14:24:53 +00:00
Clément Pasteau
db60151150 Fix Tiled Sprite being incorrectly displayed (pixelated) when the X/Y offset was too large (#3287) 2021-11-24 14:05:04 +00:00
Leo_Red
0971a4b464 Move the object variables editor into the objects editor (easier to find and faster to access) (#3263)
* Also rework the other variables editor dialogs to have the toolbar buttons always at the bottom of the window.
2021-11-24 13:21:38 +00:00
Clément Pasteau
93a57b1a31 Fix the display of the decreased build limits after exporting a game (#3284) 2021-11-24 09:40:27 +00:00
github-actions[bot]
d0f7e2517d Update translations [skip ci] (#3262)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-11-23 21:48:25 +00:00
AlexandreS
9523c98cad Add multiple achievements ("badges") that can be earned while using GDevelop (#3256)
* These achievements are visible in your user profile.
* They are all for now fairly simple to collect, but we'll also later add some a bit more complex to win, to reward contributors, people invested in the community and people building great games!
2021-11-23 21:29:27 +00:00
Florian Rival
ea38a2ff0f Add conditions and expressions to compare the coordinates of the bounding box of an object (#3275) 2021-11-23 14:56:49 +00:00
Clément Pasteau
3065ba53b1 Fix arrow keys wrongly triggering page scroll when playing a game embedded in a web page (#3280) 2021-11-23 10:57:09 +00:00
Clément Pasteau
dc19f030fc Improve scene editor scrolling speed to be faster/slower according to the zoom level (#3279) 2021-11-23 10:33:21 +00:00
Florian Rival
9fb36a375f Add minor UI changes
* Don't open asset categories in the asset store
* Remove a button redirecting to the asset store as there is already a tab for it
* Make the new object dialog always take the full height of the window
2021-11-21 18:03:09 +00:00
D8H
a366934fdb Avoid to fetch again a JSON in the game engine when it's already being fetched (#3261) 2021-11-21 12:30:59 +00:00
Florian Rival
9626ea6dcf Indicate on link events that they can't be used in an extension/custom behavior 2021-11-20 17:18:21 +00:00
Oxey405
08388893bf Don't list the current scene/external event name in a Link event when choosing what to include (#3228)
* This prevents an infinite loop/crash if launching a preview after selecting the current scene/external events in a link of the same scene/external events.
2021-11-20 15:20:45 +00:00
Arthur Pacaud
2f933f2cad Refactor the implementation of the Tween behavior (#3218)
Only show in developer changelog
2021-11-20 12:37:32 +00:00
Clément Pasteau
2517b47401 Feature/allow user receive game stats (#3259)
Add a checkbox to user profile to receive weekly game stats via email
2021-11-19 16:03:23 +01:00
D8H
86cad60194 Fix platforms sometimes not properly detected when rotated and at the edge of the scene (#3260) 2021-11-19 09:32:24 +00:00
github-actions[bot]
b1658d4619 Update translations [skip ci] (#3253)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-11-16 21:24:17 +01:00
Florian Rival
c72026e8cd Show the usernames of the contributors on the wiki pages for extensions 2021-11-16 16:07:05 +00:00
Florian Rival
4936b4b104 Fix compilation on MinGW
Don't show in changelog
2021-11-16 14:50:05 +00:00
Florian Rival
5623d12eac Fix TimeScale expression not shown in expressions autocompletion
* Also fix inconsistent naming of time scale related actions/conditions and expressions.

Fix #3248
2021-11-16 11:18:29 +00:00
Clément Pasteau
8757cfe8b2 Fix autocomplete not displaying options if only translatable values (#3254)
Do not show in changelog
2021-11-16 10:51:51 +00:00
MyNameIsRinax
968402e99f Fix a typo for rotate toward angle and position in the event sheet sentence (#3252) 2021-11-16 08:21:43 +01:00
github-actions[bot]
6f59a0921d Update translations [skip ci] (#3239)
Don't show in changelog
Co-authored-by: Bouh <Bouh@users.noreply.github.com>
2021-11-15 17:26:12 +01:00
Leo_Red
167307f1c4 Fix a typo for rotate action in the event sheet sentence (#3249) 2021-11-15 17:17:04 +01:00
Florian Rival
36fb4ec9b2 Bump newIDE version 2021-11-12 11:47:21 +00:00
Florian Rival
124e1f3683 Improve changelog extraction script
Don't show in changelog
2021-11-12 11:46:57 +00:00
github-actions[bot]
9c350729a8 Update translations [skip ci] (#3235) 2021-11-12 11:43:27 +00:00
Florian Rival
9186daa782 Update Platformer starter to allow the character to walk on rotated platforms
Don't show in changelog
2021-11-11 17:18:14 +00:00
Florian Rival
c6161c4752 Add an option in the Sprite editor to allow animations to play even when the object is hidden/outside the screen 2021-11-11 17:08:40 +00:00
D8H
5d3f207216 Add major improvements to the platformer engine to better handle slopes and moving platforms (#3009)
* The characters on platform are no long stopping when going from one rotated platform to another.
* Platforms going up and down are now properly handled by characters - they won't fall or vibrate like before. You can now freely use platforms doing any movement and characters will stay on them and can move freely on them.
* Characters Y position will now stay stable when moving on a flat platform and between jumps.
* Note that if you use the collision condition to check if an object is touching a platform, you should instead use the condition "Is object on given floor": this will always work consistently.
2021-11-11 16:11:02 +00:00
Florian Rival
cf462f6c6e Fix volume sounds and musics not clamped at exactly 0 and 1 (#3234)
Don't show in changelog
2021-11-10 11:36:20 +00:00
github-actions[bot]
bc979031e3 Update translations (#3202) 2021-11-09 16:08:14 +00:00
Clément Pasteau
406bae5e12 Fix a glitch where a sound being played at a low volume can actually be heard at a high volume for a split second 2021-11-09 16:00:56 +00:00
Florian Rival
5f5f50e039 Fix wording
Don't show in changelog
2021-11-09 14:43:53 +00:00
Florian Rival
394fb4c587 Fix warning
Don't show in changelog
2021-11-08 15:24:07 +00:00
Florian Rival
599d48afca Refactor scan/removal of unused resources
Don't show in changelog
2021-11-07 19:38:50 +00:00
Florian Rival
bccef185cb Clean up some code
Don't show in changelog
2021-11-07 19:38:50 +00:00
Florian Rival
d0f4370026 Add support for blob urls in the web-app export/preview
Experimental support - just so the behavior of the export/preview are correct.

Don't show in changelog
2021-11-07 19:38:50 +00:00
Florian Rival
77d6f0310c Add support for using arbitrary URLs for images/sounds/resources in the web-app 2021-11-07 19:38:50 +00:00
Florian Rival
c73a5a046f Fix semaphore CI npm cache not working because of npm 7+ upgrading the package-lock.json
Using "npm ci" to allow clean installation without changes to the lock files.

Don't show in changelog
2021-11-04 11:48:04 +00:00
Florian Rival
c37e129a5b Implement support for the Debugger in the web-app
* One or more preview windows can be launched and used with the GDevelop Debugger, like on the desktop app.
  * To run the Debugger, click on the button next to the Play button in the toolbar and choose "Start Preview with Debugger and Performance Profiler"
* This is useful to inspect instances of objects, inspect internal messages or run the performance profiler.
* A right click on the Play button will also allow to launch a new preview, in a new window.
* Also fix the loading screen not shown in the preview on the web-app even when asked to be shown (using the game properties preview button)
2021-11-04 11:48:04 +00:00
Florian Rival
aeecb0e29f Revert Rectangle to Line in the Particle Emitter editor
Don't show in changelog
2021-11-02 10:26:57 +00:00
Tristan Rhodes
a6525e5617 Clarify names of particle types in the Particle Emitter editor (#3217) 2021-11-01 14:35:10 +00:00
Arthur Pacaud
f67aeedaeb Update esbuild (#3220)
Only show in developer changelog
2021-11-01 10:01:52 +00:00
Leo_Red
0c2f023c63 Fix contributors list (#3221) 2021-11-01 09:55:03 +00:00
Leo_Red
d6d4569dbf Update naming of events in the menus to make them easier to understand (#3212) 2021-10-29 14:47:09 +01:00
Florian Rival
965ec330cf Improve preview in the web-app so that it opens in a separate window using the size from the game properties 2021-10-28 22:51:27 +01:00
Arthur Pacaud
c09d29a959 Show the preview window, when corresponding preference is enabled, above the editor but not above all windows on the screen (#3203) 2021-10-28 20:11:03 +01:00
Florian Rival
67612009d1 Add CrazyGames.com in the links where to publish a HTML5 game (#3211) 2021-10-28 18:05:35 +01:00
Florian Rival
2da5194672 Fix crash in the Debugger
Don't show in the changelog
2021-10-28 17:32:42 +01:00
Florian Rival
7f5821a299 Fix user not always logged when opening export after relaunching the app (#3205)
* Also fix signup/login dialog closing before the signup/login is entirely finished
2021-10-27 15:43:39 +01:00
Florian Rival
a3fdeec6a7 Refactor gdjs.Logger to allow disabling specific log groups in the console (#3204)
* This reduces the logs during GDJS tests, as this was cluttering the
terminal.

Only show in developer changelog
2021-10-27 11:39:36 +01:00
Florian Rival
852ad1d92b Do not make the preview window always on top by default in preferences
Don't show in changelog
2021-10-25 22:30:21 +01:00
Florian Rival
8fdba503ab Fix potential crash in the scene editor 2021-10-25 22:21:17 +01:00
Florian Rival
50d7bec375 Bump newIDE version 2021-10-25 15:30:31 +01:00
Clément Pasteau
0c85e9bf30 Prevent sending session hits if the session is not correctly created 2021-10-25 15:58:30 +02:00
github-actions[bot]
08c41ece71 Update translations (#3197) 2021-10-25 14:27:17 +01:00
Aurélien Vivet
bd9fffba3f Remove unused test games from GDevelop 4 (#3192)
Don't show in changelog
2021-10-24 19:22:31 +01:00
github-actions[bot]
413caf6f62 Update translations (#3182) 2021-10-22 22:15:43 +01:00
Aurélien Vivet
530d0baffe Remove beta term from the docs (#3187)
Don't show in changelog
2021-10-22 22:13:14 +01:00
Aurélien Vivet
e78d2c6962 Add automation to automatically close stale issues (#3184)
Only show in developer changelog
2021-10-22 11:26:31 +01:00
Arthur Pacaud
bc606ed1be Add a console to the games debugger (#2770)
* When launching the Debugger to inspect a game, open the Console to see internal messages sent by the game, JavaScript code or the game engine.
* This is an advanced feature that is useful to find issues in your game or to see if your game is displaying any internal error.
2021-10-21 19:47:28 +01:00
Clément Pasteau
c705f89de8 Feature/external layout options (#3178)
Improve scene selection and help text for an external layout
2021-10-21 14:18:44 +02:00
github-actions[bot]
3b73b5eb6d Update translations 2021-10-21 12:40:29 +01:00
D8H
107410f0a4 Improve typing and simplify implementation of Linked Objects' getObjectsLinkedWith function (#3180)
Only show in developer changelog
2021-10-21 09:42:34 +01:00
D8H
b7b95d5e09 Add new tests for the platformer engine to prepare for the upcoming changes (#3176)
Only show in developer changelog
2021-10-20 15:40:01 +02:00
AlexandreS
a470e9b86c Fixes an issue with ghost instances of behaviors (light obstacle and pathfinder obstacle) 2021-10-20 10:45:01 +02:00
Clément Pasteau
cf5c8ae631 Improve Expression autocomplete for functions 2021-10-18 13:22:01 +02:00
AlexandreS
8f8ac2fd1e Show index of current focused result in Event Sheet 2021-10-15 09:34:48 +02:00
Clément Pasteau
cdac70425e Prevent debugger server being launched twice, causing a crash
Do not show in changelog
2021-10-14 16:58:46 +02:00
AlexandreS
378f0a48ad Add possibility to search in events sentences
* Better highlight of search results
* Remove highlights when closing the search panel
2021-10-14 15:52:30 +02:00
Clément Pasteau
e653639366 Improve Expression autocomplete (#3167)
Improve Expression Autocomplete to be more intuitive
2021-10-14 15:08:18 +02:00
Florian Rival
e105d4c9f6 Disable history when publishing the web-app to GitHub pages
Don't show in changelog
2021-10-11 16:43:33 +01:00
Florian Rival
5b80bed305 Fix crash happening when modifying "dead" instances after removing a layer and instances that were on it
Fix #3164
2021-10-11 16:39:35 +01:00
AlexandreS
a4ac323e63 Only show the operators that can be actually used in actions/conditions for strings (#3156)
* For strings/texts, only = and ≠ can be used for comparisons, and "set to"/"add to" for modifications.
2021-10-08 17:14:02 +01:00
Clément Pasteau
bc23d6a084 Fix effects being applied multiple times on objects when creating and destroying a lot of them (#3149)
* Effects from destroyed objects could be cleaned incorrectly and applied to new objects, creating glitches on newly created objects.
2021-10-08 16:14:23 +01:00
Florian Rival
2c24359fba Skip CI for Pull Request automatically open to update translations
We can always remove this tag before merging. This is to avoid doubling the usage of Travis/Semaphore CIs at every commit on master.

Don't show in changelog
2021-10-08 11:48:32 +01:00
Clément Pasteau
a6b01fc01d Add conditions for mouse key pressed from text 2021-10-08 12:15:35 +02:00
github-actions[bot]
44b81f52ea Update translations (#3154)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-10-07 23:06:41 +01:00
Florian Rival
cfdf13538e Automate update of translations (#3153)
* Automatically download and build latest translations from Crowdin at each commit on master, opening a PR with the changes (if any).
2021-10-07 22:58:50 +01:00
Florian Rival
7ee38a50bf Fix translations
Launched npm run compile-translations *after* launching npm run extract-all-translations, so that the English .po file was up-to-date to avoid missing translations in English language.

Don't show in changelog
2021-10-07 19:28:01 +01:00
Florian Rival
e2b8620b83 Fix translations
Don't show in changelog
2021-10-07 17:18:57 +01:00
AlexandreS
7ed8660edc Fix translations - Don't show in changelog 2021-10-07 18:10:18 +02:00
AlexandreS
75cc70368c Bump newIDE version and update translations 2021-10-07 16:38:19 +02:00
AlexandreS
0d3dfe5cf4 Display another user's profile when clicking their badge 2021-10-07 15:57:49 +02:00
Clément Pasteau
e7aa75bcd7 Add possibility to change one's email with confirmation 2021-10-07 13:28:44 +02:00
Clément Pasteau
c5ad127e83 Fix opening up the parameters dialog for object or behaviors expressions having no parameters 2021-10-07 12:28:27 +02:00
Clément Pasteau
acfdebfc0f Prevent calling a user update on every authState change
This fixes a race condition where the fetchUser would be called too early, causing a conflict with the signup function while creating the user in the database.

Do not show in changelog
2021-10-06 12:15:56 +02:00
Leo_Red
d3f8b410b0 Fix wording of "empty game" to "empty project" for consistency (#3136) 2021-10-06 09:04:53 +01:00
Florian Rival
4b7d67ce97 Fix very long object names overflowing or messing up some dialogs in the editor 2021-10-05 23:46:08 +01:00
Florian Rival
46a81ef4be Fix icons aligment and text ellipsis for long resource names in resources selector 2021-10-05 23:46:08 +01:00
Florian Rival
fe2812b8e8 Update caching by service worker of libGD.js/wasm
Don't show in changelog
2021-10-05 23:46:08 +01:00
Clément Pasteau
042cf49b3b Allow user to verify their email (#3132)
Allow users to verify their email
2021-10-05 18:23:29 +02:00
538 changed files with 100052 additions and 48754 deletions

15
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# Automatically close issues with certain tags indicating that we need more information,
# after some days have passed.
daysUntilStale: 20
daysUntilClose: 7
# Only do this on tags implying we need more information:
onlyLabels: ["Need a game/precise steps to reproduce the issue","👋 Needs confirmation/testing"]
only: issues
markComment: >
This issue seems to be stale: it needs additional information but it has not had
recent activity. It will be closed in 7 days if no further activity occurs. Thank you
for your contributions.

View File

@@ -0,0 +1,73 @@
# GitHub Action to update translations by downloading them from Crowdin,
# and open a Pull Request with the changes.
name: Update translations
on:
# Execute only on master
push:
branches:
- master
# Allows to run this workflow manually from the Actions tab.
workflow_dispatch:
jobs:
update-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Cache npm dependencies to speed up the workflow
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-newIDE-app-node_modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('newIDE/app/package-lock.json') }}
- name: Install gettext
run: sudo apt update && sudo apt install gettext -y
- name: Install newIDE dependencies
run: npm install
working-directory: newIDE/app
# We need to extract translations first to make sure all the source strings
# are included in the English catalogs. Otherwise, missing source strings
# with parameters (like "My name is {0}.") would be shown as-is when
# the app is built (but not in development - unclear why, LinguiJS issue?).
- name: Extract translations
run: npm run extract-all-translations
working-directory: newIDE/app
# (Build and) download the most recent translations (PO files) from Crowdin.
- name: Install Crowdin CLI
run: npm i -g @crowdin/cli
- name: Download new translations from Crowdin
run: crowdin download
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# Seems like the three letters code is not handled properly by LinguiJS?
# Do without this language while we find a solution.
- name: Remove catalogs not handled properly by LinguiJS compile command.
run: rm -rf newIDE/app/src/locales/pcm_NG/
- name: Compile translations into .js files that are read by LinguiJS
run: npm run compile-translations
working-directory: newIDE/app
- name: Create a Pull Request with the changes
uses: peter-evans/create-pull-request@v3.10.1
with:
commit-message: Update translations [skip ci]
branch: chore/update-translations
delete-branch: true
title: '[Auto PR] Update translations'
body: |
This updates the translations by downloading them from Crowdin and compiling them for usage by the app.
Please double check the values in `newIDE/app/src/locales/LocalesMetadata.js` to ensure the changes are sensible.

View File

@@ -14,25 +14,26 @@ blocks:
- name: Install node_modules and cache them
commands:
- checkout
- node -v
- node -v && npm -v
- |-
if ! cache has_key newIDE-app-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum newIDE/app/package-lock.json); then
cd newIDE/app
npm i
npm ci
cd ../..
cache store newIDE-app-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum newIDE/app/package-lock.json) newIDE/app/node_modules
fi
- |-
if ! cache has_key GDJS-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum GDJS/package-lock.json); then
cd GDJS
npm i
git checkout package-lock.json # Ensure no changes was made by newIDE post-install tasks.
npm ci
cd ..
cache store GDJS-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum GDJS/package-lock.json) GDJS/node_modules
fi
- |-
if ! cache has_key GDJS-tests-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum GDJS/tests/package-lock.json); then
cd GDJS/tests
npm i
npm ci
cd ../..
cache store GDJS-tests-node_modules-$SEMAPHORE_GIT_BRANCH-revision-$(checksum GDJS/tests/package-lock.json) GDJS/tests/node_modules
fi

2
.vscode/tasks.json vendored
View File

@@ -80,7 +80,7 @@
},
{
"type": "typescript",
"tsconfig": "GDJS/tsconfig.json",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": ["$tsc-watch"],
"group": "test",

View File

@@ -67,13 +67,13 @@ class GD_CORE_API ExpressionParser2 {
}
/**
* Given an object name (or empty if none) and a behavior name (or empty if none),
* return the index of the first parameter that is inside the parenthesis:
* 0, 1 or 2.
*
* For example, in an expression like `Object.MyBehavior::Method("hello")`, the
* parameter "hello" is the second parameter (the first being by convention Object,
* and the second MyBehavior, also by convention).
* Given an object name (or empty if none) and a behavior name (or empty if
* none), return the index of the first parameter that is inside the
* parenthesis: 0, 1 or 2.
*
* For example, in an expression like `Object.MyBehavior::Method("hello")`,
* the parameter "hello" is the second parameter (the first being by
* convention Object, and the second MyBehavior, also by convention).
*/
static size_t WrittenParametersFirstIndex(const gd::String &objectName,
const gd::String &behaviorName) {
@@ -403,9 +403,18 @@ class GD_CORE_API ExpressionParser2 {
const gd::ExpressionMetadata &metadata =
MetadataProvider::GetAnyExpressionMetadata(platform, functionFullName);
// In case we can't find a valid expression, ensure the node has the type
// that is requested by the parent, so we avoid putting "unknown" (which
// would be also correct, but less precise and would prevent completions to
// be shown to the user)
const gd::String returnType =
gd::MetadataProvider::IsBadExpressionMetadata(metadata) == true
? type
: metadata.GetReturnType();
auto parametersNode = Parameters(metadata.parameters);
auto function =
gd::make_unique<FunctionCallNode>(metadata.GetReturnType(),
gd::make_unique<FunctionCallNode>(returnType,
std::move(parametersNode.parameters),
metadata,
functionFullName);
@@ -458,9 +467,18 @@ class GD_CORE_API ExpressionParser2 {
MetadataProvider::GetObjectAnyExpressionMetadata(
platform, objectType, objectFunctionOrBehaviorName);
// In case we can't find a valid expression, ensure the node has the type
// that is requested by the parent, so we avoid putting "unknown" (which
// would be also correct, but less precise and would prevent completions
// to be shown to the user)
const gd::String returnType =
gd::MetadataProvider::IsBadExpressionMetadata(metadata) == true
? type
: metadata.GetReturnType();
auto parametersNode = Parameters(metadata.parameters, objectName);
auto function = gd::make_unique<FunctionCallNode>(
metadata.GetReturnType(),
returnType,
objectName,
std::move(parametersNode.parameters),
metadata,
@@ -520,10 +538,19 @@ class GD_CORE_API ExpressionParser2 {
MetadataProvider::GetBehaviorAnyExpressionMetadata(
platform, behaviorType, functionName);
// In case we can't find a valid expression, ensure the node has the type
// that is requested by the parent, so we avoid putting "unknown" (which
// would be also correct, but less precise and would prevent completions
// to be shown to the user)
const gd::String returnType =
gd::MetadataProvider::IsBadExpressionMetadata(metadata) == true
? type
: metadata.GetReturnType();
auto parametersNode =
Parameters(metadata.parameters, objectName, behaviorName);
auto function = gd::make_unique<FunctionCallNode>(
metadata.GetReturnType(),
returnType,
objectName,
behaviorName,
std::move(parametersNode.parameters),

View File

@@ -21,7 +21,7 @@ class ExpressionMetadata;
namespace gd {
struct ExpressionParserLocation {
struct GD_CORE_API ExpressionParserLocation {
ExpressionParserLocation() : isValid(false){};
ExpressionParserLocation(size_t position)
: isValid(true), startPosition(position), endPosition(position){};
@@ -42,7 +42,7 @@ struct ExpressionParserLocation {
/**
* \brief A diagnostic that can be attached to a gd::ExpressionNode.
*/
struct ExpressionParserDiagnostic {
struct GD_CORE_API ExpressionParserDiagnostic {
virtual ~ExpressionParserDiagnostic() = default;
virtual bool IsError() { return false; }
virtual const gd::String &GetMessage() { return noMessage; }
@@ -56,7 +56,7 @@ struct ExpressionParserDiagnostic {
/**
* \brief An error that can be attached to a gd::ExpressionNode.
*/
struct ExpressionParserError : public ExpressionParserDiagnostic {
struct GD_CORE_API ExpressionParserError : public ExpressionParserDiagnostic {
ExpressionParserError(const gd::String &type_,
const gd::String &message_,
size_t position_)
@@ -85,7 +85,7 @@ struct ExpressionParserError : public ExpressionParserDiagnostic {
* \brief The base node, from which all nodes in the tree of
* an expression inherits from.
*/
struct ExpressionNode {
struct GD_CORE_API ExpressionNode {
ExpressionNode(const gd::String &type_) : type(type_){};
virtual ~ExpressionNode(){};
virtual void Visit(ExpressionParser2NodeWorker &worker){};
@@ -104,7 +104,7 @@ struct ExpressionNode {
// gd::ParameterMetadata::IsExpression or "unknown".
};
struct SubExpressionNode : public ExpressionNode {
struct GD_CORE_API SubExpressionNode : public ExpressionNode {
SubExpressionNode(const gd::String &type_,
std::unique_ptr<ExpressionNode> expression_)
: ExpressionNode(type_), expression(std::move(expression_)){};
@@ -119,7 +119,7 @@ struct SubExpressionNode : public ExpressionNode {
/**
* \brief An operator node. For example: "lhs + rhs".
*/
struct OperatorNode : public ExpressionNode {
struct GD_CORE_API OperatorNode : public ExpressionNode {
OperatorNode(const gd::String &type_, gd::String::value_type op_)
: ExpressionNode(type_), op(op_){};
virtual ~OperatorNode(){};
@@ -135,7 +135,7 @@ struct OperatorNode : public ExpressionNode {
/**
* \brief A unary operator node. For example: "-2".
*/
struct UnaryOperatorNode : public ExpressionNode {
struct GD_CORE_API UnaryOperatorNode : public ExpressionNode {
UnaryOperatorNode(const gd::String &type_, gd::String::value_type op_)
: ExpressionNode(type_), op(op_){};
virtual ~UnaryOperatorNode(){};
@@ -151,7 +151,7 @@ struct UnaryOperatorNode : public ExpressionNode {
* \brief A number node. For example: "123".
* Its `type` is always "number".
*/
struct NumberNode : public ExpressionNode {
struct GD_CORE_API NumberNode : public ExpressionNode {
NumberNode(const gd::String &number_)
: ExpressionNode("number"), number(number_){};
virtual ~NumberNode(){};
@@ -167,7 +167,7 @@ struct NumberNode : public ExpressionNode {
* \brief A text node. For example: "Hello World".
* Its `type` is always "string".
*/
struct TextNode : public ExpressionNode {
struct GD_CORE_API TextNode : public ExpressionNode {
TextNode(const gd::String &text_) : ExpressionNode("string"), text(text_){};
virtual ~TextNode(){};
virtual void Visit(ExpressionParser2NodeWorker &worker) {
@@ -177,7 +177,7 @@ struct TextNode : public ExpressionNode {
gd::String text;
};
struct VariableAccessorOrVariableBracketAccessorNode : public ExpressionNode {
struct GD_CORE_API VariableAccessorOrVariableBracketAccessorNode : public ExpressionNode {
VariableAccessorOrVariableBracketAccessorNode() : ExpressionNode(""){};
std::unique_ptr<VariableAccessorOrVariableBracketAccessorNode> child;
@@ -191,7 +191,7 @@ struct VariableAccessorOrVariableBracketAccessorNode : public ExpressionNode {
* \see gd::VariableAccessorNode
* \see gd::VariableBracketAccessorNode
*/
struct VariableNode : public ExpressionNode {
struct GD_CORE_API VariableNode : public ExpressionNode {
VariableNode(const gd::String &type_,
const gd::String &name_,
const gd::String &objectName_)
@@ -214,7 +214,7 @@ struct VariableNode : public ExpressionNode {
* \brief A bracket accessor of a variable. Example: MyChild
* in MyVariable.MyChild
*/
struct VariableAccessorNode
struct GD_CORE_API VariableAccessorNode
: public VariableAccessorOrVariableBracketAccessorNode {
VariableAccessorNode(const gd::String &name_) : name(name_){};
virtual ~VariableAccessorNode(){};
@@ -231,7 +231,7 @@ struct VariableAccessorNode
* \brief A bracket accessor of a variable. Example: ["MyChild"]
* (in MyVariable["MyChild"]).
*/
struct VariableBracketAccessorNode
struct GD_CORE_API VariableBracketAccessorNode
: public VariableAccessorOrVariableBracketAccessorNode {
VariableBracketAccessorNode(std::unique_ptr<ExpressionNode> expression_)
: expression(std::move(expression_)){};
@@ -243,7 +243,7 @@ struct VariableBracketAccessorNode
std::unique_ptr<ExpressionNode> expression;
};
struct IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode
struct GD_CORE_API IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode
: public ExpressionNode {
IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode(
const gd::String &type)
@@ -253,7 +253,7 @@ struct IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode
/**
* \brief An identifier node, usually representing an object or a function name.
*/
struct IdentifierNode
struct GD_CORE_API IdentifierNode
: public IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode {
IdentifierNode(const gd::String &identifierName_, const gd::String &type_)
: IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode(type_),
@@ -266,7 +266,7 @@ struct IdentifierNode
gd::String identifierName;
};
struct FunctionCallOrObjectFunctionNameOrEmptyNode
struct GD_CORE_API FunctionCallOrObjectFunctionNameOrEmptyNode
: public IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode {
FunctionCallOrObjectFunctionNameOrEmptyNode(const gd::String &type)
: IdentifierOrFunctionCallOrObjectFunctionNameOrEmptyNode(type){};
@@ -279,7 +279,7 @@ struct FunctionCallOrObjectFunctionNameOrEmptyNode
* For example: "MyObject.Function" or "MyObject.Physics" or
* "MyObject.Physics::LinearVelocity".
*/
struct ObjectFunctionNameNode
struct GD_CORE_API ObjectFunctionNameNode
: public FunctionCallOrObjectFunctionNameOrEmptyNode {
ObjectFunctionNameNode(const gd::String &type_,
const gd::String &objectName_,
@@ -332,7 +332,7 @@ struct ObjectFunctionNameNode
* For example: "MyExtension::MyFunction(1, 2)", "MyObject.Function()" or
* "MyObject.Physics::LinearVelocity()".
*/
struct FunctionCallNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
struct GD_CORE_API FunctionCallNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
/** \brief Construct a free function call node. */
FunctionCallNode(const gd::String &type_,
std::vector<std::unique_ptr<ExpressionNode>> parameters_,
@@ -400,7 +400,7 @@ struct FunctionCallNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
* \brief An empty node, used when parsing failed/a syntax error was
* encountered and any other node could not make sense.
*/
struct EmptyNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
struct GD_CORE_API EmptyNode : public FunctionCallOrObjectFunctionNameOrEmptyNode {
EmptyNode(const gd::String &type_, const gd::String &text_ = "")
: FunctionCallOrObjectFunctionNameOrEmptyNode(type_), text(text_){};
virtual ~EmptyNode(){};

View File

@@ -83,9 +83,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"res/actions/position.png")
.AddParameter("object", _("Object"))
.AddParameter("operator", _("Modification's sign"))
.AddParameter("operator", _("Modification's sign"), "number")
.AddParameter("expression", _("X position"))
.AddParameter("operator", _("Modification's sign"))
.AddParameter("operator", _("Modification's sign"), "number")
.AddParameter("expression", _("Y position"))
.MarkAsSimple();
@@ -98,15 +98,15 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"res/actions/position24.png",
"res/actions/position.png")
.AddParameter("object", _("Object"))
.AddParameter("operator", _("Modification's sign"))
.AddParameter("operator", _("Modification's sign"), "number")
.AddParameter("expression", _("X position"))
.AddParameter("operator", _("Modification's sign"))
.AddParameter("operator", _("Modification's sign"), "number")
.AddParameter("expression", _("Y position"))
.MarkAsSimple();
obj.AddExpressionAndConditionAndAction("number", "CenterX",
_("Center X position"),
_("the X position of the center"),
_("the X position of the center of rotation"),
_("the X position of the center"),
_("Position/Center"),
"res/actions/position24.png")
@@ -115,13 +115,67 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddExpressionAndConditionAndAction("number", "CenterY",
_("Center Y position"),
_("the Y position of the center"),
_("the Y position of the center of rotation"),
_("the Y position of the center"),
_("Position/Center"),
"res/actions/position24.png")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxLeft",
_("Bounding box left position"),
_("the bounding box (the area encapsulating the object) left position"),
_("the bounding box left position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-left.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxTop",
_("Bounding box top position"),
_("the bounding box (the area encapsulating the object) top position"),
_("the bounding box top position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-top.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxRight",
_("Bounding box right position"),
_("the bounding box (the area encapsulating the object) right position"),
_("the bounding box right position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-right.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxBottom",
_("Bounding box bottom position"),
_("the bounding box (the area encapsulating the object) bottom position"),
_("the bounding box bottom position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-bottom.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxCenterX",
_("Bounding box center X position"),
_("the bounding box (the area encapsulating the object) center X position"),
_("the bounding box center X position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-center.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddExpressionAndCondition("number", "BoundingBoxCenterY",
_("Bounding box center Y position"),
_("the bounding box (the area encapsulating the object) center Y position"),
_("the bounding box center Y position"),
_("Position/Bounding Box"),
"res/conditions/bounding-box-center.svg")
.AddParameter("object", _("Object"))
.UseStandardParameters("number");
obj.AddAction("MettreAutourPos",
_("Put around a position"),
_("Position the center of the given object around a position, "
@@ -155,7 +209,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Rotate"),
_("Rotate an object, clockwise if the speed is positive, "
"counterclockwise otherwise."),
_("Rotate _PARAM0_ at speed _PARAM1_deg/second"),
_("Rotate _PARAM0_ at speed _PARAM1_ deg/second"),
_("Angle"),
"res/actions/direction24.png",
"res/actions/direction.png")
@@ -169,7 +223,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"RotateTowardAngle",
_("Rotate toward angle"),
_("Rotate an object towards an angle with the specified speed."),
_("Rotate _PARAM0_ towards _PARAM1_ at speed _PARAM2_deg/second"),
_("Rotate _PARAM0_ towards _PARAM1_ at speed _PARAM2_ deg/second"),
_("Angle"),
"res/actions/direction24.png",
"res/actions/direction.png")
@@ -185,7 +239,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Rotate toward position"),
_("Rotate an object towards a position, with the specified speed."),
_("Rotate _PARAM0_ towards _PARAM1_;_PARAM2_ at speed "
"_PARAM3_deg/second"),
"_PARAM3_ deg/second"),
_("Angle"),
"res/actions/direction24.png",
"res/actions/direction.png")
@@ -1143,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")
@@ -1161,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")

View File

@@ -419,7 +419,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
_("Layer time scale"),
_("Compare the time scale applied to the objects of the layer."),
_("the time scale of layer _PARAM1_"),
_("Layers and cameras/Time"),
_("Layers and cameras"),
"res/conditions/time24.png",
"res/conditions/time.png")
.AddCodeOnlyParameter("currentScene", "")
@@ -433,8 +433,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
"ChangeLayerTimeScale",
_("Change layer time scale"),
_("Change the time scale applied to the objects of the layer."),
_("Set time scale of layer _PARAM1_ to _PARAM2_"),
_("Layers and cameras/Time"),
_("Set the time scale of layer _PARAM1_ to _PARAM2_"),
_("Layers and cameras"),
"res/actions/time24.png",
"res/actions/time.png")
.AddCodeOnlyParameter("currentScene", "")
@@ -551,8 +551,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
extension
.AddExpression("LayerTimeScale",
_("Time scale"),
_("Time scale"),
_("Layer time scale"),
_("Returns the time scale of the specified layer."),
_("Layers and cameras"),
"res/actions/time.png")
.AddCodeOnlyParameter("currentScene", "")

View File

@@ -83,29 +83,29 @@ BuiltinExtensionsImplementer::ImplementsCommonInstructionsExtension(
std::make_shared<gd::StandardEvent>());
extension.AddEvent("Link",
_("Link"),
_("Link to some external events"),
_("Link external events"),
_("Link to external events."),
"",
"res/lienaddicon.png",
std::make_shared<gd::LinkEvent>());
extension.AddEvent("Comment",
_("Comment"),
_("Event displaying a text in the events editor"),
_("Event displaying a text in the events editor."),
"",
"res/comment.png",
std::make_shared<gd::CommentEvent>());
extension.AddEvent("While",
_("While"),
_("The event is repeated while the conditions are true"),
_("Repeat the event while the conditions are true."),
"",
"res/while.png",
std::make_shared<gd::WhileEvent>());
extension.AddEvent("Repeat",
_("Repeat"),
_("Event repeated a number of times"),
_("Repeat the event for a specified number of times."),
"",
"res/repeat.png",
std::make_shared<gd::RepeatEvent>());
@@ -126,8 +126,8 @@ BuiltinExtensionsImplementer::ImplementsCommonInstructionsExtension(
std::make_shared<gd::ForEachChildVariableEvent>());
extension.AddEvent("Group",
_("Group"),
_("Group containing events"),
_("Event group"),
_("Group containing events."),
"",
"res/foreach.png",
std::make_shared<gd::GroupEvent>());

View File

@@ -26,7 +26,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
extension
.AddCondition("KeyPressed",
_("Key pressed"),
_("Test if a key is pressed"),
_("Check if a key is pressed"),
_("_PARAM1_ key is pressed"),
_("Keyboard"),
"res/conditions/keyboard24.png",
@@ -37,7 +37,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
extension
.AddCondition("KeyReleased",
_("Key released"),
_("Test if a key was just released"),
_("Check if a key was just released"),
_("_PARAM1_ key is released"),
_("Keyboard"),
"res/conditions/keyboard24.png",
@@ -48,33 +48,33 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
extension
.AddCondition("KeyFromTextPressed",
_("Key pressed (text expression)"),
_("Test if a key, retrieved from the result of the "
_("Check if a key, retrieved from the result of the "
"expression, is pressed"),
_("_PARAM1_ key is pressed"),
_("Keyboard"),
"res/conditions/keyboard24.png",
"res/conditions/keyboard.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("string", _("Expression generating the key to test"))
.AddParameter("string", _("Expression generating the key to check"))
.MarkAsAdvanced();
extension
.AddCondition("KeyFromTextReleased",
_("Key released (text expression)"),
_("Test if a key, retrieved from the result of the "
_("Check if a key, retrieved from the result of the "
"expression, was just released"),
_("_PARAM1_ key is released"),
_("Keyboard"),
"res/conditions/keyboard24.png",
"res/conditions/keyboard.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("string", _("Expression generating the key to test"))
.AddParameter("string", _("Expression generating the key to check"))
.MarkAsAdvanced();
extension
.AddCondition("AnyKeyPressed",
_("Any key pressed"),
_("Test if any key is pressed"),
_("Check if any key is pressed"),
_("Any key is pressed"),
_("Keyboard"),
"res/conditions/keyboard24.png",
@@ -84,7 +84,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
extension
.AddCondition("AnyKeyReleased",
_("Any key released"),
_("Test if any key is released"),
_("Check if any key is released"),
_("Any key is released"),
_("Keyboard"),
"res/conditions/keyboard24.png",

View File

@@ -216,6 +216,39 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
.AddParameter("mouse", _("Button to check"))
.MarkAsSimple();
extension
.AddCondition(
"MouseButtonFromTextPressed",
_("Mouse button pressed or touch held (text expression)"),
_("Check if a mouse button, retrieved from the result of the "
"expression, is pressed."),
_("_PARAM1_ mouse button is pressed"),
_("Mouse and touch"),
"res/conditions/mouse24.png",
"res/conditions/mouse.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("string", _("Expression generating the button to check"))
.SetParameterLongDescription(
_("Possible values are Left, Right and Middle."))
.MarkAsAdvanced();
extension
.AddCondition(
"MouseButtonFromTextReleased",
_("Mouse button released (text expression)"),
_("Check if a mouse button, retrieved from the result of the "
"expression, was just released."),
_("_PARAM1_ mouse button is released"),
_("Mouse and touch"),
"res/conditions/mouse24.png",
"res/conditions/mouse.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("string",
_("Expression generating the mouse button to check"))
.SetParameterLongDescription(
_("Possible values are Left, Right and Middle."))
.MarkAsAdvanced();
extension
.AddExpressionAndCondition("number",
"TouchX",
@@ -301,7 +334,6 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
_("Mouse and touch/Multitouch"),
"res/conditions/touch.png")
.AddCodeOnlyParameter("currentScene", "");
}
} // namespace gd

View File

@@ -195,7 +195,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
"res/conditions/egal.png")
.SetHelpPath("/all-features/advanced-conditions")
.AddParameter("expression", _("First expression"))
.AddParameter("relationalOperator", _("Sign of the test"))
.AddParameter("relationalOperator", _("Sign of the test"), "number")
.AddParameter("expression", _("Second expression"))
.MarkAsAdvanced();
@@ -209,7 +209,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
"res/conditions/egal.png")
.SetHelpPath("/all-features/advanced-conditions")
.AddParameter("string", _("First string expression"))
.AddParameter("relationalOperator", _("Sign of the test"))
.AddParameter("relationalOperator", _("Sign of the test"), "string")
.AddParameter("string", _("Second string expression"))
.MarkAsAdvanced();
}

View File

@@ -78,7 +78,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
_("Direction"),
"res/actions/direction24.png",
"res/actions/direction.png")
.SetHidden() // Hide as 8 direction is not supported officially in the interface.
.AddParameter("object", _("Object"), "Sprite")
.UseStandardOperatorParameters("number")
.MarkAsAdvanced();
@@ -242,7 +242,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
_("Direction"),
"res/conditions/direction24.png",
"res/conditions/direction.png")
.SetHidden() // Hide as 8 direction is not supported officially in the interface.
.AddParameter("object", _("Object"), "Sprite")
.UseStandardRelationalOperatorParameters("number");
@@ -464,6 +464,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
_("Direction of the object"),
_("Direction"),
"res/actions/direction.png")
.SetHidden() // Hide as 8 direction is not supported officially in the interface.
.AddParameter("object", _("Object"), "Sprite");
obj.AddExpression("Anim",

View File

@@ -116,6 +116,18 @@ class GD_CORE_API SpriteObject : public gd::Object {
* animation of the object.
*/
const std::vector<Animation>& GetAllAnimations() const { return animations; }
/**
* \brief Set if the object animation should be played even if the object is hidden
* or far from the camera.
*/
void SetUpdateIfNotVisible(bool updateIfNotVisible_) { updateIfNotVisible = updateIfNotVisible_; }
/**
* \brief Check if the object animation should be played even if the object is hidden
* or far from the camera (false by default).
*/
bool GetUpdateIfNotVisible() const { return updateIfNotVisible; }
///@}
private:

View File

@@ -38,8 +38,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
extension
.AddCondition("TimeScale",
_("Time scale"),
_("Test the time scale."),
_("the time scale"),
_("Compare the time scale of the scene."),
_("the time scale of the scene"),
_("Timers and time"),
"res/conditions/time24.png",
"res/conditions/time.png")
@@ -111,8 +111,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
extension
.AddAction("ChangeTimeScale",
_("Change time scale"),
_("Change the time scale of the game."),
_("Set time scale to _PARAM1_"),
_("Change the time scale of the scene."),
_("Set the time scale of the scene to _PARAM1_"),
_("Timers and time"),
"res/actions/time24.png",
"res/actions/time.png")
@@ -175,20 +175,11 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
extension
.AddExpression("TimeScale",
_("Time scale"),
_("Time scale"),
_("Returns the time scale of the scene."),
_("Time"),
"res/actions/time.png")
.AddCodeOnlyParameter("currentScene", "");
extension
.AddExpression("TimeScale",
_("Time scale"),
_("Time scale"),
_("Time"),
"res/actions/time.png")
.SetHidden()
.AddCodeOnlyParameter("currentScene", "");
extension
.AddExpression("Time",
_("Current time"),

View File

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

View File

@@ -34,7 +34,7 @@ ExpressionMetadata& ExpressionMetadata::SetHidden() {
gd::ExpressionMetadata& ExpressionMetadata::AddParameter(
const gd::String& type,
const gd::String& description,
const gd::String& optionalObjectType,
const gd::String& supplementaryInformation,
bool parameterIsOptional) {
gd::ParameterMetadata info;
info.type = type;
@@ -46,15 +46,15 @@ gd::ExpressionMetadata& ExpressionMetadata::AddParameter(
// parameter is an object/behavior type...
(gd::ParameterMetadata::IsObject(type) ||
gd::ParameterMetadata::IsBehavior(type))
? (optionalObjectType.empty()
? (supplementaryInformation.empty()
? ""
: extensionNamespace +
optionalObjectType //... so prefix it with the extension
supplementaryInformation //... so prefix it with the extension
// namespace.
)
: optionalObjectType; // Otherwise don't change anything
: supplementaryInformation; // Otherwise don't change anything
// TODO: Assert against optionalObjectType === "emsc" (when running with
// TODO: Assert against supplementaryInformation === "emsc" (when running with
// Emscripten), and warn about a missing argument when calling addParameter.
parameters.push_back(info);

View File

@@ -190,7 +190,7 @@ class GD_CORE_API ExpressionMetadata {
gd::ExpressionMetadata& AddParameter(
const gd::String& type,
const gd::String& description,
const gd::String& optionalObjectType = "",
const gd::String& supplementaryInformation = "",
bool parameterIsOptional = false);
/**

View File

@@ -51,7 +51,7 @@ InstructionMetadata::InstructionMetadata(const gd::String& extensionNamespace_,
InstructionMetadata& InstructionMetadata::AddParameter(
const gd::String& type,
const gd::String& description,
const gd::String& optionalObjectType,
const gd::String& supplementaryInformation,
bool parameterIsOptional) {
ParameterMetadata info;
info.type = type;
@@ -63,15 +63,15 @@ InstructionMetadata& InstructionMetadata::AddParameter(
// parameter is an object/behavior type...
(gd::ParameterMetadata::IsObject(type) ||
gd::ParameterMetadata::IsBehavior(type))
? (optionalObjectType.empty()
? (supplementaryInformation.empty()
? ""
: extensionNamespace +
optionalObjectType //... so prefix it with the extension
supplementaryInformation //... so prefix it with the extension
// namespace.
)
: optionalObjectType; // Otherwise don't change anything
: supplementaryInformation; // Otherwise don't change anything
// TODO: Assert against optionalObjectType === "emsc" (when running with
// TODO: Assert against supplementaryInformation === "emsc" (when running with
// Emscripten), and warn about a missing argument when calling addParameter.
parameters.push_back(info);
@@ -93,7 +93,7 @@ InstructionMetadata& InstructionMetadata::UseStandardOperatorParameters(
const gd::String& type) {
SetManipulatedType(type);
AddParameter("operator", _("Modification's sign"));
AddParameter("operator", _("Modification's sign"), type);
AddParameter(type == "number" ? "expression" : type, _("Value"));
size_t operatorParamIndex = parameters.size() - 2;
size_t valueParamIndex = parameters.size() - 1;
@@ -129,7 +129,7 @@ InstructionMetadata::UseStandardRelationalOperatorParameters(
const gd::String& type) {
SetManipulatedType(type);
AddParameter("relationalOperator", _("Sign of the test"));
AddParameter("relationalOperator", _("Sign of the test"), type);
AddParameter(type == "number" ? "expression" : type, _("Value to compare"));
size_t operatorParamIndex = parameters.size() - 2;
size_t valueParamIndex = parameters.size() - 1;

View File

@@ -6,7 +6,6 @@
#ifndef INSTRUCTIONMETADATA_H
#define INSTRUCTIONMETADATA_H
#if defined(GD_IDE_ONLY)
#include <functional>
#include <map>
#include <memory>
@@ -137,8 +136,11 @@ class GD_CORE_API InstructionMetadata {
* will also determine the type of the argument used when calling the function
* in the generated code.
* \param description Description for parameter
* \param optionalObjectType If type is "object", this parameter will describe
* which objects are allowed. If it is empty, all objects are allowed.
* \param supplementaryInformation Additional information that can be used for
* rendering or logic. For example:
* - If type is "object", this argument will describe which objects are allowed.
* If this argument is empty, all objects are allowed.
* - If type is "operator", this argument will be used to display only pertinent operators.
* \param parameterIsOptional true if the parameter must be optional, false
* otherwise.
*
@@ -146,7 +148,7 @@ class GD_CORE_API InstructionMetadata {
*/
InstructionMetadata &AddParameter(const gd::String &type,
const gd::String &label,
const gd::String &optionalObjectType = "",
const gd::String &supplementaryInformation = "",
bool parameterIsOptional = false);
/**
@@ -319,7 +321,7 @@ class GD_CORE_API InstructionMetadata {
* "CppPlatform/Extensions/text.png");
*
* .AddParameter("object", _("Object"), "Text", false)
* .AddParameter("operator", _("Modification operator"))
* .AddParameter("operator", _("Modification operator"), "string")
* .AddParameter("string", _("String"))
* .SetFunctionName("SetString").SetManipulatedType("string").SetGetter("GetString").SetIncludeFile("MyExtension/TextObject.h");
*
@@ -452,5 +454,4 @@ class GD_CORE_API InstructionMetadata {
} // namespace gd
#endif
#endif // INSTRUCTIONMETADATA_H

View File

@@ -38,19 +38,17 @@ class GD_CORE_API MultipleInstructionMetadata {
MultipleInstructionMetadata &AddParameter(
const gd::String &type,
const gd::String &label,
const gd::String &optionalObjectType = "",
const gd::String &supplementaryInformation = "",
bool parameterIsOptional = false) {
#if defined(GD_IDE_ONLY)
if (expression)
expression->AddParameter(
type, label, optionalObjectType, parameterIsOptional);
type, label, supplementaryInformation, parameterIsOptional);
if (condition)
condition->AddParameter(
type, label, optionalObjectType, parameterIsOptional);
type, label, supplementaryInformation, parameterIsOptional);
if (action)
action->AddParameter(
type, label, optionalObjectType, parameterIsOptional);
#endif
type, label, supplementaryInformation, parameterIsOptional);
return *this;
}
@@ -59,13 +57,11 @@ class GD_CORE_API MultipleInstructionMetadata {
*/
MultipleInstructionMetadata &AddCodeOnlyParameter(
const gd::String &type, const gd::String &supplementaryInformation) {
#if defined(GD_IDE_ONLY)
if (expression)
expression->AddCodeOnlyParameter(type, supplementaryInformation);
if (condition)
condition->AddCodeOnlyParameter(type, supplementaryInformation);
if (action) action->AddCodeOnlyParameter(type, supplementaryInformation);
#endif
return *this;
}
@@ -73,11 +69,9 @@ class GD_CORE_API MultipleInstructionMetadata {
* \see gd::InstructionMetadata::SetDefaultValue
*/
MultipleInstructionMetadata &SetDefaultValue(const gd::String &defaultValue) {
#if defined(GD_IDE_ONLY)
if (expression) expression->SetDefaultValue(defaultValue);
if (condition) condition->SetDefaultValue(defaultValue);
if (action) action->SetDefaultValue(defaultValue);
#endif
return *this;
};
@@ -86,11 +80,9 @@ class GD_CORE_API MultipleInstructionMetadata {
*/
MultipleInstructionMetadata &SetParameterLongDescription(
const gd::String &longDescription) {
#if defined(GD_IDE_ONLY)
if (expression) expression->SetParameterLongDescription(longDescription);
if (condition) condition->SetParameterLongDescription(longDescription);
if (action) action->SetParameterLongDescription(longDescription);
#endif
return *this;
};
@@ -98,11 +90,9 @@ class GD_CORE_API MultipleInstructionMetadata {
* \see gd::InstructionMetadata::SetHidden
*/
MultipleInstructionMetadata &SetHidden() {
#if defined(GD_IDE_ONLY)
if (expression) expression->SetHidden();
if (condition) condition->SetHidden();
if (action) action->SetHidden();
#endif
return *this;
};
@@ -111,50 +101,40 @@ class GD_CORE_API MultipleInstructionMetadata {
* \see gd::InstructionMetadata::UseStandardRelationalOperatorParameters
*/
MultipleInstructionMetadata &UseStandardParameters(const gd::String &type) {
#if defined(GD_IDE_ONLY)
if (condition) condition->UseStandardRelationalOperatorParameters(type);
if (action) action->UseStandardOperatorParameters(type);
#endif
return *this;
}
MultipleInstructionMetadata &SetFunctionName(const gd::String &functionName) {
#if defined(GD_IDE_ONLY)
if (expression) expression->SetFunctionName(functionName);
if (condition) condition->SetFunctionName(functionName);
if (action) action->GetCodeExtraInformation().SetFunctionName(functionName);
#endif
return *this;
}
MultipleInstructionMetadata &SetGetter(const gd::String &getter) {
#if defined(GD_IDE_ONLY)
if (expression) expression->SetFunctionName(getter);
if (condition) condition->SetFunctionName(getter);
if (action) action->GetCodeExtraInformation().SetGetter(getter);
#endif
return *this;
}
MultipleInstructionMetadata &SetIncludeFile(const gd::String &includeFile) {
#if defined(GD_IDE_ONLY)
if (expression)
expression->GetCodeExtraInformation().SetIncludeFile(includeFile);
if (condition)
condition->GetCodeExtraInformation().SetIncludeFile(includeFile);
if (action) action->GetCodeExtraInformation().SetIncludeFile(includeFile);
#endif
return *this;
}
MultipleInstructionMetadata &AddIncludeFile(const gd::String &includeFile) {
#if defined(GD_IDE_ONLY)
if (expression)
expression->GetCodeExtraInformation().AddIncludeFile(includeFile);
if (condition)
condition->GetCodeExtraInformation().AddIncludeFile(includeFile);
if (action) action->GetCodeExtraInformation().AddIncludeFile(includeFile);
#endif
return *this;
}
@@ -162,10 +142,8 @@ class GD_CORE_API MultipleInstructionMetadata {
* \see gd::InstructionMetadata::MarkAsSimple
*/
MultipleInstructionMetadata &MarkAsSimple() {
#if defined(GD_IDE_ONLY)
if (condition) condition->MarkAsSimple();
if (action) action->MarkAsSimple();
#endif
return *this;
}
@@ -173,10 +151,8 @@ class GD_CORE_API MultipleInstructionMetadata {
* \see gd::InstructionMetadata::MarkAsAdvanced
*/
MultipleInstructionMetadata &MarkAsAdvanced() {
#if defined(GD_IDE_ONLY)
if (condition) condition->MarkAsAdvanced();
if (action) action->MarkAsAdvanced();
#endif
return *this;
}
@@ -184,10 +160,8 @@ class GD_CORE_API MultipleInstructionMetadata {
* \see gd::InstructionMetadata::MarkAsComplex
*/
MultipleInstructionMetadata &MarkAsComplex() {
#if defined(GD_IDE_ONLY)
if (condition) condition->MarkAsComplex();
if (action) action->MarkAsComplex();
#endif
return *this;
}

View File

@@ -19,11 +19,14 @@
#include "GDCore/Extensions/Platform.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/ObjectsContainer.h"
#include "GDCore/IDE/Events/InstructionSentenceFormatter.h"
using namespace std;
namespace gd {
const gd::String EventsRefactorer::searchIgnoredCharacters = ";:,#()";
/**
* \brief Go through the nodes and change the given object name to a new one.
*
@@ -675,16 +678,27 @@ bool EventsRefactorer::ReplaceStringInConditions(
}
vector<EventsSearchResult> EventsRefactorer::SearchInEvents(
gd::ObjectsContainer& project,
gd::ObjectsContainer& layout,
const gd::Platform& platform,
gd::EventsList& events,
gd::String search,
bool matchCase,
bool inConditions,
bool inActions,
bool inEventStrings) {
bool inEventStrings,
bool inEventSentences) {
vector<EventsSearchResult> results;
const gd::String& ignored_characters = EventsRefactorer::searchIgnoredCharacters;
search.replace_if(search.begin(),
search.end(),
[ignored_characters](const char &c) {
return ignored_characters.find(c) != gd::String::npos;
},
"");
search = search.LeftTrim().RightTrim();
search.RemoveConsecutiveOccurrences(search.begin(), search.end(), ' ');
for (std::size_t i = 0; i < events.size(); ++i) {
bool eventAddedInResults = false;
@@ -694,7 +708,7 @@ vector<EventsSearchResult> EventsRefactorer::SearchInEvents(
for (std::size_t j = 0; j < conditionsVectors.size(); ++j) {
if (!eventAddedInResults &&
SearchStringInConditions(
project, layout, *conditionsVectors[j], search, matchCase)) {
platform, *conditionsVectors[j], search, matchCase, inEventSentences)) {
results.push_back(EventsSearchResult(
std::weak_ptr<gd::BaseEvent>(events.GetEventSmartPtr(i)),
&events,
@@ -709,7 +723,7 @@ vector<EventsSearchResult> EventsRefactorer::SearchInEvents(
for (std::size_t j = 0; j < actionsVectors.size(); ++j) {
if (!eventAddedInResults &&
SearchStringInActions(
project, layout, *actionsVectors[j], search, matchCase)) {
platform, *actionsVectors[j], search, matchCase, inEventSentences)) {
results.push_back(EventsSearchResult(
std::weak_ptr<gd::BaseEvent>(events.GetEventSmartPtr(i)),
&events,
@@ -720,7 +734,7 @@ vector<EventsSearchResult> EventsRefactorer::SearchInEvents(
if (inEventStrings) {
if (!eventAddedInResults &&
SearchStringInEvent(project, layout, events[i], search, matchCase)) {
SearchStringInEvent(events[i], search, matchCase)) {
results.push_back(EventsSearchResult(
std::weak_ptr<gd::BaseEvent>(events.GetEventSmartPtr(i)),
&events,
@@ -730,14 +744,14 @@ vector<EventsSearchResult> EventsRefactorer::SearchInEvents(
if (events[i].CanHaveSubEvents()) {
vector<EventsSearchResult> subResults =
SearchInEvents(project,
layout,
SearchInEvents(platform,
events[i].GetSubEvents(),
search,
matchCase,
inConditions,
inActions,
inEventStrings);
inEventStrings,
inEventSentences);
std::copy(
subResults.begin(), subResults.end(), std::back_inserter(results));
}
@@ -746,11 +760,12 @@ vector<EventsSearchResult> EventsRefactorer::SearchInEvents(
return results;
}
bool EventsRefactorer::SearchStringInActions(gd::ObjectsContainer& project,
gd::ObjectsContainer& layout,
gd::InstructionsList& actions,
gd::String search,
bool matchCase) {
bool EventsRefactorer::SearchStringInActions(
const gd::Platform& platform,
gd::InstructionsList& actions,
gd::String search,
bool matchCase,
bool inSentences) {
for (std::size_t aId = 0; aId < actions.size(); ++aId) {
for (std::size_t pNb = 0; pNb < actions[aId].GetParameters().size();
++pNb) {
@@ -765,24 +780,60 @@ bool EventsRefactorer::SearchStringInActions(gd::ObjectsContainer& project,
if (foundPosition != gd::String::npos) return true;
}
if (inSentences && SearchStringInFormattedText(
platform, actions[aId], search, matchCase, false))
return true;
if (!actions[aId].GetSubInstructions().empty() &&
SearchStringInActions(project,
layout,
SearchStringInActions(platform,
actions[aId].GetSubInstructions(),
search,
matchCase))
matchCase,
inSentences))
return true;
}
return false;
}
bool EventsRefactorer::SearchStringInFormattedText(
const gd::Platform& platform,
gd::Instruction& instruction,
gd::String search,
bool matchCase,
bool isCondition) {
const auto& metadata = isCondition
? gd::MetadataProvider::GetConditionMetadata(
platform, instruction.GetType())
: gd::MetadataProvider::GetActionMetadata(
platform, instruction.GetType());
gd::String completeSentence = gd::InstructionSentenceFormatter::Get()->GetFullText(instruction, metadata);
const gd::String& ignored_characters = EventsRefactorer::searchIgnoredCharacters;
completeSentence.replace_if(completeSentence.begin(),
completeSentence.end(),
[ignored_characters](const char &c) {
return ignored_characters.find(c) != gd::String::npos;
},
"");
completeSentence.RemoveConsecutiveOccurrences(
completeSentence.begin(), completeSentence.end(), ' ');
size_t foundPosition = matchCase
? completeSentence.find(search)
: completeSentence.FindCaseInsensitive(search);
return foundPosition != gd::String::npos;
}
bool EventsRefactorer::SearchStringInConditions(
gd::ObjectsContainer& project,
gd::ObjectsContainer& layout,
const gd::Platform& platform,
gd::InstructionsList& conditions,
gd::String search,
bool matchCase) {
bool matchCase,
bool inSentences) {
for (std::size_t cId = 0; cId < conditions.size(); ++cId) {
for (std::size_t pNb = 0; pNb < conditions[cId].GetParameters().size();
++pNb) {
@@ -797,21 +848,23 @@ bool EventsRefactorer::SearchStringInConditions(
if (foundPosition != gd::String::npos) return true;
}
if (inSentences && SearchStringInFormattedText(
platform, conditions[cId], search, matchCase, true))
return true;
if (!conditions[cId].GetSubInstructions().empty() &&
SearchStringInConditions(project,
layout,
SearchStringInConditions(platform,
conditions[cId].GetSubInstructions(),
search,
matchCase))
matchCase,
inSentences))
return true;
}
return false;
}
bool EventsRefactorer::SearchStringInEvent(gd::ObjectsContainer& project,
gd::ObjectsContainer& layout,
gd::BaseEvent& event,
bool EventsRefactorer::SearchStringInEvent(gd::BaseEvent& event,
gd::String search,
bool matchCase) {
for (gd::String str : event.GetAllSearchableStrings()) {

View File

@@ -41,7 +41,7 @@ class GD_CORE_API EventsSearchResult {
std::size_t positionInList;
bool IsEventsListValid() const { return eventsList != nullptr; }
/**
* \brief Get the events list containing the event pointed by the EventsSearchResult.
* \warning Only call this when IsEventsListValid returns true.
@@ -49,7 +49,7 @@ class GD_CORE_API EventsSearchResult {
const gd::EventsList & GetEventsList() const { return *eventsList; }
std::size_t GetPositionInList() const { return positionInList; }
bool IsEventValid() const { return !event.expired(); }
/**
@@ -72,7 +72,7 @@ class GD_CORE_API EventsSearchResult {
class GD_CORE_API EventsRefactorer {
public:
/**
* Replace all occurences of an object name by another name
* Replace all occurrences of an object name by another name
* ( include : objects in parameters and in math/text expressions of all
* events ).
*/
@@ -98,14 +98,14 @@ class GD_CORE_API EventsRefactorer {
* \return A vector containing EventsSearchResult objects filled with events
* containing the string
*/
static std::vector<EventsSearchResult> SearchInEvents(gd::ObjectsContainer& project,
gd::ObjectsContainer& layout,
static std::vector<EventsSearchResult> SearchInEvents(const gd::Platform& platform,
gd::EventsList& events,
gd::String search,
bool matchCase,
bool inConditions,
bool inActions,
bool inEventStrings);
bool inEventStrings,
bool inEventSentences);
/**
* Replace all occurrences of a gd::String in events
@@ -123,7 +123,7 @@ class GD_CORE_API EventsRefactorer {
private:
/**
* Replace all occurences of an object name by another name in an action
* Replace all occurrences of an object name by another name in an action
* ( include : objects in parameters and in math/text expressions ).
*
* \return true if something was modified.
@@ -136,7 +136,7 @@ class GD_CORE_API EventsRefactorer {
gd::String newName);
/**
* Replace all occurences of an object name by another name in a condition
* Replace all occurrences of an object name by another name in a condition
* ( include : objects in parameters and in math/text expressions ).
*
* \return true if something was modified.
@@ -185,7 +185,7 @@ class GD_CORE_API EventsRefactorer {
gd::String name);
/**
* Replace all occurences of a gd::String in conditions
* Replace all occurrences of a gd::String in conditions
*
* \return true if something was modified.
*/
@@ -197,7 +197,7 @@ class GD_CORE_API EventsRefactorer {
bool matchCase);
/**
* Replace all occurences of a gd::String in actions
* Replace all occurrences of a gd::String in actions
*
* \return true if something was modified.
*/
@@ -208,21 +208,26 @@ class GD_CORE_API EventsRefactorer {
gd::String newString,
bool matchCase);
static bool SearchStringInActions(gd::ObjectsContainer& project,
gd::ObjectsContainer& layout,
static bool SearchStringInFormattedText(const gd::Platform& platform,
gd::Instruction& instruction,
gd::String search,
bool matchCase,
bool isCondition);
static bool SearchStringInActions(const gd::Platform& platform,
gd::InstructionsList& actions,
gd::String search,
bool matchCase);
static bool SearchStringInConditions(gd::ObjectsContainer& project,
gd::ObjectsContainer& layout,
bool matchCase,
bool inSentences);
static bool SearchStringInConditions(const gd::Platform& platform,
gd::InstructionsList& conditions,
gd::String search,
bool matchCase);
static bool SearchStringInEvent(gd::ObjectsContainer& project,
gd::ObjectsContainer& layout,
gd::BaseEvent& events,
gd::String search,
bool matchCase);
bool matchCase,
bool inSentences);
static bool SearchStringInEvent(gd::BaseEvent& events,
gd::String search,
bool matchCase);
static const gd::String searchIgnoredCharacters;
EventsRefactorer(){};
};

View File

@@ -9,12 +9,12 @@
#include <memory>
#include <vector>
#include "GDCore/Events/Parsers/ExpressionParser2.h"
#include "GDCore/Events/Parsers/ExpressionParser2Node.h"
#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h"
#include "GDCore/Extensions/Metadata/ExpressionMetadata.h"
#include "GDCore/Extensions/Metadata/InstructionMetadata.h"
#include "GDCore/IDE/Events/ExpressionNodeLocationFinder.h"
#include "GDCore/Events/Parsers/ExpressionParser2.h"
namespace gd {
class Expression;
@@ -32,7 +32,7 @@ namespace gd {
* The IDE is responsible for actually *searching* and showing the completions -
* this is only describing what must be listed.
*/
struct ExpressionCompletionDescription {
struct GD_CORE_API ExpressionCompletionDescription {
public:
/**
* The different kind of completions that can be described.
@@ -274,7 +274,7 @@ struct ExpressionCompletionDescription {
/**
* \brief Turn an ExpressionCompletionDescription to a string.
*/
std::ostream& operator<<(std::ostream& os,
GD_CORE_API std::ostream& operator<<(std::ostream& os,
ExpressionCompletionDescription const& value);
/**
@@ -326,10 +326,7 @@ class GD_CORE_API ExpressionCompletionFinder
node.type, "", searchedPosition + 1, searchedPosition + 1));
}
void OnVisitOperatorNode(OperatorNode& node) override {
completions.push_back(ExpressionCompletionDescription::ForObject(
node.type, "", searchedPosition + 1, searchedPosition + 1));
completions.push_back(ExpressionCompletionDescription::ForExpression(
node.type, "", searchedPosition + 1, searchedPosition + 1));
// No completions.
}
void OnVisitUnaryOperatorNode(UnaryOperatorNode& node) override {
completions.push_back(ExpressionCompletionDescription::ForObject(
@@ -359,8 +356,9 @@ class GD_CORE_API ExpressionCompletionFinder
}
// Search the parameter metadata index skipping invisible ones.
size_t visibleParameterIndex = 0;
size_t metadataParameterIndex = ExpressionParser2::WrittenParametersFirstIndex(
functionCall->objectName, functionCall->behaviorName);
size_t metadataParameterIndex =
ExpressionParser2::WrittenParametersFirstIndex(
functionCall->objectName, functionCall->behaviorName);
const gd::ParameterMetadata* parameterMetadata = nullptr;
while (metadataParameterIndex <

View File

@@ -4,7 +4,6 @@
* reserved. This project is released under the MIT License.
*/
#if defined(GD_IDE_ONLY)
#include "GDCore/IDE/Events/InstructionSentenceFormatter.h"
#include <algorithm>
#include <iostream>
@@ -90,6 +89,19 @@ InstructionSentenceFormatter::GetAsFormattedText(
return formattedStr;
}
} // namespace gd
gd::String InstructionSentenceFormatter::GetFullText(
const gd::Instruction &instr, const gd::InstructionMetadata &metadata)
{
const std::vector<std::pair<gd::String, gd::TextFormatting> > formattedText =
GetAsFormattedText(instr, metadata);
#endif
gd::String completeSentence = "";
for (std::size_t id = 0; id < formattedText.size(); ++id) {
completeSentence += formattedText.at(id).first;
}
return completeSentence;
}
} // namespace gd

View File

@@ -4,7 +4,6 @@
* reserved. This project is released under the MIT License.
*/
#if defined(GD_IDE_ONLY)
#ifndef TRANSLATEACTION_H
#define TRANSLATEACTION_H
#include <map>
@@ -39,6 +38,9 @@ class GD_CORE_API InstructionSentenceFormatter {
return (static_cast<InstructionSentenceFormatter *>(_singleton));
}
gd::String GetFullText(const gd::Instruction &instr,
const gd::InstructionMetadata &metadata);
static void DestroySingleton() {
if (NULL != _singleton) {
delete _singleton;
@@ -55,4 +57,3 @@ class GD_CORE_API InstructionSentenceFormatter {
} // namespace gd
#endif // TRANSLATEACTION_H
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
#if defined(GD_IDE_ONLY)
#include "GDCore/Project/ExternalEvents.h"
#include "ExternalEvents.h"
#include "GDCore/Events/Event.h"
#include "GDCore/Events/Serialization.h"
@@ -48,4 +48,3 @@ void ExternalEvents::UnserializeFrom(gd::Project& project,
}
} // namespace gd
#endif

View File

@@ -3,12 +3,12 @@
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#if defined(GD_IDE_ONLY)
#ifndef GDCORE_EXTERNALEVENTS_H
#define GDCORE_EXTERNALEVENTS_H
#include <ctime>
#include <memory>
#include <vector>
#include "GDCore/Events/EventsList.h"
#include "GDCore/String.h"
namespace gd {
@@ -135,4 +135,3 @@ struct ExternalEventsHasName
} // namespace gd
#endif // GDCORE_EXTERNALEVENTS_H
#endif

View File

@@ -5,6 +5,7 @@
*/
#include "GDCore/Project/ExternalLayout.h"
#include "GDCore/IDE/Dialogs/LayoutEditorCanvas/EditorSettings.h"
#include "GDCore/Project/InitialInstancesContainer.h"
#include "GDCore/Serialization/SerializerElement.h"
@@ -15,19 +16,15 @@ namespace gd {
void ExternalLayout::UnserializeFrom(const SerializerElement& element) {
name = element.GetStringAttribute("name", "", "Name");
instances.UnserializeFrom(element.GetChild("instances", 0, "Instances"));
#if defined(GD_IDE_ONLY)
editorSettings.UnserializeFrom(element.GetChild("editionSettings"));
#endif
associatedLayout = element.GetStringAttribute("associatedLayout");
}
#if defined(GD_IDE_ONLY)
void ExternalLayout::SerializeTo(SerializerElement& element) const {
element.SetAttribute("name", name);
instances.SerializeTo(element.AddChild("instances"));
editorSettings.SerializeTo(element.AddChild("editionSettings"));
element.SetAttribute("associatedLayout", associatedLayout);
}
#endif
} // namespace gd

View File

@@ -7,14 +7,13 @@
#ifndef GDCORE_EXTERNALLAYOUT_H
#define GDCORE_EXTERNALLAYOUT_H
#include <memory>
#include "GDCore/Project/InitialInstancesContainer.h"
#include "GDCore/String.h"
namespace gd {
class SerializerElement;
}
#if defined(GD_IDE_ONLY)
#include "GDCore/IDE/Dialogs/LayoutEditorCanvas/EditorSettings.h"
#endif
namespace gd {
@@ -54,7 +53,6 @@ class GD_CORE_API ExternalLayout {
*/
gd::InitialInstancesContainer& GetInitialInstances() { return instances; }
#if defined(GD_IDE_ONLY)
/**
* \brief Get the user settings for the IDE.
*/
@@ -65,10 +63,7 @@ class GD_CORE_API ExternalLayout {
/**
* \brief Get the user settings for the IDE.
*/
gd::EditorSettings& GetAssociatedEditorSettings() {
return editorSettings;
}
#endif
gd::EditorSettings& GetAssociatedEditorSettings() { return editorSettings; }
/**
* \brief Get the name of the layout last used to edit the external layout.
@@ -80,15 +75,13 @@ class GD_CORE_API ExternalLayout {
*/
void SetAssociatedLayout(const gd::String& name) { associatedLayout = name; }
/** \name Serialization
*/
///@{
#if defined(GD_IDE_ONLY)
/** \name Serialization
*/
///@{
/**
* \brief Serialize external layout.
*/
void SerializeTo(SerializerElement& element) const;
#endif
/**
* \brief Unserialize the external layout.
@@ -99,9 +92,7 @@ class GD_CORE_API ExternalLayout {
private:
gd::String name;
gd::InitialInstancesContainer instances;
#if defined(GD_IDE_ONLY)
gd::EditorSettings editorSettings;
#endif
gd::String associatedLayout;
};

View File

@@ -6,6 +6,7 @@
#include "GDCore/String.h"
#include <algorithm>
#include <string.h>
#include <SFML/System/String.hpp>
@@ -283,6 +284,42 @@ String& String::insert( size_type pos, const String &str )
return *this;
}
String& String::replace_if(iterator i1, iterator i2, std::function<bool(char32_t)> p, const String &str)
{
String::size_type offset = 1;
iterator it = i1.base();
while(it < i2.base())
{
if (p(*it)) { replace(std::distance(begin(), it), offset, str); }
else { it++; }
}
return *this;
}
String& String::RemoveConsecutiveOccurrences(iterator i1, iterator i2, const char c)
{
std::vector<std::pair<size_type, size_type>> ranges_to_remove;
for(iterator current_index = i1.base(); current_index < i2.base(); current_index++)
{
if (*current_index == c){
iterator current_subindex = current_index;
std::advance(current_subindex, 1);
if (*current_subindex == c) {
while(current_subindex < end() && *current_subindex == c)
{
current_subindex++;
}
replace(std::distance(begin(), current_index),
std::distance(current_index, current_subindex),
c);
std::advance(current_index, 1);
}
}
}
return *this;
}
String& String::replace( iterator i1, iterator i2, const String &str )
{
m_string.replace(i1.base(), i2.base(), str.m_string);
@@ -290,6 +327,31 @@ String& String::replace( iterator i1, iterator i2, const String &str )
return *this;
}
String& String::replace( iterator i1, iterator i2, size_type n, const char c )
{
m_string.replace(i1.base(), i2.base(), n, c);
return *this;
}
String& String::replace( String::size_type pos, String::size_type len, const char c )
{
if(pos > size())
throw std::out_of_range("[gd::String::replace] starting pos greater than size");
iterator i1 = begin();
std::advance( i1, pos );
iterator i2 = i1;
while(i2 != end() && len > 0) //Increment "len" times and stop if end() is reached
{
++i2;
--len;
}
return replace( i1, i2, 1, c );
}
String& String::replace( String::size_type pos, String::size_type len, const String &str )
{
if(pos > size())

View File

@@ -438,15 +438,52 @@ public:
*/
String& replace( iterator i1, iterator i2, const String &str );
/**
* \brief Replace the portion of the String between **i1** and **i2** (**i2** not
* included) by **n** consecutive copies of character **c**.
* \return *this
*
* **Iterators :** All iterators may be invalidated.
*/
String& replace( iterator i1, iterator i2, size_type n, const char c );
/**
* \brief Replace the portion of the String between **pos** and **pos** + **len**
* (the character at **pos** + **len** is not included)
* (the character at **pos** + **len** is not included) with **str**.
* \return *this
*
* **Iterators :** All iterators may be invalidated.
*/
String& replace( size_type pos, size_type len, const String &str );
/**
* \brief Replace the portion of the String between **pos** and **pos** + **len**
* (the character at **pos** + **len** is not included) with the character **c**.
* \return *this
*
* **Iterators :** All iterators may be invalidated.
*/
String& replace( size_type pos, size_type len, const char c );
/**
* \brief Search in the portion of the String between **i1** and **i2** (**i2** not
* included) for characters matching predicate function **p** and replace them
* by the String **str**.
* \return *this
*
* **Iterators :** All iterators may be invalidated.
*/
String& replace_if( iterator i1, iterator i2, std::function<bool(char32_t)> p, const String &str );
/**
* \brief Remove consecutive occurrences of the character **c** in the portion of the
* between **i1** and **i2** (**i2** not included) to replace it by a single occurrence.
* \return *this
*
* **Iterators :** All iterators may be invalidated.
*/
String& RemoveConsecutiveOccurrences(iterator i1, iterator i2, const char c);
/**
* \brief Erase the characters between **first** and **last** (**last** not included).
* \param first an iterator to the first character to remove

View File

@@ -87,42 +87,27 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
}
}
SECTION("Operator (number)") {
std::vector<gd::ExpressionCompletionDescription> expectedCompletions1{
gd::ExpressionCompletionDescription::ForObject("number", "", 1, 1),
gd::ExpressionCompletionDescription::ForExpression("number", "", 1, 1)};
std::vector<gd::ExpressionCompletionDescription> expectedCompletions2{
gd::ExpressionCompletionDescription::ForObject("number", "", 2, 2),
gd::ExpressionCompletionDescription::ForExpression("number", "", 2, 2)};
std::vector<gd::ExpressionCompletionDescription> expectedCompletions3{
gd::ExpressionCompletionDescription::ForObject("number", "", 3, 3),
gd::ExpressionCompletionDescription::ForExpression("number", "", 3, 3)};
REQUIRE(getCompletionsFor("number", "1 + ", 1) == expectedCompletions1);
REQUIRE(getCompletionsFor("number", "1 + ", 2) == expectedCompletions2);
REQUIRE(getCompletionsFor("number", "1 + ", 3) == expectedCompletions3);
REQUIRE(getCompletionsFor("number", "1 + ", 1) == expectedEmptyCompletions);
REQUIRE(getCompletionsFor("number", "1 + ", 2) == expectedEmptyCompletions);
REQUIRE(getCompletionsFor("number", "1 + ", 3) == expectedEmptyCompletions);
}
SECTION("Operator (string)") {
std::vector<gd::ExpressionCompletionDescription> expectedCompletions3{
gd::ExpressionCompletionDescription::ForObject("string", "", 3, 3),
gd::ExpressionCompletionDescription::ForExpression("string", "", 3, 3)};
std::vector<gd::ExpressionCompletionDescription> expectedCompletions4{
gd::ExpressionCompletionDescription::ForObject("string", "", 4, 4),
gd::ExpressionCompletionDescription::ForExpression("string", "", 4, 4)};
std::vector<gd::ExpressionCompletionDescription> expectedCompletions5{
gd::ExpressionCompletionDescription::ForObject("string", "", 5, 5),
gd::ExpressionCompletionDescription::ForExpression("string", "", 5, 5)};
REQUIRE(getCompletionsFor("string", "\"a\" + ", 3) == expectedCompletions3);
REQUIRE(getCompletionsFor("string", "\"a\" + ", 4) == expectedCompletions4);
REQUIRE(getCompletionsFor("string", "\"a\" + ", 5) == expectedCompletions5);
REQUIRE(getCompletionsFor("string", "\"a\" + ", 3) ==
expectedEmptyCompletions);
REQUIRE(getCompletionsFor("string", "\"a\" + ", 4) ==
expectedEmptyCompletions);
REQUIRE(getCompletionsFor("string", "\"a\" + ", 5) ==
expectedEmptyCompletions);
}
SECTION("Free function") {
SECTION("Test 1") {
std::vector<gd::ExpressionCompletionDescription> expectedCompletions{
gd::ExpressionCompletionDescription::ForExpression(
"unknown", "Function", 0, 8)};
"string", "Function", 0, 8)};
std::vector<gd::ExpressionCompletionDescription> expectedExactCompletions{
gd::ExpressionCompletionDescription::ForExpression(
"unknown", "Function", 0, 8)
"string", "Function", 0, 8)
.SetIsExact(true)};
REQUIRE(getCompletionsFor("string", "Function(", 0) ==
expectedCompletions);
@@ -230,17 +215,17 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
std::vector<gd::ExpressionCompletionDescription>
expectedObjectCompletions{
gd::ExpressionCompletionDescription::ForObject(
"unknown", "MyObject", 0, 8)};
"string", "MyObject", 0, 8)};
std::vector<gd::ExpressionCompletionDescription>
expectedBehaviorOrFunctionCompletions{
gd::ExpressionCompletionDescription::ForBehavior(
"Func", 9, 13, "MyObject"),
gd::ExpressionCompletionDescription::ForExpression(
"unknown", "Func", 9, 13, "MyObject")};
"string", "Func", 9, 13, "MyObject")};
std::vector<gd::ExpressionCompletionDescription>
expectedExactFunctionCompletions{
gd::ExpressionCompletionDescription::ForExpression(
"unknown", "Func", 9, 13, "MyObject")
"string", "Func", 9, 13, "MyObject")
.SetIsExact(true)};
REQUIRE(getCompletionsFor("string", "MyObject.Func(", 0) ==
expectedObjectCompletions);
@@ -329,7 +314,7 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
std::vector<gd::ExpressionCompletionDescription>
expectedObjectCompletions{
gd::ExpressionCompletionDescription::ForObject(
"unknown", "MyObject", 0, 8)};
"string", "MyObject", 0, 8)};
std::vector<gd::ExpressionCompletionDescription>
expectedBehaviorCompletions{
gd::ExpressionCompletionDescription::ForBehavior(
@@ -337,11 +322,11 @@ TEST_CASE("ExpressionCompletionFinder", "[common][events]") {
std::vector<gd::ExpressionCompletionDescription>
expectedFunctionCompletions{
gd::ExpressionCompletionDescription::ForExpression(
"unknown", "Func", 21, 25, "MyObject", "MyBehavior")};
"string", "Func", 21, 25, "MyObject", "MyBehavior")};
std::vector<gd::ExpressionCompletionDescription>
expectedExactFunctionCompletions{
gd::ExpressionCompletionDescription::ForExpression(
"unknown", "Func", 21, 25, "MyObject", "MyBehavior")
"string", "Func", 21, 25, "MyObject", "MyBehavior")
.SetIsExact(true)};
REQUIRE(getCompletionsFor("string", "MyObject.MyBehavior::Func(", 0) ==
expectedObjectCompletions);

View File

@@ -971,6 +971,35 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
REQUIRE(objectFunctionName.objectFunctionOrBehaviorName == "");
}
SECTION("Unfinished object function name of type string with parentheses") {
auto node = parser.ParseExpression("string", "MyObject.()");
REQUIRE(node != nullptr);
auto &objectFunctionCall = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(objectFunctionCall.objectName == "MyObject");
REQUIRE(objectFunctionCall.functionName == "");
REQUIRE(objectFunctionCall.type == "string");
}
SECTION("Unfinished object function name of type number with parentheses") {
auto node = parser.ParseExpression("number", "MyObject.()");
REQUIRE(node != nullptr);
auto &objectFunctionCall = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(objectFunctionCall.objectName == "MyObject");
REQUIRE(objectFunctionCall.functionName == "");
REQUIRE(objectFunctionCall.type == "number");
}
SECTION(
"Unfinished object function name of type number|string with "
"parentheses") {
auto node = parser.ParseExpression("number|string", "MyObject.()");
REQUIRE(node != nullptr);
auto &objectFunctionCall = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(objectFunctionCall.objectName == "MyObject");
REQUIRE(objectFunctionCall.functionName == "");
REQUIRE(objectFunctionCall.type == "number|string");
}
SECTION("Unfinished object behavior name") {
auto node = parser.ParseExpression("string", "MyObject.MyBehavior::");
REQUIRE(node != nullptr);
@@ -981,6 +1010,67 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
REQUIRE(objectFunctionName.behaviorFunctionName == "");
}
SECTION("Unfinished object behavior name of type string with parentheses") {
auto node = parser.ParseExpression("string", "MyObject.MyBehavior::()");
REQUIRE(node != nullptr);
auto &objectFunctionName = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(objectFunctionName.objectName == "MyObject");
REQUIRE(objectFunctionName.behaviorName == "MyBehavior");
REQUIRE(objectFunctionName.functionName == "");
REQUIRE(objectFunctionName.type == "string");
}
SECTION("Unfinished object behavior name of type number with parentheses") {
auto node = parser.ParseExpression("number", "MyObject.MyBehavior::()");
REQUIRE(node != nullptr);
auto &objectFunctionName = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(objectFunctionName.objectName == "MyObject");
REQUIRE(objectFunctionName.behaviorName == "MyBehavior");
REQUIRE(objectFunctionName.functionName == "");
REQUIRE(objectFunctionName.type == "number");
}
SECTION(
"Unfinished object behavior name of type number|string with "
"parentheses") {
auto node =
parser.ParseExpression("number|string", "MyObject.MyBehavior::()");
REQUIRE(node != nullptr);
auto &objectFunctionName = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(objectFunctionName.objectName == "MyObject");
REQUIRE(objectFunctionName.behaviorName == "MyBehavior");
REQUIRE(objectFunctionName.functionName == "");
REQUIRE(objectFunctionName.type == "number|string");
}
SECTION("Unfinished free function name of type string with parentheses") {
auto node = parser.ParseExpression("string", "fun()");
REQUIRE(node != nullptr);
auto &freeFunctionCall = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(freeFunctionCall.objectName == "");
REQUIRE(freeFunctionCall.functionName == "fun");
REQUIRE(freeFunctionCall.type == "string");
}
SECTION("Unfinished free function name of type number with parentheses") {
auto node = parser.ParseExpression("number", "fun()");
REQUIRE(node != nullptr);
auto &freeFunctionCall = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(freeFunctionCall.objectName == "");
REQUIRE(freeFunctionCall.functionName == "fun");
REQUIRE(freeFunctionCall.type == "number");
}
SECTION(
"Unfinished free function name of type number|string with parentheses") {
auto node = parser.ParseExpression("number|string", "fun()");
REQUIRE(node != nullptr);
auto &freeFunctionCall = dynamic_cast<gd::FunctionCallNode &>(*node);
REQUIRE(freeFunctionCall.objectName == "");
REQUIRE(freeFunctionCall.functionName == "fun");
REQUIRE(freeFunctionCall.type == "number|string");
}
SECTION("Invalid function calls") {
{
auto node = parser.ParseExpression("number", "Idontexist(12)");

View File

@@ -2,6 +2,8 @@ namespace gdjs {
declare var admob: any;
export namespace adMob {
const logger = new gdjs.Logger('AdMob');
export enum AdSizeType {
BANNER,
LARGE_BANNER,
@@ -127,13 +129,13 @@ namespace gdjs {
() => {
bannerShowing = true;
bannerLoading = false;
console.info('AdMob banner successfully shown.');
logger.info('AdMob banner successfully shown.');
},
(error) => {
bannerShowing = false;
bannerLoading = false;
bannerErrored = true;
console.error('Error while showing an AdMob banner:', error);
logger.error('Error while showing an AdMob banner:', error);
}
);
};
@@ -190,14 +192,14 @@ namespace gdjs {
})
.then(
() => {
console.info('AdMob interstitial successfully loaded.');
logger.info('AdMob interstitial successfully loaded.');
if (displayWhenLoaded) showInterstitial();
},
(error) => {
interstitialLoading = false;
interstitialReady = false;
interstitialErrored = true;
console.error('Error while loading a interstitial:', error);
logger.error('Error while loading a interstitial:', error);
}
);
};
@@ -216,7 +218,7 @@ namespace gdjs {
(error) => {
interstitialShowing = false;
interstitialErrored = true;
console.error('Error while trying to show an interstitial:', error);
logger.error('Error while trying to show an interstitial:', error);
}
);
};
@@ -270,7 +272,7 @@ namespace gdjs {
})
.then(
() => {
console.info('AdMob reward video successfully loaded.');
logger.info('AdMob reward video successfully loaded.');
if (displayWhenLoaded) showVideo();
},
@@ -278,7 +280,7 @@ namespace gdjs {
videoLoading = false;
videoReady = false;
videoErrored = true;
console.error('Error while loading a reward video:', error);
logger.error('Error while loading a reward video:', error);
}
);
};
@@ -298,7 +300,7 @@ namespace gdjs {
(error) => {
videoShowing = false;
videoErrored = true;
console.error('Error while trying to show a reward video:', error);
logger.error('Error while trying to show a reward video:', error);
}
);
};

View File

@@ -83,6 +83,30 @@ module.exports = {
.setIncludeFile('Extensions/DebuggerTools/debuggertools.js')
.setFunctionName('gdjs.evtTools.debuggerTools.enableDebugDraw');
extension
.addAction(
'ConsoleLog',
_('Log a message to the console'),
_("Logs a message to the debugger's console."),
_(
'Log message _PARAM0_ of type _PARAM1_ to the console in group _PARAM2_'
),
_('Debugger Tools'),
'res/actions/bug32.png',
'res/actions/bug32.png'
)
.addParameter('string', 'Message to log', '', false)
.addParameter(
'stringWithSelector',
'Message type',
'["info", "warning", "error"]',
true
)
.addParameter('string', 'Group of messages', '', true)
.getCodeExtraInformation()
.setIncludeFile('Extensions/DebuggerTools/debuggertools.js')
.setFunctionName('gdjs.evtTools.debuggerTools.log');
return extension;
},
runExtensionSanityTests: function (

View File

@@ -13,6 +13,20 @@ namespace gdjs {
runtimeScene.getGame().pause(true);
};
/**
* Logs a message to the console.
* @param message - The message to log.
* @param type - The type of log (info, warning or error).
* @param group - The group of messages it belongs to.
*/
export const log = function (
message: string,
type: 'info' | 'warning' | 'error',
group: string
) {
gdjs.Logger.getLoggerOutput().log(group, message, type, false);
};
/**
* Enable or disable the debug draw.
* @param runtimeScene - The current scene.

View File

@@ -62,7 +62,7 @@ module.exports = {
"JsPlatform/Extensions/orientation_alpha24.png",
"JsPlatform/Extensions/orientation_alpha32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value"))
.getCodeExtraInformation()
.setIncludeFile(
@@ -82,7 +82,7 @@ module.exports = {
"JsPlatform/Extensions/orientation_beta24.png",
"JsPlatform/Extensions/orientation_beta32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value"))
.getCodeExtraInformation()
.setIncludeFile(
@@ -102,7 +102,7 @@ module.exports = {
"JsPlatform/Extensions/orientation_gamma24.png",
"JsPlatform/Extensions/orientation_gamma32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value"))
.getCodeExtraInformation()
.setIncludeFile(
@@ -228,7 +228,7 @@ module.exports = {
"JsPlatform/Extensions/motion_rotation_alpha24.png",
"JsPlatform/Extensions/motion_rotation_alpha32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value (m/s²)"))
.getCodeExtraInformation()
.setIncludeFile(
@@ -248,7 +248,7 @@ module.exports = {
"JsPlatform/Extensions/motion_rotation_beta24.png",
"JsPlatform/Extensions/motion_rotation_beta32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value (m/s²)"))
.getCodeExtraInformation()
.setIncludeFile(
@@ -268,7 +268,7 @@ module.exports = {
"JsPlatform/Extensions/motion_rotation_gamma24.png",
"JsPlatform/Extensions/motion_rotation_gamma32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value (m/s²)"))
.getCodeExtraInformation()
.setIncludeFile(
@@ -288,7 +288,7 @@ module.exports = {
"JsPlatform/Extensions/motion_acceleration_x24.png",
"JsPlatform/Extensions/motion_acceleration_x32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value (m/s²)"))
.getCodeExtraInformation()
.setIncludeFile(
@@ -308,7 +308,7 @@ module.exports = {
"JsPlatform/Extensions/motion_acceleration_y24.png",
"JsPlatform/Extensions/motion_acceleration_y32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value (m/s²)"))
.getCodeExtraInformation()
.setIncludeFile(
@@ -328,7 +328,7 @@ module.exports = {
"JsPlatform/Extensions/motion_acceleration_z24.png",
"JsPlatform/Extensions/motion_acceleration_z32.png"
)
.addParameter("relationalOperator", _("Sign of the test"))
.addParameter("relationalOperator", _("Sign of the test"), "number")
.addParameter("expression", _("Value (m/s²)"))
.getCodeExtraInformation()
.setIncludeFile(

View File

@@ -1,6 +1,8 @@
// @ts-nocheck - Weird usage of `this` in this file. Should be refactored.
namespace gdjs {
const logger = new gdjs.Logger('Dialogue tree');
gdjs.dialogueTree = {};
gdjs.dialogueTree.runner = new bondage.Runner();
@@ -21,7 +23,7 @@ namespace gdjs {
gdjs.dialogueTree.startFrom(startDialogueNode);
}
} catch (e) {
console.error(e);
logger.error('Error while loading from scene variable: ', e);
}
};
@@ -42,10 +44,7 @@ namespace gdjs {
.getJsonManager()
.loadJson(jsonResourceName, function (error, content) {
if (error) {
console.error(
'An error happened while loading JSON resource:',
error
);
logger.error('An error happened while loading JSON resource:', error);
} else {
if (!content) {
return;
@@ -54,7 +53,7 @@ namespace gdjs {
try {
gdjs.dialogueTree.runner.load(gdjs.dialogueTree.yarnData);
} catch (error) {
console.error(
logger.error(
'An error happened while loading parsing the dialogue tree data:',
error
);
@@ -155,7 +154,7 @@ namespace gdjs {
this.clipTextEnd >= this.dialogueText.length
) {
if (gdjs.dialogueTree.getVariable('debug')) {
console.warn(
logger.warn(
'Scroll completed:',
this.clipTextEnd,
'/',
@@ -244,7 +243,7 @@ namespace gdjs {
gdjs.dialogueTree.pauseScrolling = false;
commandCalls.splice(index, 1);
if (gdjs.dialogueTree.getVariable('debug')) {
console.info('CMD:', call);
logger.info('CMD:', call);
}
}, parseInt(call.params[1], 10));
}
@@ -252,7 +251,7 @@ namespace gdjs {
gdjs.dialogueTree.commandParameters = call.params;
commandCalls.splice(index, 1);
if (gdjs.dialogueTree.getVariable('debug')) {
console.info('CMD:', call);
logger.info('CMD:', call);
}
return true;
}
@@ -366,7 +365,7 @@ namespace gdjs {
this.dialogueData = this.dialogue.next().value;
gdjs.dialogueTree.goToNextDialogueLine();
} catch (error) {
console.error(
logger.error(
`An error happened when trying to access the dialogue branch!`,
error
);
@@ -562,7 +561,7 @@ namespace gdjs {
this.selectedOption = -1;
this.selectedOptionUpdated = false;
if (gdjs.dialogueTree.getVariable('debug')) {
console.info('parsing:', this.dialogueData);
logger.info('Parsing:', this.dialogueData);
}
if (!this.dialogueData) {
gdjs.dialogueTree.stopRunningDialogue();
@@ -797,7 +796,7 @@ namespace gdjs {
gdjs.dialogueTree.loadState = function (inputVariable: gdjs.Variable) {
const loadedState = inputVariable.toJSObject();
if (!loadedState) {
console.error('Load state variable is empty:', inputVariable);
logger.error('Load state variable is empty:', inputVariable);
return;
}
try {
@@ -808,7 +807,7 @@ namespace gdjs {
gdjs.dialogueTree.runner.variables.set(key, value);
});
} catch (e) {
console.error('Failed to load state from variable:', inputVariable, e);
logger.error('Failed to load state from variable:', inputVariable, e);
}
};

View File

@@ -1,5 +1,6 @@
//A simple PIXI filter doing some color changes
namespace gdjs {
const logger = new gdjs.Logger('Dummy effect');
import PIXI = GlobalPIXIModule.PIXI;
const DummyPixiFilter = function () {
@@ -46,7 +47,7 @@ namespace gdjs {
// `effectData.stringParameters.someImage`
// `effectData.stringParameters.someColor`
// `effectData.booleanParameters.someBoolean`
console.info(
logger.info(
'The PIXI texture found for the Dummy Effect (not actually used):',
(layer
.getRuntimeScene()

View File

@@ -1,4 +1,6 @@
namespace gdjs {
const logger = new gdjs.Logger('Dummy behavior');
/**
* The DummyRuntimeBehavior changes a variable in the object that is owning
* it, at every tick before events are run, to set it to the string that was
@@ -20,7 +22,7 @@ namespace gdjs {
this._textToSet = behaviorData.property1;
// You can also run arbitrary code at the creation of the behavior:
console.log('DummyRuntimeBehavior was created for object:', owner);
logger.log('DummyRuntimeBehavior was created for object:', owner);
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {

View File

@@ -1,4 +1,6 @@
namespace gdjs {
const logger = new gdjs.Logger('Dummy object');
/**
* A dummy object doing showing a text on screen.
* @ignore
@@ -126,9 +128,9 @@ namespace gdjs {
* A dummy method that can be called from events
*/
myMethod(number1: float, text1: string) {
console.log('Congrats, this method was called on a DummyRuntimeObject');
console.log('Here is the object:', this);
console.log('Here are the arguments passed from events:', number1, text1);
logger.log('Congrats, this method was called on a DummyRuntimeObject');
logger.log('Here is the object:', this);
logger.log('Here are the arguments passed from events:', number1, text1);
}
}
gdjs.registerObject(

View File

@@ -1,4 +1,5 @@
namespace gdjs {
const logger = new gdjs.Logger('Dummy behavior (with shared data)');
export class DummyWithSharedDataRuntimeBehavior extends gdjs.RuntimeBehavior {
_textToSet: string;
@@ -20,11 +21,11 @@ namespace gdjs {
this._textToSet = (sharedData as any).sharedProperty1;
// You can also run arbitrary code at the creation of the behavior:
console.log(
logger.log(
'DummyWithSharedDataRuntimeBehavior was created for object:',
owner
);
console.log('The shared data are:', sharedData);
logger.log('The shared data are:', sharedData);
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {

View File

@@ -1,4 +1,6 @@
namespace gdjs {
const logger = new gdjs.Logger('Example extension');
export namespace evtTools {
/**
* This is an example of some functions that can be used through events.
@@ -24,7 +26,7 @@ namespace gdjs {
* that will be called at this moment.
*/
gdjs.registerRuntimeSceneLoadedCallback(function (runtimeScene) {
console.log('A gdjs.RuntimeScene was loaded:', runtimeScene);
logger.log('A gdjs.RuntimeScene was loaded:', runtimeScene);
});
/**
@@ -32,7 +34,7 @@ namespace gdjs {
* that will be called at this moment.
*/
gdjs.registerRuntimeSceneUnloadedCallback(function (runtimeScene) {
console.log('A gdjs.RuntimeScene was unloaded:', runtimeScene);
logger.log('A gdjs.RuntimeScene was unloaded:', runtimeScene);
});
/**
@@ -42,7 +44,7 @@ namespace gdjs {
runtimeScene,
runtimeObject
) {
console.log(
logger.log(
'A gdjs.RuntimeObject was deleted from a gdjs.RuntimeScene:',
runtimeScene,
runtimeObject
@@ -50,7 +52,7 @@ namespace gdjs {
});
// Finally, note that you can also simply run code here. Most of the time you shouldn't need it though.
console.log(
logger.log(
'gdjs.exampleJsExtension was created, with myGlobalString containing:' +
myGlobalString
);

View File

@@ -1,4 +1,5 @@
namespace gdjs {
const logger = new gdjs.Logger('Facebook instant games');
export namespace evtTools {
export namespace facebookInstantGames {
export let _preloadedInterstitial: any = null;
@@ -153,12 +154,12 @@ namespace gdjs {
.then(function () {
gdjs.evtTools.facebookInstantGames._preloadedInterstitialLoading = false;
gdjs.evtTools.facebookInstantGames._preloadedInterstitialLoaded = true;
console.info('Facebook Instant Games interstitial preloaded.');
logger.info('Facebook Instant Games interstitial preloaded.');
})
.catch(function (err) {
gdjs.evtTools.facebookInstantGames._preloadedInterstitialLoading = false;
gdjs.evtTools.facebookInstantGames._preloadedInterstitialLoaded = false;
console.error('Interstitial failed to preload: ' + err.message);
logger.error('Interstitial failed to preload: ' + err.message);
errorVariable.setString(err.message || 'Unknown error');
});
};
@@ -173,10 +174,10 @@ namespace gdjs {
gdjs.evtTools.facebookInstantGames._preloadedInterstitial
.showAsync()
.then(function () {
console.info('Facebook Instant Games interstitial shown.');
logger.info('Facebook Instant Games interstitial shown.');
})
.catch(function (err) {
console.error('Interstitial failed to show: ' + err.message);
logger.error('Interstitial failed to show: ' + err.message);
errorVariable.setString(err.message || 'Unknown error');
})
.then(function () {
@@ -207,12 +208,12 @@ namespace gdjs {
.then(function () {
gdjs.evtTools.facebookInstantGames._preloadedRewardedVideoLoading = false;
gdjs.evtTools.facebookInstantGames._preloadedRewardedVideoLoaded = true;
console.info('Facebook Instant Games rewarded video preloaded.');
logger.info('Facebook Instant Games rewarded video preloaded.');
})
.catch(function (err) {
gdjs.evtTools.facebookInstantGames._preloadedRewardedVideoLoading = false;
gdjs.evtTools.facebookInstantGames._preloadedRewardedVideoLoaded = false;
console.error('Rewarded video failed to preload: ' + err.message);
logger.error('Rewarded video failed to preload: ' + err.message);
errorVariable.setString(err.message || 'Unknown error');
});
};
@@ -227,10 +228,10 @@ namespace gdjs {
gdjs.evtTools.facebookInstantGames._preloadedRewardedVideo
.showAsync()
.then(function () {
console.info('Facebook Instant Games rewarded video shown.');
logger.info('Facebook Instant Games rewarded video shown.');
})
.catch(function (err) {
console.error('Rewarded video failed to show: ' + err.message);
logger.error('Rewarded video failed to show: ' + err.message);
errorVariable.setString(err.message || 'Unknown error');
})
.then(function () {
@@ -242,7 +243,7 @@ namespace gdjs {
return gdjs.evtTools.facebookInstantGames._preloadedRewardedVideoLoaded;
};
if (typeof FBInstant === 'undefined' && typeof window !== 'undefined') {
console.log('Creating a mocked version of Facebook Instant Games.');
logger.log('Creating a mocked version of Facebook Instant Games.');
/**
* A mocked Leaderboard, part of the mock of FBInstant.
@@ -295,7 +296,7 @@ namespace gdjs {
showAsync(): Promise<void> {
if (this._isLoaded) {
console.info(
logger.info(
'In a real Instant Game, a video reward should have been shown to the user.'
);
return Promise.resolve();
@@ -318,7 +319,7 @@ namespace gdjs {
showAsync(): Promise<void> {
if (this._isLoaded) {
console.info(
logger.info(
'In a real Instant Game, an interstitial should have been shown to the user.'
);
return Promise.resolve();

View File

@@ -1,4 +1,5 @@
namespace gdjs {
const logger = new gdjs.Logger('Filesystem');
export namespace fileSystem {
// The Node.js path module, or null if it can't be loaded.
export let _path: any = null;
@@ -203,7 +204,7 @@ namespace gdjs {
fileSystem.mkdirSync(directory);
result = 'ok';
} catch (err) {
console.error(
logger.error(
"Unable to create directory at: '" + directory + "': ",
err
);
@@ -228,7 +229,7 @@ namespace gdjs {
fileSystem.writeFile(savePath, text, 'utf8', (err) => {
resultVar.setString('ok');
if (err) {
console.error(
logger.error(
"Unable to save the text to path: '" + savePath + "': ",
err
);
@@ -256,7 +257,7 @@ namespace gdjs {
fileSystem.writeFileSync(savePath, text, 'utf8');
result = 'ok';
} catch (err) {
console.error(
logger.error(
"Unable to save the text to path: '" + savePath + "': ",
err
);
@@ -287,7 +288,7 @@ namespace gdjs {
);
result = 'ok';
} catch (err) {
console.error(
logger.error(
"Unable to save the variable to path: '" + savePath + "': ",
err
);
@@ -316,7 +317,7 @@ namespace gdjs {
(err) => {
resultVar.setString('ok');
if (err) {
console.error(
logger.error(
"Unable to save the variable to path: '" + savePath + "': ",
err
);
@@ -348,7 +349,7 @@ namespace gdjs {
result = 'ok';
}
} catch (err) {
console.error(
logger.error(
"Unable to load the file at path: '" + loadPath + "': ",
err
);
@@ -378,7 +379,7 @@ namespace gdjs {
result = 'ok';
}
} catch (err) {
console.error(
logger.error(
"Unable to load variable from the file at path: '" +
loadPath +
"': ",
@@ -408,7 +409,7 @@ namespace gdjs {
resultVar.setString('ok');
}
if (err) {
console.error(
logger.error(
"Unable to load variable from the file at path: '" +
loadPath +
"': ",
@@ -439,7 +440,7 @@ namespace gdjs {
resultVar.setString('ok');
}
if (err) {
console.error(
logger.error(
"Unable to load the file at path: '" + loadPath + "': ",
err
);
@@ -465,7 +466,7 @@ namespace gdjs {
fileSystem.unlinkSync(filePath);
result = 'ok';
} catch (err) {
console.error("Unable to delete the file: '" + filePath + "': ", err);
logger.error("Unable to delete the file: '" + filePath + "': ", err);
result = 'error';
}
}
@@ -486,7 +487,7 @@ namespace gdjs {
fileSystem.unlink(filePath, (err) => {
resultVar.setString('ok');
if (err) {
console.error(
logger.error(
"Unable to delete the file: '" + filePath + "': ",
err
);

View File

@@ -1,4 +1,5 @@
namespace gdjs {
const logger = new gdjs.Logger('Firebase (setup)');
export namespace evtTools {
/**
* Firebase Event Tools
@@ -23,7 +24,7 @@ namespace gdjs {
.getExtensionProperty('Firebase', 'FirebaseConfig')
);
} catch (e) {
console.error('The Firebase configuration is invalid! Error: ' + e);
logger.error('The Firebase configuration is invalid! Error: ' + e);
return;
}
if (typeof firebaseConfig !== 'object') return;

View File

@@ -5,12 +5,7 @@ namespace gdjs {
_obstacleRBush: any;
constructor(runtimeScene: gdjs.RuntimeScene) {
this._obstacleRBush = new rbush(9, [
'.owner.getAABB().min[0]',
'.owner.getAABB().min[1]',
'.owner.getAABB().max[0]',
'.owner.getAABB().max[1]',
]);
this._obstacleRBush = new rbush();
}
/**
@@ -35,7 +30,10 @@ namespace gdjs {
* Add a light obstacle to the list of existing obstacles.
*/
addObstacle(obstacle: gdjs.LightObstacleRuntimeBehavior) {
this._obstacleRBush.insert(obstacle);
if (obstacle.currentRBushAABB)
obstacle.currentRBushAABB.updateAABBFromOwner();
else obstacle.currentRBushAABB = new gdjs.BehaviorRBushAABB(obstacle);
this._obstacleRBush.insert(obstacle.currentRBushAABB);
}
/**
@@ -43,7 +41,7 @@ namespace gdjs {
* added before.
*/
removeObstacle(obstacle: gdjs.LightObstacleRuntimeBehavior) {
this._obstacleRBush.remove(obstacle);
this._obstacleRBush.remove(obstacle.currentRBushAABB);
}
/**
@@ -55,7 +53,7 @@ namespace gdjs {
getAllObstaclesAround(
object: gdjs.RuntimeObject,
radius: number,
result: gdjs.LightObstacleRuntimeBehavior[]
result: gdjs.BehaviorRBushAABB<gdjs.LightObstacleRuntimeBehavior>[]
) {
// 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).
@@ -83,15 +81,22 @@ namespace gdjs {
_oldY: float = 0;
_oldWidth: float = 0;
_oldHeight: float = 0;
currentRBushAABB: gdjs.BehaviorRBushAABB<
LightObstacleRuntimeBehavior
> | null = null;
_manager: any;
_registeredInManager: boolean = false;
constructor(runtimeScene, behaviorData, owner) {
constructor(
runtimeScene: gdjs.RuntimeScene,
behaviorData,
owner: gdjs.RuntimeObject
) {
super(runtimeScene, behaviorData, owner);
this._manager = LightObstaclesManager.getManager(runtimeScene);
}
doStepPreEvents(runtimeScene) {
doStepPreEvents(runtimeScene: gdjs.RuntimeScene) {
// Make sure the obstacle is or is not in the obstacles manager.
if (!this.activated() && this._registeredInManager) {
this._manager.removeObstacle(this);

View File

@@ -1,4 +1,5 @@
namespace gdjs {
const logger = new gdjs.Logger('Light object');
import PIXI = GlobalPIXIModule.PIXI;
/**
@@ -7,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(
@@ -52,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.
@@ -84,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();
@@ -134,7 +135,7 @@ namespace gdjs {
updateMesh(): void {
if (!PIXI.utils.isWebGLSupported()) {
console.warn(
logger.warn(
'This device does not support webgl, which is required for Lighting Extension.'
);
return;
@@ -307,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;
@@ -367,8 +368,10 @@ namespace gdjs {
* Computes the vertices of mesh using raycasting.
* @returns the vertices of mesh.
*/
_computeLightVertices(): Array<any> {
const lightObstacles: Array<gdjs.LightObstacleRuntimeBehavior> = [];
_computeLightVertices(): Array<FloatPoint> {
const lightObstacles: gdjs.BehaviorRBushAABB<
LightObstacleRuntimeBehavior
>[] = [];
if (this._manager) {
this._manager.getAllObstaclesAround(
this._object,
@@ -376,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].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;
@@ -449,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]);
}
@@ -543,9 +556,11 @@ namespace gdjs {
varying vec2 vPos;
void main() {
vec2 topleft = vec2(center.x - radius, center.y - radius);
vec2 texCoord = (vPos - topleft)/(2.0 * radius);
gl_FragColor = vec4(color, 1.0) * texture2D(uSampler, texCoord);
vec2 topleft = vec2(center.x - radius, center.y - radius);
vec2 texCoord = (vPos - topleft)/(2.0 * radius);
gl_FragColor = (texCoord.x > 0.0 && texCoord.x < 1.0 && texCoord.y > 0.0 && texCoord.y < 1.0)
? vec4(color, 1.0) * texture2D(uSampler, texCoord)
: vec4(0.0, 0.0, 0.0, 0.0);
}`;
}

View File

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

View File

@@ -47,8 +47,9 @@ namespace gdjs {
/**
* @returns an iterable on every object linked with objA.
*/
// : Iterable<gdjs.RuntimeObject> in practice
getObjectsLinkedWith(objA: gdjs.RuntimeObject) {
getObjectsLinkedWith(
objA: gdjs.RuntimeObject
): Iterable<gdjs.RuntimeObject> {
if (!this._links.has(objA.id)) {
this._links.set(objA.id, new IterableLinkedObjects());
}
@@ -149,19 +150,20 @@ namespace gdjs {
}
}
class IterableLinkedObjects {
class IterableLinkedObjects implements Iterable<gdjs.RuntimeObject> {
linkedObjectMap: Map<string, gdjs.RuntimeObject[]>;
static emptyItr: Iterator<gdjs.RuntimeObject> = {
next: () => ({ value: undefined, done: true }),
};
constructor() {
this.linkedObjectMap = new Map<string, gdjs.RuntimeObject[]>();
}
[Symbol.iterator]() {
let mapItr = this.linkedObjectMap.entries();
let listItr: IterableIterator<[
number,
gdjs.RuntimeObject
]> = [].entries();
let mapItr = this.linkedObjectMap.values();
let listItr: Iterator<gdjs.RuntimeObject> =
IterableLinkedObjects.emptyItr;
return {
next: () => {
@@ -169,15 +171,12 @@ namespace gdjs {
while (listNext.done) {
const mapNext = mapItr.next();
if (mapNext.done) {
// IteratorReturnResult<gdjs.RuntimeObject> require a defined value
// even though the spec state otherwise.
// So, this class can't be typed as an iterable.
return { value: undefined, done: true };
return listNext;
}
listItr = mapNext.value[1].entries();
listItr = mapNext.value[Symbol.iterator]();
listNext = listItr.next();
}
return { value: listNext.value[1], done: false };
return listNext;
},
};
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -19,12 +19,7 @@ namespace gdjs {
* @param object The object
*/
constructor(runtimeScene: gdjs.RuntimeScene) {
this._obstaclesRBush = new rbush(9, [
'.owner.getAABB().min[0]',
'.owner.getAABB().min[1]',
'.owner.getAABB().max[0]',
'.owner.getAABB().max[1]',
]);
this._obstaclesRBush = new rbush();
}
/**
@@ -46,7 +41,14 @@ namespace gdjs {
addObstacle(
pathfindingObstacleBehavior: PathfindingObstacleRuntimeBehavior
) {
this._obstaclesRBush.insert(pathfindingObstacleBehavior);
if (pathfindingObstacleBehavior.currentRBushAABB)
pathfindingObstacleBehavior.currentRBushAABB.updateAABBFromOwner();
else
pathfindingObstacleBehavior.currentRBushAABB = new gdjs.BehaviorRBushAABB(
pathfindingObstacleBehavior
);
this._obstaclesRBush.insert(pathfindingObstacleBehavior.currentRBushAABB);
}
/**
@@ -56,7 +58,7 @@ namespace gdjs {
removeObstacle(
pathfindingObstacleBehavior: PathfindingObstacleRuntimeBehavior
) {
this._obstaclesRBush.remove(pathfindingObstacleBehavior);
this._obstaclesRBush.remove(pathfindingObstacleBehavior.currentRBushAABB);
}
/**
@@ -68,7 +70,7 @@ namespace gdjs {
x: float,
y: float,
radius: float,
result: gdjs.PathfindingObstacleRuntimeBehavior[]
result: gdjs.BehaviorRBushAABB<gdjs.PathfindingObstacleRuntimeBehavior>[]
): any {
const searchArea = gdjs.staticObject(
PathfindingObstaclesManager.prototype.getAllObstaclesAround
@@ -100,6 +102,9 @@ namespace gdjs {
_oldHeight: float = 0;
_manager: PathfindingObstaclesManager;
_registeredInManager: boolean = false;
currentRBushAABB: gdjs.BehaviorRBushAABB<
PathfindingObstacleRuntimeBehavior
> | null = null;
constructor(
runtimeScene: gdjs.RuntimeScene,

View File

@@ -3,6 +3,7 @@ GDevelop - Pathfinding Behavior Extension
Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
const logger = new gdjs.Logger('Pathfinding behavior');
/**
* PathfindingRuntimeBehavior represents a behavior allowing objects to
* follow a path computed to avoid obstacles.
@@ -542,7 +543,9 @@ namespace gdjs {
//An array of nodes sorted by their estimate cost (First node = Lower estimate cost).
_openNodes: Node[] = [];
//Used by getNodes to temporarily store obstacles near a position.
_closeObstacles: PathfindingObstacleRuntimeBehavior[] = [];
_closeObstacles: gdjs.BehaviorRBushAABB<
PathfindingObstacleRuntimeBehavior
>[] = [];
//Old nodes constructed in a previous search are stored here to avoid temporary objects (see _freeAllNodes method).
_nodeCache: Node[] = [];
@@ -612,7 +615,7 @@ namespace gdjs {
computePathTo(targetX: float, targetY: float) {
if (this._obstacles === null) {
console.log(
logger.log(
'You tried to compute a path without specifying the obstacles'
);
return;
@@ -783,7 +786,7 @@ namespace gdjs {
this._closeObstacles
);
for (let k = 0; k < this._closeObstacles.length; ++k) {
const obj = this._closeObstacles[k].owner;
const obj = this._closeObstacles[k].behavior.owner;
const topLeftCellX = Math.floor(
(obj.getDrawableX() - this._rightBorder - this._gridOffsetX) /
this._cellWidth
@@ -813,13 +816,13 @@ namespace gdjs {
yPos < bottomRightCellY
) {
objectsOnCell = true;
if (this._closeObstacles[k].isImpassable()) {
if (this._closeObstacles[k].behavior.isImpassable()) {
//The cell is impassable, stop here.
newNode.cost = -1;
break;
} else {
//Superimpose obstacles
newNode.cost += this._closeObstacles[k].getCost();
newNode.cost += this._closeObstacles[k].behavior.getCost();
}
}
}

View File

@@ -20,7 +20,6 @@ This project is released under the MIT License.
void PlatformerObjectBehavior::InitializeContent(
gd::SerializerElement& behaviorContent) {
behaviorContent.SetAttribute("roundCoordinates", true);
behaviorContent.SetAttribute("gravity", 1000);
behaviorContent.SetAttribute("maxFallingSpeed", 700);
behaviorContent.SetAttribute("ladderClimbingSpeed", 150);
@@ -80,12 +79,6 @@ PlatformerObjectBehavior::GetProperties(
gd::String::From(behaviorContent.GetDoubleAttribute("yGrabOffset")));
properties[_("Grab tolerance on X axis")].SetValue(gd::String::From(
behaviorContent.GetDoubleAttribute("xGrabTolerance", 10)));
properties[_("Round coordinates")]
.SetValue(behaviorContent.GetBoolAttribute("roundCoordinates", false)
? "true"
: "false")
.SetType("Boolean");
return properties;
}
@@ -95,8 +88,6 @@ bool PlatformerObjectBehavior::UpdateProperty(
const gd::String& value) {
if (name == _("Default controls"))
behaviorContent.SetAttribute("ignoreDefaultControls", (value == "0"));
else if (name == _("Round coordinates"))
behaviorContent.SetAttribute("roundCoordinates", (value == "1"));
else if (name == _("Can grab platform ledges"))
behaviorContent.SetAttribute("canGrabPlatforms", (value == "1"));
else if (name == _("Grab offset on Y axis"))

View File

@@ -0,0 +1,57 @@
# Platformer Extension technical documentation
## Floor following
### Horizontal search
When the character walks on a platform, he must follow its slope.
The `slopeMaxAngle` property is used to calculate how much the character can move vertically to follow it.
If the platform is too high, the platform is considered to be an obstacle and the character will stop before it.
When there is no obstacle detected by the horizontal search, the movement is done in 1 step and the vertical search is done at the new `x` position.
[![RequestedDeltaX](./diagrams/SlopeFollowingRequestedDeltaX.png)](./diagrams/SlopeFollowingRequestedDeltaX.svgz)
Otherwise, when there is a junction, 2 vertical searches are done:
- one before a potential obstacle (in pink)
- one at the end of the movement
[![RequestedDeltaX](./diagrams/SlopeFollowingClimbFactor.png)](./diagrams/SlopeFollowingClimbFactor.svgz)
This allows to calculate the right slope angle. Indeed, in one step, the angle could appear lower (the dotted line).
Which means that the character could climb it during 1 frame and then stop.
[![RequestedDeltaX](./diagrams/SlopeFollowingClimbFactorMean.png)](./diagrams/SlopeFollowingClimbFactorMean.svgz)
For further details on the implementation, please take a look at the comments in:
- the function `gdjs.PlatformerObjectRuntimeBehavior._moveX`
- the function `gdjs.PlatformerObjectRuntimeBehavior.OnFloor.beforeMovingY`
### Vertical search
The aim of the vertical search is to find the highest platform where the character can land.
There are 2 constraints:
- `allowedMinDeltaY` how much the character can go upward
- `allowedMaxDeltaY` how much the character can go downward
During the search, these 2 constraints can tighten around the character.
If they become incompatible, it means that the character can't go through the hole,
it will go back to its original position and lose its speed.
There are also more obvious obstacles that cover the character in the middle and end the search directly.
[![RequestedDeltaX](./diagrams/SlopeFollowingResult.png)](./diagrams/SlopeFollowingResult.svgz)
Obstacles can eventually encompass the character. So platforms edges don't have any collision with character.
To detect such cases, 2 flags are used:
- `foundOverHead` when an edge is over `headMaxY`
- `foundUnderHead` when an edge is under `floorMinY`
[![RequestedDeltaX](./diagrams/SlopeFollowingContext.png)](./diagrams/SlopeFollowingContext.svgz)
For further details on the implementation, please take a look at the comments in:
- the function `gdjs.PlatformerObjectRuntimeBehavior._findHighestFloorAndMoveOnTop`
- the class `gdjs.PlatformerObjectRuntimeBehavior.FollowConstraintContext`

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -105,6 +105,7 @@ namespace gdjs {
_oldY: float = 0;
_oldWidth: float = 0;
_oldHeight: float = 0;
_oldAngle: float = 0;
_manager: gdjs.PlatformObjectsManager;
_registeredInManager: boolean = false;
@@ -173,7 +174,8 @@ namespace gdjs {
this._oldX !== this.owner.getX() ||
this._oldY !== this.owner.getY() ||
this._oldWidth !== this.owner.getWidth() ||
this._oldHeight !== this.owner.getHeight()
this._oldHeight !== this.owner.getHeight() ||
this._oldAngle !== this.owner.getAngle()
) {
if (this._registeredInManager) {
this._manager.removePlatform(this);
@@ -183,6 +185,7 @@ namespace gdjs {
this._oldY = this.owner.getY();
this._oldWidth = this.owner.getWidth();
this._oldHeight = this.owner.getHeight();
this._oldAngle = this.owner.getAngle();
}
}

View File

@@ -0,0 +1,695 @@
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
[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 object 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.within(-31, -30); // -30 = -10 (platform y) + -20 (object height)
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
);
expect(object.getY()).to.be(-19); // -19 = -10 (platform y) + -9 (object height)
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.within(17.638, 17.639);
object.setCustomWidthAndHeight(object.getWidth(), 20);
runtimeScene.renderAndStep(1000 / 60);
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);
});
});
});

View File

@@ -0,0 +1,934 @@
describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
describe('(falling)', 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: 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);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
runtimeScene.renderAndStep(1000 / 60);
});
it('can fall when in the air', function () {
for (let i = 0; i < 30; ++i) {
runtimeScene.renderAndStep(1000 / 60);
if (i < 10) expect(object.getBehavior('auto1').isFalling()).to.be(true);
if (i < 10)
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
}
//Check the platform stopped the platformer object.
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);
for (let i = 0; i < 35; ++i) {
//Check that the platformer object can fall.
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getX()).to.be.within(87.5, 87.51);
expect(object.getY()).to.be(-24.75);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
for (let i = 0; i < 100; ++i) {
//Let the speed on X axis go back to 0.
runtimeScene.renderAndStep(1000 / 60);
}
});
it('falls when a platform is moved away', function () {
object.setPosition(0, -32);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be.within(-31, -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);
// move the platform away
platform.setPosition(-100, -100);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
it('falls when a platform is removed', function () {
object.setPosition(0, -32);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be.within(-31, -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);
// Remove the platform
runtimeScene.markObjectForDeletion(platform);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
});
describe('(jump and jump sustain)', function () {
let runtimeScene;
let object;
let platform;
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);
object.setPosition(0, -32);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
});
it('can jump', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump without sustaining
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 18; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Check that we reached the maximum height
expect(object.getY()).to.be.within(-180, -179);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be(-180);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-180, -179);
// Then let the object fall
for (let i = 0; i < 17; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getY()).to.be(-30);
});
it('can jump, sustaining the jump', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Forbid to jump
object.getBehavior('auto1').setCanNotAirJump();
// It has no impact as the object is on a platform.
expect(object.getBehavior('auto1').canJump()).to.be(true);
// Jump with sustaining as much as possible, and
// even more (18 frames at 60fps is greater than 0.2s)
for (let i = 0; i < 18; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check the height reached
expect(object.getY()).to.be(-230);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be(-235);
for (let i = 0; i < 5; ++i) {
// Verify that pressing the jump key does not change anything
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check that we reached the maximum height
expect(object.getY()).to.be(-247.5);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be(-247.5);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-247, -246);
// Then let the object fall
for (let i = 0; i < 60; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(-30);
});
it('can jump, and only sustain the jump while key held', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump with sustaining a bit (5 frames at 60fps = 0.08s), then stop
for (let i = 0; i < 5; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.within(-101, -100);
// Stop holding the jump key
runtimeScene.renderAndStep(1000 / 60);
for (let i = 0; i < 13; ++i) {
// then hold it again (but it's too late, jump sustain is gone for this jump)
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check that we reached the maximum height
expect(object.getY()).to.be.within(-206, -205);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-208, -207);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-208, -207);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-208, -207);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-206, -205);
runtimeScene.renderAndStep(1000 / 60);
// Then let the object fall
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
for (let i = 0; i < 60; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(-30);
});
it('should not jump after falling from a platform', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
// So at this point, the object could jump
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);
// Fall from the platform
for (let i = 0; i < 35; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Try to jump
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
it('can be allowed to jump in mid air', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Fall from the platform
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Allow to jump in mid air
expect(object.getBehavior('auto1').isFalling()).to.be(true);
object.getBehavior('auto1').setCanJump();
expect(object.getBehavior('auto1').canJump()).to.be(true);
// Can jump in the air
object.getBehavior('auto1').simulateJumpKey();
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
);
for (let i = 0; i < 40; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getBehavior('auto1').isJumping()).to.be(false);
// Can no longer to jump
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
it('can allow coyote time', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Fall from the platform
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Allow to jump
expect(object.getBehavior('auto1').isFalling()).to.be(true);
object.getBehavior('auto1').setCanJump();
expect(object.getBehavior('auto1').canJump()).to.be(true);
// Still falling from the platform
for (let i = 0; i < 4; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Suppose that we miss an eventual time frame or some condition.
// So we forbid to jump again:
object.getBehavior('auto1').setCanNotAirJump();
expect(object.getBehavior('auto1').canJump()).to.be(false);
// Can no longer to jump in mid air
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
it('should not grab a platform while in the ascending phase of a jump', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(12, -80);
runtimeScene.renderAndStep(1000 / 60);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump without sustaining
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 3; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
}
// the object is against the platform side
expect(object.getY()).to.be.within(
topPlatform.getY(),
topPlatform.getY() + object.getHeight()
);
// try to grab the platform
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Check that the object didn't grabbed the platform
expect(object.getX()).to.be.above(
topPlatform.getX() - object.getWidth() + 20
);
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(false);
});
it('can grab a platform while in the descending phase of a jump', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(12, -120);
runtimeScene.renderAndStep(1000 / 60);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump, reach the top and go down
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 30; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
}
// the object is against the platform side
expect(object.getY()).to.be.within(
topPlatform.getY() - object.getHeight(),
topPlatform.getY()
);
// Verify the object is in the falling state of the jump:
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
// try to grab the platform
for (let i = 0; i < 30; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check that the object grabbed the platform
expect(object.getY()).to.be(topPlatform.getY());
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(true);
});
it('should not grab a platform while walking', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(20, platform.getY() - object.getHeight());
runtimeScene.renderAndStep(1000 / 60);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// try to grab the platform
for (let i = 0; i < 30; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
}
// The object is where it could grab the top platform if it was 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);
});
});
describe('(jumpthru)', function () {
let runtimeScene;
let object;
let platform;
let jumpthru;
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: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -30);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
// Put a jump thru, higher than the platform so that the object jump from under it
// and will land on it at the end of the jump.
jumpthru = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj2',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
platformType: 'Jumpthru',
},
],
effects: [],
});
jumpthru.setCustomWidthAndHeight(60, 5);
runtimeScene.addObject(jumpthru);
});
it('can jump through a jumpthru and land', function () {
jumpthru.setPosition(0, -33);
//Check the platform stopped the platformer object.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
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);
// Check that the jump starts properly, and is not stopped on the jumpthru
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-39, -38);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-47, -46);
runtimeScene.renderAndStep(1000 / 60);
// At this step, the object is almost on the jumpthru (-53 + 20 (object height) = -33 (jump thru Y position)),
// but the object should not stop.
expect(object.getY()).to.be.within(-54, -53);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-61, -60);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-67, -66);
// Verify the object is still jumping
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
// Continue the simulation and check that position is correct in the middle of the jump
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.within(-89, -88);
// Verify the object is now considered as falling in its jump:
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
// Continue simulation and check that we arrive on the jumpthru
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.within(
jumpthru.getY() - object.getHeight(),
jumpthru.getY() - object.getHeight() + 1
);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
});
it('can jump right under a jumpthru without landing', function () {
// A big one because the object jump to the right.
jumpthru.setCustomWidthAndHeight(600, 20);
const highestJumpY = -104; // actually -103.6
// Right above the maximum reach by jumping
jumpthru.setPosition(0, highestJumpY + object.getHeight());
// The object landed on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
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);
// The object jumps.
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 17; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
}
// The object is at the highest of the jump.
expect(object.getY()).to.be.within(highestJumpY, highestJumpY + 1);
// The object starts to fall.
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
// The object still falls.
for (let i = 0; i < 10; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
}
expect(object.getY()).to.be.above(-85);
});
it('can jump right above a jumpthru and landing', function () {
// A big one because the object jump to the right.
jumpthru.setCustomWidthAndHeight(600, 20);
const highestJumpY = -104; // actually -103.6
// Right above the maximum reach by jumping
jumpthru.setPosition(0, highestJumpY + 1 + object.getHeight());
// The object landed on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
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);
// The object jumps.
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 17; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
}
// The object is at the highest of the jump.
expect(object.getY()).to.be.within(highestJumpY, highestJumpY + 1);
// The object landed on the jumpthru.
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getY()).to.be(jumpthru.getY() - object.getHeight());
});
it('can fall through the jumpthru from the left side', function () {
jumpthru.setPosition(0, -33);
object.setPosition(0, -100);
jumpthru.setPosition(12, -90);
jumpthru.setCustomWidthAndHeight(60, 100);
// Check the jumpthru let the platformer object go through.
for (let i = 0; i < 10; ++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
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
}
// Overlapping the jumpthru
expect(object.getX()).to.above(5);
expect(object.getY()).to.be.within(-100, -80);
});
});
describe('and gdjs.PlatformRuntimeBehavior at same time', function () {
let runtimeScene;
let object;
let platform;
var object2;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform.
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'PlatformerObject',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -30);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
// Put a platformer object that is also a platform itself.
object2 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj2',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'PlatformerObject',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
},
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
platformType: 'Platform',
},
],
effects: [],
});
object2.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object2);
// Position it above the other platformer object and just on its right,
// but one pixel too much so that the first platformer object will be moved
// left by 1px when the second platformer object+platform falls.
object2.setPosition(9, -60);
});
it('can jump through the jumpthru', function () {
// Check that the second object falls (it's not stopped by itself)
expect(object2.getY()).to.be(-60);
runtimeScene.renderAndStep(1000 / 60);
expect(object2.getY()).to.be(-59.75);
runtimeScene.renderAndStep(1000 / 60);
expect(object2.getY()).to.be(-59.25);
runtimeScene.renderAndStep(1000 / 60);
expect(object2.getY()).to.be(-58.5);
runtimeScene.renderAndStep(1000 / 60);
expect(object2.getY()).to.be(-57.5);
//Check the first object stays on the platform.
expect(object.getY()).to.be(-30);
// Simulate more frames. Check that trying to jump won't do anything.
for (let i = 0; i < 5; ++i) {
object2.getBehavior('PlatformerObject').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getY()).to.be(-48.75);
expect(object.getX()).to.be(0);
expect(object.getY()).to.be(-30);
// Verify that the first platformer object is moved 1px to the left
// as the falling platformer object+platform collides with it
runtimeScene.renderAndStep(1000 / 60);
expect(object2.getY()).to.be(-46.25);
expect(object.getX()).to.be(-1);
expect(object.getY()).to.be(-30);
// Simulate more frames so that the object reaches the floor
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be(9);
expect(object2.getY()).to.be(-30);
expect(object.getX()).to.be(-1);
expect(object.getY()).to.be(-30);
// Start a jump for both objects
object.getBehavior('PlatformerObject').simulateJumpKey();
object2.getBehavior('PlatformerObject').simulateJumpKey();
for (let i = 0; i < 6; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be(9);
expect(object2.getY()).to.be(-72.5);
expect(object.getX()).to.be(-1);
expect(object.getY()).to.be(-72.5);
// Try to go right for the first object: won't work because the other
// object is a platform.
for (let i = 0; i < 5; ++i) {
object.getBehavior('PlatformerObject').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be(9);
expect(object2.getY()).to.be.within(-94.2, -94.1);
expect(object.getX()).to.be(-1);
expect(object.getY()).to.be.within(-94.2, -94.1);
// Try to go right for the first and second object: can do.
for (let i = 0; i < 3; ++i) {
object.getBehavior('PlatformerObject').simulateRightKey();
object2.getBehavior('PlatformerObject').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be.within(9.83, 9.84);
expect(object2.getY()).to.be.within(-101.2, -101.1);
expect(object.getX()).to.be.within(-0.59, -0.58);
expect(object.getY()).to.be.within(-101.2, -101.1);
// Let the object fall back on the floor.
for (let i = 0; i < 30; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be.within(9.83, 9.84);
expect(object2.getY()).to.be(-30);
expect(object.getX()).to.be.within(-0.59, -0.58);
expect(object.getY()).to.be(-30);
});
});
});

View File

@@ -0,0 +1,552 @@
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 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.getX()).to.be.within(
platform.getX() + platform.getWidth() + 0,
platform.getX() + platform.getWidth() + 1
);
expect(object.getY()).to.be(platform.getY());
object.getBehavior('auto1').simulateReleasePlatformKey();
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
);
}
//Check that the object is falling
expect(object.getY()).to.be(3.75);
});
[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 ~ 23px
objectPositionAfterFirstClimb + 22,
objectPositionAfterFirstClimb + 24
);
climbLadder(24);
// 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 < 19; ++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
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
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);
}
fall(5);
});
it('can walk from a ladder', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Climb the ladder
object.getBehavior('auto1').simulateLadderKey();
stayOnLadder(10);
// Going from the ladder to the right
runtimeScene.renderAndStep(1000 / 60);
for (let i = 0; i < 16; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
}
// and directly on the floor
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
});
it('can jump from a ladder', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Climb the ladder
object.getBehavior('auto1').simulateLadderKey();
climbLadder(10);
stayOnLadder(10);
// Jump from the ladder
const stayY = object.getY();
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.below(stayY);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
});
it('can grab a ladder when falling', function () {
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
// Climb the ladder
object.getBehavior('auto1').simulateLadderKey();
climbLadder(24);
// Check that we reached the maximum height
// The player goes a little over the ladder...
object.getBehavior('auto1').simulateUpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
fall(10);
object.getBehavior('auto1').simulateLadderKey();
stayOnLadder(10);
climbLadder(5);
});
it('should not grab a platform when grabbed to a ladder', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(ladder.getX() + ladder.getWidth(), -50);
runtimeScene.renderAndStep(1000 / 60);
object.setPosition(
topPlatform.getX() - object.getWidth(),
topPlatform.getY()
);
// Grab the ladder
object.getBehavior('auto1').simulateLadderKey();
stayOnLadder(10);
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
// The object is where it could grab the top platform if it where falling.
expect(object.getX()).to.be.within(
topPlatform.getX() - object.getWidth(),
topPlatform.getX() - object.getWidth() + 2
);
expect(object.getY()).to.be(topPlatform.getY());
// Check that the object didn't grabbed the platform
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(false);
stayOnLadder(10);
});
it('can grab a ladder when grabbed to a platform', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(ladder.getX() + ladder.getWidth(), -50);
runtimeScene.renderAndStep(1000 / 60);
// Fall and Grab the platform
object.setPosition(
topPlatform.getX() - object.getWidth(),
topPlatform.getY() - 10
);
for (let i = 0; i < 6; ++i) {
object.getBehavior('auto1').simulateRightKey();
fall(1);
}
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(true);
// try to grab the ladder
object.getBehavior('auto1').simulateLadderKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnLadder()).to.be(true);
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(false);
});
});
});

View File

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

View File

@@ -0,0 +1,229 @@
const makePlatformerTestRuntimeScene = () => {
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 (1 / 60) * 1000;
};
return runtimeScene;
};
const addPlatformObject = (runtimeScene) => {
const platform = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj2',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
});
platform.setCustomWidthAndHeight(60, 32);
runtimeScene.addObject(platform);
return platform;
};
const addUpSlopePlatformObject = (runtimeScene) => {
const platform = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'slope',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 0, y: 0 },
centerPoint: { x: 50, y: 50 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 50, y: 50 },
],
hasCustomCollisionMask: true,
customCollisionMask: [
[
{ x: 100, y: 100 },
{ x: 0, y: 100 },
{ x: 100, y: 0 },
],
],
},
],
},
],
},
],
});
runtimeScene.addObject(platform);
platform.setUnscaledWidthAndHeight(100, 100);
platform.setCustomWidthAndHeight(100, 100);
return platform;
};
const addDownSlopePlatformObject = (runtimeScene) => {
const platform = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'slope',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 0, y: 0 },
centerPoint: { x: 50, y: 50 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 50, y: 50 },
],
hasCustomCollisionMask: true,
customCollisionMask: [
[
{ x: 100, y: 100 },
{ x: 0, y: 100 },
{ x: 0, y: 0 },
],
],
},
],
},
],
},
],
});
runtimeScene.addObject(platform);
platform.setUnscaledWidthAndHeight(100, 100);
platform.setCustomWidthAndHeight(100, 100);
return platform;
};
const addTunnelPlatformObject = (runtimeScene) => {
const platform = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'slope',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
},
],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 0, y: 0 },
centerPoint: { x: 50, y: 50 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 50, y: 50 },
],
hasCustomCollisionMask: true,
customCollisionMask: [
[
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
{ x: 100, y: 0 },
],
[
{ x: 0, y: 200 },
{ x: 0, y: 300 },
{ x: 100, y: 300 },
{ x: 100, y: 200 },
],
],
},
],
},
],
},
],
});
runtimeScene.addObject(platform);
platform.setUnscaledWidthAndHeight(100, 300);
platform.setCustomWidthAndHeight(100, 300);
return platform;
};
const addJumpThroughPlatformObject = (runtimeScene) => {
const platform = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj2',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
platformType: 'Jumpthru',
canBeGrabbed: false,
},
],
effects: [],
});
platform.setCustomWidthAndHeight(60, 32);
runtimeScene.addObject(platform);
return platform;
};
const addLadderObject = (runtimeScene) => {
const ladder = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj3',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: false,
platformType: 'Ladder',
},
],
effects: [],
});
ladder.setCustomWidthAndHeight(20, 60);
runtimeScene.addObject(ladder);
return ladder;
};

View File

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

View File

@@ -0,0 +1,604 @@
describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, function () {
const makeTestRuntimeScene = () => {
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 (1 / 60) * 1000;
};
return runtimeScene;
};
const addCharacter = (runtimeScene) => {
const character = 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: [],
});
character.setCustomWidthAndHeight(100, 100);
runtimeScene.addObject(character);
return character;
};
const addPlatform = (runtimeScene, collisionMask) => {
const platform = new gdjs.TestSpriteRuntimeObject(runtimeScene, {
name: 'platform',
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: collisionMask,
},
],
},
],
},
],
});
runtimeScene.addObject(platform);
platform.setUnscaledWidthAndHeight(100, 100);
platform.setCustomWidthAndHeight(100, 100);
return platform;
};
const checkMoveOn = (
characterBehavior,
platformBehavior,
upwardDeltaY,
downwardDeltaY
) => {
const result = characterBehavior._findHighestFloorAndMoveOnTop(
[platformBehavior],
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(platformBehavior);
};
const checkNoFloor = (
characterBehavior,
platformBehavior,
upwardDeltaY,
downwardDeltaY
) => {
const oldY = characterBehavior.owner.getY();
const result = characterBehavior._findHighestFloorAndMoveOnTop(
[platformBehavior],
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(null);
expect(result.isCollidingAnyPlatform).to.be(false);
expect(characterBehavior.owner.getY()).to.be(oldY);
};
const checkObstacle = (
characterBehavior,
platformBehavior,
upwardDeltaY,
downwardDeltaY
) => {
const oldY = characterBehavior.owner.getY();
const result = characterBehavior._findHighestFloorAndMoveOnTop(
[platformBehavior],
upwardDeltaY,
downwardDeltaY
);
expect(result.highestGround).to.be(null);
expect(result.isCollidingAnyPlatform).to.be(true);
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 = {
square: [
[
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
{ x: 0, y: 100 },
],
],
bottomLeftTriangle: [
[
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
],
],
bottomRightTriangle: [
[
{ x: 100, y: 0 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
],
],
topRightTriangle: [
[
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
],
],
topLeftTriangle: [
[
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 0, y: 100 },
],
],
topLeftTriangleWithLowEdge: [
[
{ x: -1, y: 100 },
{ x: -1, y: 0 },
{ x: 100, y: 0 },
{ x: 0, y: 100 },
],
],
bottomTriangle: [
[
{ x: 50, y: 0 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
],
],
topTriangle: [
[
{ x: 50, y: 100 },
{ x: 0, y: 0 },
{ x: 100, y: 0 },
],
],
leftTriangle: [
[
{ x: 100, y: 50 },
{ x: 0, y: 0 },
{ x: 0, y: 100 },
],
],
rightTriangle: [
[
{ x: 0, y: 50 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
],
],
horizontalTunnel: [
[
{ x: 0, y: 0 },
{ x: 0, y: 25 },
{ x: 100, y: 25 },
{ x: 100, y: 0 },
],
[
{ x: 0, y: 75 },
{ x: 0, y: 100 },
{ x: 100, y: 100 },
{ x: 100, y: 75 },
],
],
verticalTunnel: [
[
{ x: 25, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 25, y: 100 },
],
[
{ x: 75, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
{ x: 75, y: 100 },
],
],
};
if (swapVerticesOrder) {
for (const key in collisionMasks) {
if (Object.hasOwnProperty.call(collisionMasks, key)) {
collisionMasks[key] = collisionMasks[key].reverse();
}
}
}
[
{
description: '(An edge crossing from the left to the right)',
mask: collisionMasks.square,
position: [200, -100],
},
{
description: '(A vertex inside and edges crossing at the bottom)',
mask: collisionMasks.bottomTriangle,
position: [200, -100],
},
{
description: '(An edge crossing from the left to the bottom)',
mask: collisionMasks.bottomLeftTriangle,
position: [200, -200],
},
{
description:
'(An edge crossing from the left to the bottom with the vertex right on the left border)',
mask: collisionMasks.bottomLeftTriangle,
position: [300, -100],
},
{
description: '(An edge crossing from the right to the bottom)',
mask: collisionMasks.bottomRightTriangle,
position: [200, -200],
},
{
description:
'(An edge crossing from the right to the bottom with the vertex right on the right border)',
mask: collisionMasks.bottomRightTriangle,
position: [100, -100],
},
{
description: '(A vertex inside and edges crossing at the left)',
mask: collisionMasks.leftTriangle,
position: [1, -249.5],
},
{
description: '(A vertex inside and edges crossing at the right)',
mask: collisionMasks.rightTriangle,
position: [399, -249.5],
},
].forEach(({ description, mask, position }) => {
describe(description, function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(runtimeScene, mask);
platform.setCustomWidthAndHeight(300, 300);
platform.setPosition(position[0], position[1]);
const platformBehavior = platform.getBehavior('Platform');
it('can detect a platform away downward', function () {
character.setPosition(300, -210.1);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can detect a floor to follow down', function () {
character.setPosition(300, -210);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect a floor when right on it', function () {
character.setPosition(300, -200);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect a floor to follow up', function () {
character.setPosition(300, -190);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect an obstacle a bit too high to follow', function () {
character.setPosition(300, -189.9);
checkObstacle(behavior, platformBehavior, -10, 10);
});
});
});
[
{
description: '(An edge crossing from the left to the right)',
mask: collisionMasks.square,
position: [200, -100],
},
{
description: '(A vertex inside and edges crossing at the top)',
mask: collisionMasks.topTriangle,
position: [200, -100],
},
{
description: '(An edge crossing from the left to the top)',
mask: collisionMasks.topRightTriangle,
position: [200, 0],
},
{
description: '(An edge crossing from the right to the top)',
mask: collisionMasks.topLeftTriangle,
position: [200, 0],
},
{
description: '(An edge crossing from the right to the top)',
// An edge will be lower than the character (but not under).
mask: collisionMasks.topLeftTriangleWithLowEdge,
position: [180, 20],
},
].forEach(({ description, mask, position }) => {
describe(description, function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(runtimeScene, mask);
platform.setCustomWidthAndHeight(300, 300);
platform.setPosition(position[0], position[1]);
const platformBehavior = platform.getBehavior('Platform');
it('can detect an obstacle overlapping the top', function () {
// -10 because the character can follow a platform downward.
character.setPosition(300, 199.9 - 10);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect a platform away downward', function () {
character.setPosition(300, 200 - 10);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
});
});
describe('(A platform with an hitbox under and another one above)', function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(
runtimeScene,
collisionMasks.horizontalTunnel
);
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(250, -250);
const platformBehavior = platform.getBehavior('Platform');
it('can detect a tunnel ceiling', function () {
character.setPosition(300, -210.1);
checkObstacle(behavior, platformBehavior, -10, 10);
});
// The character won't collide the ceiling anymore when he follows the floor.
// So, the ceiling should not be seen as an obstacle.
// This can happen if a tunnel is going down
it('can detect a floor to follow down', function () {
character.setPosition(300, -210);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect a floor when right on it', function () {
character.setPosition(300, -200);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect a floor to follow up', function () {
character.setPosition(300, -190);
checkMoveOn(behavior, platformBehavior, -10, 10);
expect(character.getY()).to.be(-200);
});
it('can detect an obstacle a bit too high to follow', function () {
character.setPosition(300, -189.9);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect a too thin horizontal tunnel', function () {
platform.setCustomWidthAndHeight(200, 199.8);
platform.setPosition(250, -250.1);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
});
describe('(A platform with an hitbox on the left and another one on the right)', function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(
runtimeScene,
collisionMasks.verticalTunnel
);
const platformBehavior = platform.getBehavior('Platform');
it('can fell inside a vertical tunnel that fit the character', function () {
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(250, -250);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can fell inside a vertical tunnel', function () {
platform.setCustomWidthAndHeight(200.2, 200);
platform.setPosition(249.9, -250);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can detect a too thin vertical tunnel', function () {
platform.setCustomWidthAndHeight(199.8, 200);
platform.setPosition(250.1, -250);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
});
describe('(A platform sharing a vertex with the character})', function () {
const runtimeScene = makeTestRuntimeScene();
const character = addCharacter(runtimeScene);
const behavior = character.getBehavior('auto1');
const platform = addPlatform(runtimeScene, collisionMasks.square);
const platformBehavior = platform.getBehavior('Platform');
it('can detect a platform at its exact position', function () {
platform.setCustomWidthAndHeight(100, 100);
platform.setPosition(300, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassing platform sharing the top left corner', function () {
// Shared vertex at (300, -200)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(300, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassed platform sharing the top left corner', function () {
// Shared vertex at (300, -200)
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(300, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassing platform sharing the top right corner', function () {
// Shared vertex at (400, -200)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(200, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassed platform sharing the top right corner', function () {
// Shared vertex at (400, -200)
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(350, -200);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassing platform sharing the bottom left corner', function () {
// Shared vertex at (300, -100)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(300, -300);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassed platform sharing the bottom left corner', function () {
// Shared vertex at (300, -100)
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(300, -150);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassing platform sharing the bottom right corner', function () {
// Shared vertex at (400, -100)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(200, -300);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can detect an encompassed platform sharing the bottom right corner', function () {
// Shared vertex at (400, -100)
platform.setCustomWidthAndHeight(50, 50);
platform.setPosition(350, -150);
character.setPosition(300, -200);
checkObstacle(behavior, platformBehavior, -10, 10);
});
it('can be next to a platform sharing the top left corner', function () {
// Shared vertex at (300, -200)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(100, -200);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can be next to a platform sharing the top right corner', function () {
// Shared vertex at (400, -200)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(400, -200);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can be next to a platform sharing the bottom left corner', function () {
// Shared vertex at (300, -100)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(100, -300);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
it('can be next to a platform sharing the bottom right corner', function () {
// Shared vertex at (400, -100)
platform.setCustomWidthAndHeight(200, 200);
platform.setPosition(400, -300);
character.setPosition(300, -200);
checkNoFloor(behavior, platformBehavior, -10, 10);
});
});
});
});
});

View File

@@ -1,5 +1,7 @@
namespace gdjs {
export namespace screenshot {
const logger = new gdjs.Logger('Screenshot');
/**
* Save a screenshot of the game.
* @param runtimeScene The scene
@@ -22,13 +24,13 @@ namespace gdjs {
}
fileSystem.writeFile(savePath, content, 'base64', (err) => {
if (err) {
console.error(
logger.error(
'Unable to save the screenshot at path: ' + savePath
);
}
});
} else {
console.error(
logger.error(
'Screenshot are not supported on rendering engines without canvas.'
);
}

View File

@@ -1,4 +1,6 @@
namespace gdjs {
const logger = new gdjs.Logger('Tilemap object');
/**
* The PIXI.js renderer for the Tile map runtime object.
*
@@ -81,7 +83,7 @@ namespace gdjs {
.getJsonManager()
.loadJson(this._object._tilemapJsonFile, (error, tileMapJsonData) => {
if (error) {
console.error(
logger.error(
'An error happened while loading a Tilemap JSON data:',
error
);
@@ -95,7 +97,7 @@ namespace gdjs {
this._object._tilesetJsonFile,
(error, tilesetJsonData) => {
if (error) {
console.error(
logger.error(
'An error happened while loading Tileset JSON data:',
error
);

View File

@@ -76,11 +76,19 @@ namespace gdjs {
}
updateXOffset(): void {
this._tiledSprite.tilePosition.x = -this._object._xOffset;
// Known PIXI.js issue, the coordinates should not exceed the width/height of the texture,
// otherwise the texture will be pixelated over time.
// See https://github.com/pixijs/pixijs/issues/7891#issuecomment-947549553
this._tiledSprite.tilePosition.x =
-this._object._xOffset % this._tiledSprite.texture.width;
}
updateYOffset(): void {
this._tiledSprite.tilePosition.y = -this._object._yOffset;
// Known PIXI.js issue, the coordinates should not exceed the width/height of the texture,
// otherwise the texture will be pixelated over time.
// See https://github.com/pixijs/pixijs/issues/7891#issuecomment-947549553
this._tiledSprite.tilePosition.y =
-this._object._yOffset % this._tiledSprite.texture.height;
}
setColor(rgbColor): void {

View File

@@ -1,22 +0,0 @@
Copyright (c) 2013 Jeremy Kahn
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

571
Extensions/TweenBehavior/shifty.d.ts vendored Normal file
View File

@@ -0,0 +1,571 @@
// Shifty.js 2.16.0 type definitions by arthuro555
declare namespace shifty {
// index.js
type easingFunction = (position: number) => number;
type startFunction = (state: any, data?: any) => any;
type finishFunction = (promisedData: shifty.promisedData) => any;
/**
* Gets called for every tick of the tween. This function is not called on the
* final tick of the animation.
*/
type renderFunction = (
state: any,
data: any | undefined,
timeElapsed: number
) => any;
type scheduleFunction = (callback: Function, timeout: number) => any;
interface tweenConfig {
/**
* Starting position. If omitted, {@link * shifty.Tweenable#get} is used.
*/
from?: any;
/**
* Ending position. The keys of this Object should
* match those of `to`.
*/
to?: any;
/**
* How many milliseconds to animate for.
*/
duration?: number;
/**
* How many milliseconds to wait before starting the
* tween.
*/
delay?: number;
/**
* Executes when the tween begins.
*/
start?: shifty.startFunction;
/**
* Executes when the tween
* completes. This will get overridden by {@link shifty.Tweenablethen } if that
* is called, and it will not fire if {@link shifty.Tweenablecancel } is
* called.
*/
finish?: shifty.finishFunction;
/**
* Executes on every tick. Shifty
* assumes a [retained mode](https://en.wikipedia.org/wiki/Retained_mode)
* rendering environment, which in practice means that `render` only gets
* called when the tween state changes. Importantly, this means that `render`
* is _not_ called when a tween is not animating (for instance, when it is
* paused or waiting to start via the `delay` option). This works naturally
* with DOM environments, but you may need to account for this design in more
* custom environments such as `<canvas>`.
*
* Legacy property name: `step`.
*/
render?: shifty.renderFunction;
/**
* Easing curve name(s) or {@link shifty.easingFunction }(s) to apply
* to the properties of the tween. If this is an Object, the keys should
* correspond to `to`/`from`. You can learn more about this in the {@tutorial
* easing-function-in-depth} tutorial.
*/
easing?: Record<string, easingFunction> | string | easingFunction;
/**
* Data that is passed to {@link * shifty.startFunction}, {@link shifty.renderFunction }, and {@link * shifty.promisedData}. Legacy property name: `attachment`.
*/
data?: any;
/**
* Promise constructor for when you want
* to use Promise library or polyfill Promises in unsupported environments.
*/
promise?: Function;
}
type promisedData = {
/**
* The current state of the tween.
*/
state: any;
/**
* The `data` Object that the tween was configured with.
*/
data: any;
/**
* The {@link shifty.Tweenable } instance to
* which the tween belonged.
*/
tweenable: Tweenable;
};
/**
* Is called when a tween is created to determine if a filter is needed.
* Filters are only added to a tween when it is created so that they are not
* unnecessarily processed if they don't apply during an update tick.
*/
type doesApplyFilter = (tweenable: any) => boolean;
/**
* Is called when a tween is created. This should perform any setup needed by
* subsequent per-tick calls to {@link shifty.beforeTween } and {@link * shifty.afterTween}.
*/
type tweenCreatedFilter = (tweenable: any) => any;
/**
* Is called right before a tween is processed in a tick.
*/
type beforeTweenFilter = (tweenable: any) => any;
/**
* Is called right after a tween is processed in a tick.
*/
type afterTweenFilter = (tweenable: any) => any;
/**
* An Object that contains functions that are called at key points in a tween's
* lifecycle. Shifty can only process `Number`s internally, but filters can
* expand support for any type of data. This is the mechanism that powers
* [string interpolation]{@tutorial string-interpolation}.
*/
type filter = {
/**
* Is called when a tween is
* created.
*/
doesApply: shifty.doesApplyFilter;
/**
* Is called when a tween is
* created.
*/
tweenCreated: shifty.tweenCreatedFilter;
/**
* Is called right before a
* tween starts.
*/
beforeTween: shifty.beforeTweenFilter;
/**
* Is called right after a tween
* ends.
*/
afterTween: shifty.afterTweenFilter;
};
// easing-functions.js
export function linear(pos: number): number;
export function easeInQuad(pos: any): number;
export function easeOutQuad(pos: any): number;
export function easeInOutQuad(pos: any): number;
export function easeInCubic(pos: any): number;
export function easeOutCubic(pos: any): number;
export function easeInOutCubic(pos: any): number;
export function easeInQuart(pos: any): number;
export function easeOutQuart(pos: any): number;
export function easeInOutQuart(pos: any): number;
export function easeInQuint(pos: any): number;
export function easeOutQuint(pos: any): number;
export function easeInOutQuint(pos: any): number;
export function easeInSine(pos: any): number;
export function easeOutSine(pos: any): number;
export function easeInOutSine(pos: any): number;
export function easeInExpo(pos: any): number;
export function easeOutExpo(pos: any): number;
export function easeInOutExpo(pos: any): number;
export function easeInCirc(pos: any): number;
export function easeOutCirc(pos: any): number;
export function easeInOutCirc(pos: any): number;
export function easeOutBounce(pos: any): number;
export function easeInBack(pos: any): number;
export function easeOutBack(pos: any): number;
export function easeInOutBack(pos: any): number;
export function elastic(pos: any): number;
export function swingFromTo(pos: any): number;
export function swingFrom(pos: any): number;
export function swingTo(pos: any): number;
export function bounce(pos: any): number;
export function bouncePast(pos: any): number;
export function easeFromTo(pos: any): number;
export function easeFrom(pos: any): number;
export function easeTo(pos: any): number;
// bezier.js
export function setBezierFunction(
name: string,
x1: number,
y1: number,
x2: number,
y2: number
): any;
export function unsetBezierFunction(name: string): boolean;
// interpolate.js
export function interpolate<T extends Object>(
from: T,
to: T,
position: number,
easing: Record<string, easingFunction> | string | easingFunction,
delay?: number
): T;
// scene.js
export class Scene {
/**
* The {@link shifty.Scene} class provides a way to control groups of {@link
* shifty.Tweenable}s. It is lightweight, minimalistic, and meant to provide
* performant {@link shifty.Tweenable} batch control that users of Shifty
* might otherwise have to implement themselves. It is **not** a robust
* timeline solution, and it does **not** provide utilities for sophisticated
* animation sequencing or orchestration. If that is what you need for your
* project, consider using a more robust tool such as
* [Rekapi](http://jeremyckahn.github.io/rekapi/doc/) (a timeline layer built
* on top of Shifty).
*
* Please be aware that {@link shifty.Scene} does **not** perform any
* automatic cleanup. If you want to remove a {@link shifty.Tweenable} from a
* {@link shifty.Scene}, you must do so explicitly with either {@link
* shifty.Scene#remove} or {@link shifty.Scene#empty}.
*
* <p class="codepen" data-height="677" data-theme-id="0" data-default-tab="js,result" data-user="jeremyckahn" data-slug-hash="qvZKbe" style="height: 677px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="Shifty Scene Demo">
* <span>See the Pen <a href="https://codepen.io/jeremyckahn/pen/qvZKbe/">
* Shifty Scene Demo</a> by Jeremy Kahn (<a href="https://codepen.io/jeremyckahn">@jeremyckahn</a>)
* on <a href="https://codepen.io">CodePen</a>.</span>
* </p>
* <script async src="https://static.codepen.io/assets/embed/ei.js"></script>
* @param {...shifty.Tweenable} tweenables
* @see https://codepen.io/jeremyckahn/pen/qvZKbe
* @constructs shifty.Scene
*/
constructor(...tweenables: Tweenable[]);
/**
* A copy of the internal {@link shifty.Tweenable}s array.
* @member shifty.Scene#tweenables
* @type {Array.<shifty.Tweenable>}
* @readonly
*/
get tweenables(): Tweenable[];
/**
* The {@link external:Promise}s for all {@link shifty.Tweenable}s in this
* {@link shifty.Scene} that have been configured with {@link
* shifty.Tweenable#setConfig}. Note that each call of {@link
* shifty.Scene#play} or {@link shifty.Scene#pause} creates new {@link
* external:Promise}s:
*
* const scene = new Scene(new Tweenable());
* scene.play();
*
* Promise.all(scene.promises).then(() =>
* // Plays the scene again upon completion, but a new promise is
* // created so this line only runs once.
* scene.play()
* );
*
* @member shifty.Scene#promises
* @type {Array.<external:Promise>}
* @readonly
*/
get promises(): Promise<Object>[];
/**
* Add a {@link shifty.Tweenable} to be controlled by this {@link
* shifty.Scene}.
* @method shifty.Scene#add
* @param {shifty.Tweenable} tweenable
* @return {shifty.Tweenable} The {@link shifty.Tweenable} that was added.
*/
add(tweenable: Tweenable): Tweenable;
/**
* Remove a {@link shifty.Tweenable} that is controlled by this {@link
* shifty.Scene}.
* @method shifty.Scene#remove
* @param {shifty.Tweenable} tweenable
* @return {shifty.Tweenable} The {@link shifty.Tweenable} that was removed.
*/
remove(tweenable: Tweenable): Tweenable;
/**
* [Remove]{@link shifty.Scene#remove} all {@link shifty.Tweenable}s in this {@link
* shifty.Scene}.
* @method shifty.Scene#empty
* @return {Array.<shifty.Tweenable>} The {@link shifty.Tweenable}s that were
* removed.
*/
empty(): Array<Tweenable>;
/**
* Is `true` if any {@link shifty.Tweenable} in this {@link shifty.Scene} is
* playing.
* @method shifty.Scene#isPlaying
* @return {boolean}
*/
isPlaying(): boolean;
/**
* Play all {@link shifty.Tweenable}s from their beginning.
* @method shifty.Scene#play
* @return {shifty.Scene}
*/
play(): Scene;
/**
* {@link shifty.Tweenable#pause} all {@link shifty.Tweenable}s in this
* {@link shifty.Scene}.
* @method shifty.Scene#pause
* @return {shifty.Scene}
*/
pause(): Scene;
/**
* {@link shifty.Tweenable#resume} all paused {@link shifty.Tweenable}s.
* @method shifty.Scene#resume
* @return {shifty.Scene}
*/
resume(): Scene;
/**
* {@link shifty.Tweenable#stop} all {@link shifty.Tweenable}s in this {@link
* shifty.Scene}.
* @method shifty.Scene#stop
* @param {boolean} [gotoEnd]
* @return {shifty.Scene}
*/
stop(gotoEnd?: boolean): Scene;
}
// tweenable.js
/**
* @method shifty.tween
* @param {shifty.tweenConfig} [config={}]
* @description Standalone convenience method that functions identically to
* {@link shifty.Tweenable#tween}. You can use this to create tweens without
* needing to set up a {@link shifty.Tweenable} instance.
*
* ```
* import { tween } from 'shifty';
*
* tween({ from: { x: 0 }, to: { x: 10 } }).then(
* () => console.log('All done!')
* );
* ```
*
* @returns {shifty.Tweenable} A new {@link shifty.Tweenable} instance.
*/
export function tween(config?: tweenConfig): Tweenable;
export function tweenProps(
forPosition: number,
currentState: any,
originalState: any,
targetState: any,
duration: number,
timestamp: number,
easing: Record<any, string | Function>
): Object;
export function processTweens(): void;
export function scheduleUpdate(): void;
export function composeEasingObject(
fromTweenParams: any,
easing?: any | string | Function,
composedEasing?: any
): any | Function;
export class Tweenable {
/**
* @method shifty.Tweenable.now
* @static
* @returns {number} The current timestamp.
*/
static now: () => number;
/**
* @param {Object} [initialState={}] The values that the initial tween should
* start at if a `from` value is not provided to {@link
* shifty.Tweenable#tween} or {@link shifty.Tweenable#setConfig}.
* @param {shifty.tweenConfig} [config] Configuration object to be passed to
* {@link shifty.Tweenable#setConfig}.
* @constructs shifty.Tweenable
*/
constructor(initialState?: Object, config?: tweenConfig);
private _config: tweenConfig;
private _data: Object;
private _delay: number;
private _filters: filter[];
private _next: any;
private _previous: any;
private _timestamp: number;
private _resolve: any;
private _reject: (reason?: any) => void;
private _currentState: any;
private _originalState: Object;
private _targetState: Object;
private _start: () => void;
private _render: () => void;
private _promiseCtor: PromiseConstructor;
/**
* Applies a filter to Tweenable instance.
* @param {string} filterName The name of the filter to apply.
* @private
*/
private _applyFilter;
private _isPlaying: boolean;
private _pausedAtTime: number;
private _duration: any;
private _scheduleId: any;
private _easing: any;
/**
* Configure and start a tween. If this {@link shifty.Tweenable}'s instance
* is already running, then it will stop playing the old tween and
* immediately play the new one.
* @method shifty.Tweenable#tween
* @param {shifty.tweenConfig} [config] Gets passed to {@link
* shifty.Tweenable#setConfig}.
* @return {shifty.Tweenable}
*/
tween(config?: tweenConfig): this;
/**
* Configure a tween that will start at some point in the future. Aside from
* `delay`, `from`, and `to`, each configuration option will automatically
* default to the same option used in the preceding tween of this {@link
* shifty.Tweenable} instance.
* @method shifty.Tweenable#setConfig
* @param {shifty.tweenConfig} [config={}]
* @return {shifty.Tweenable}
*/
setConfig(config?: tweenConfig): this;
/**
* Overrides any `finish` function passed via a {@link shifty.tweenConfig}.
* @method shifty.Tweenable#then
* @param {function} onFulfilled Receives {@link shifty.promisedData} as the
* first parameter.
* @param {function} onRejected Receives {@link shifty.promisedData} as the
* first parameter.
* @return {external:Promise}
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
*/
then(onFulfilled: Function, onRejected?: Function): Promise<any>;
private _promise: Promise<any>;
/**
* @method shifty.Tweenable#catch
* @param {function} onRejected Receives {@link shifty.promisedData} as the
* first parameter.
* @return {external:Promise}
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
*/
catch(onRejected: Function): Promise<any>;
/**
* @method shifty.Tweenable#get
* @return {Object} The current state.
*/
get(): Object;
/**
* Set the current state.
* @method shifty.Tweenable#set
* @param {Object} state The state to set.
*/
set(state: Object): void;
/**
* Pause a tween. Paused tweens can be resumed from the point at which they
* were paused. If a tween is not running, this is a no-op.
* @method shifty.Tweenable#pause
* @return {shifty.Tweenable}
*/
pause(): this;
/**
* Resume a paused tween.
* @method shifty.Tweenable#resume
* @return {shifty.Tweenable}
*/
resume(): this;
private _resume(currentTime?: number): any;
/**
* Move the state of the animation to a specific point in the tween's
* timeline. If the animation is not running, this will cause {@link
* shifty.renderFunction} handlers to be called.
* @method shifty.Tweenable#seek
* @param {millisecond} millisecond The millisecond of the animation to seek
* to. This must not be less than `0`.
* @return {shifty.Tweenable}
*/
seek(millisecond: number): this;
/**
* Stops a tween. If a tween is not running, this is a no-op. This method
* does not cancel the tween {@link external:Promise}. For that, use {@link
* shifty.Tweenable#cancel}.
* @param {boolean} [gotoEnd] If `false`, the tween just stops at its current
* state. If `true`, the tweened object's values are instantly set to the
* target values.
* @method shifty.Tweenable#stop
* @return {shifty.Tweenable}
*/
stop(gotoEnd?: boolean): this;
/**
* {@link shifty.Tweenable#stop}s a tween and also `reject`s its {@link
* external:Promise}. If a tween is not running, this is a no-op. Prevents
* calling any provided `finish` function.
* @param {boolean} [gotoEnd] Is propagated to {@link shifty.Tweenable#stop}.
* @method shifty.Tweenable#cancel
* @return {shifty.Tweenable}
* @see https://github.com/jeremyckahn/shifty/issues/122
*/
cancel(gotoEnd?: boolean): this;
/**
* Whether or not a tween is running.
* @method shifty.Tweenable#isPlaying
* @return {boolean}
*/
isPlaying(): boolean;
/**
* @method shifty.Tweenable#setScheduleFunction
* @param {shifty.scheduleFunction} scheduleFunction
* @deprecated Will be removed in favor of {@link shifty.Tweenable.setScheduleFunction} in 3.0.
*/
setScheduleFunction(scheduleFunction: scheduleFunction): void;
/**
* Get and optionally set the data that gets passed as `data` to {@link
* shifty.promisedData}, {@link shifty.startFunction} and {@link
* shifty.renderFunction}.
* @param {Object} [data]
* @method shifty.Tweenable#data
* @return {Object} The internally stored `data`.
*/
data(data?: any): any;
/**
* `delete` all "own" properties. Call this when the {@link
* shifty.Tweenable} instance is no longer needed to free memory.
* @method shifty.Tweenable#dispose
*/
dispose(): void;
}
export namespace Tweenable {
/**
* Set a custom schedule function.
*
* By default,
* [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame)
* is used if available, otherwise
* [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout)
* is used.
* @method shifty.Tweenable.setScheduleFunction
* @param {shifty.scheduleFunction} fn The function to be
* used to schedule the next frame to be rendered.
* @return {shifty.scheduleFunction} The function that was set.
*/
export function setScheduleFunction(fn: scheduleFunction): scheduleFunction;
export const filters: any;
}
// token.js
export function tweenCreated(tweenable: any): void;
export function beforeTween(tweenable: any): void;
export function afterTween(tweenable: any): void;
export function doesApply(tweenable: any): boolean;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
/*!
* TERMS OF USE - EASING EQUATIONS
* Open source under the BSD License.
* Easing Equations (c) 2003 Robert Penner, all rights reserved.
*/
/*!
* All equations are adapted from Thomas Fuchs'
* [Scripty2](https://github.com/madrobby/scripty2/blob/master/src/effects/transitions/penner.js).
*
* Based on Easing Equations (c) 2003 [Robert
* Penner](http://www.robertpenner.com/), all rights reserved. This work is
* [subject to terms](http://www.robertpenner.com/easing_terms_of_use.html).
*/
/*! Shifty 2.16.0 - https://github.com/jeremyckahn/shifty */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
namespace gdjs {
const logger = new gdjs.Logger('Video object PIXI renderer');
import PIXI = GlobalPIXIModule.PIXI;
/**
@@ -160,7 +162,7 @@ namespace gdjs {
.then(() => {})
.catch(() => {
// Autoplay was prevented.
console.warn(
logger.warn(
'The video did not start because: video is invalid or no interaction with the game has been captured before (this is blocked by the navigator: https://goo.gl/xX8pDD)'
);
});

View File

@@ -54,6 +54,12 @@ BaseObjectExtension::BaseObjectExtension() {
objectConditions["Angle"]
.SetFunctionName("getAngle")
.SetIncludeFile("runtimeobject.js");
objectConditions["BoundingBoxLeft"].SetFunctionName("getAABBLeft");
objectConditions["BoundingBoxTop"].SetFunctionName("getAABBTop");
objectConditions["BoundingBoxRight"].SetFunctionName("getAABBRight");
objectConditions["BoundingBoxBottom"].SetFunctionName("getAABBBottom");
objectConditions["BoundingBoxCenterX"].SetFunctionName("getAABBCenterX");
objectConditions["BoundingBoxCenterY"].SetFunctionName("getAABBCenterY");
objectActions["Rotate"].SetFunctionName("rotate").SetIncludeFile(
"runtimeobject.js");
objectActions["RotateTowardAngle"]
@@ -193,6 +199,12 @@ BaseObjectExtension::BaseObjectExtension() {
objectExpressions["Y"].SetFunctionName("getY");
objectExpressions["CenterX"].SetFunctionName("getCenterXInScene");
objectExpressions["CenterY"].SetFunctionName("getCenterYInScene");
objectExpressions["BoundingBoxLeft"].SetFunctionName("getAABBLeft");
objectExpressions["BoundingBoxTop"].SetFunctionName("getAABBTop");
objectExpressions["BoundingBoxRight"].SetFunctionName("getAABBRight");
objectExpressions["BoundingBoxBottom"].SetFunctionName("getAABBBottom");
objectExpressions["BoundingBoxCenterX"].SetFunctionName("getAABBCenterX");
objectExpressions["BoundingBoxCenterY"].SetFunctionName("getAABBCenterY");
objectExpressions["ZOrder"].SetFunctionName("getZOrder");
objectExpressions["Plan"].SetFunctionName("getZOrder"); // Deprecated
objectExpressions["Width"].SetFunctionName("getWidth");

View File

@@ -27,6 +27,10 @@ MouseExtension::MouseExtension() {
"gdjs.evtTools.input.isMouseButtonPressed"); // Deprecated
GetAllConditions()["MouseButtonReleased"].SetFunctionName(
"gdjs.evtTools.input.isMouseButtonReleased");
GetAllConditions()["MouseButtonFromTextPressed"].SetFunctionName(
"gdjs.evtTools.input.isMouseButtonPressed");
GetAllConditions()["MouseButtonFromTextReleased"].SetFunctionName(
"gdjs.evtTools.input.isMouseButtonReleased");
GetAllActions()["CacheSouris"].SetFunctionName(
"gdjs.evtTools.input.hideCursor");
GetAllActions()["MontreSouris"].SetFunctionName(

View File

@@ -82,7 +82,8 @@ bool Exporter::ExportWholePixiProject(
// Export engine libraries
helper.AddLibsInclude(
/*pixiRenderers=*/true,
/*websocketDebuggerClient=*/false,
/*includeWebsocketDebuggerClient=*/false,
/*includeWindowMessageDebuggerClient=*/false,
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
@@ -119,7 +120,7 @@ bool Exporter::ExportWholePixiProject(
helper.ExportProjectData(
fs, exportedProject, codeOutputDir + "/data.js", noRuntimeGameOptions);
includesFiles.push_back(codeOutputDir + "/data.js");
helper.ExportIncludesAndLibs(includesFiles, exportDir, false);
gd::String source = gdjsRoot + "/Runtime/index.html";

View File

@@ -101,7 +101,10 @@ bool ExporterHelper::ExportProjectForPixiPreview(
// Export engine libraries
AddLibsInclude(/*pixiRenderers=*/true,
/*websocketDebuggerClient=*/true,
/*includeWebsocketDebuggerClient=*/
!options.websocketDebuggerServerAddress.empty(),
/*includeWindowMessageDebuggerClient=*/
options.useWindowMessageDebuggerClient,
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
@@ -147,10 +150,10 @@ bool ExporterHelper::ExportProjectForPixiPreview(
}
runtimeGameOptions.AddChild("projectDataOnlyExport")
.SetBoolValue(options.projectDataOnlyExport);
runtimeGameOptions.AddChild("debuggerServerAddress")
.SetStringValue(options.debuggerServerAddress);
runtimeGameOptions.AddChild("debuggerServerPort")
.SetStringValue(options.debuggerServerPort);
runtimeGameOptions.AddChild("websocketDebuggerServerAddress")
.SetStringValue(options.websocketDebuggerServerAddress);
runtimeGameOptions.AddChild("websocketDebuggerServerPort")
.SetStringValue(options.websocketDebuggerServerPort);
// Pass in the options the list of scripts files - useful for hot-reloading.
auto &scriptFilesElement = runtimeGameOptions.AddChild("scriptFiles");
@@ -521,12 +524,14 @@ bool ExporterHelper::CompleteIndexFile(
}
void ExporterHelper::AddLibsInclude(bool pixiRenderers,
bool websocketDebuggerClient,
bool includeWebsocketDebuggerClient,
bool includeWindowMessageDebuggerClient,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles) {
// First, do not forget common includes (they must be included before events
// generated code files).
InsertUnique(includesFiles, "libs/jshashtable.js");
InsertUnique(includesFiles, "logger.js");
InsertUnique(includesFiles, "gd.js");
InsertUnique(includesFiles, "libs/rbush.js");
InsertUnique(includesFiles, "inputmanager.js");
@@ -570,10 +575,16 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "splash/gd-logo-light.js");
}
if (websocketDebuggerClient) {
InsertUnique(includesFiles, "websocket-debugger-client/hot-reloader.js");
if (includeWebsocketDebuggerClient || includeWindowMessageDebuggerClient) {
InsertUnique(includesFiles, "debugger-client/hot-reloader.js");
InsertUnique(includesFiles, "debugger-client/abstract-debugger-client.js");
}
if (includeWebsocketDebuggerClient) {
InsertUnique(includesFiles, "debugger-client/websocket-debugger-client.js");
}
if (includeWindowMessageDebuggerClient) {
InsertUnique(includesFiles,
"websocket-debugger-client/websocket-debugger-client.js");
"debugger-client/window-message-debugger-client.js");
}
if (pixiRenderers) {

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