Compare commits

...

239 Commits

Author SHA1 Message Date
Clément Pasteau
d73ae4c56e Bump to 5.3.193 (#6436) 2024-03-08 14:30:29 +01:00
D8H
339929e021 Sort 3D effects to show light and fog effects first (#6437) 2024-03-08 14:17:56 +01:00
github-actions[bot]
cea1cf20f1 Update translations [skip ci] (#6438)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-03-08 14:16:04 +01:00
github-actions[bot]
4ec2705f75 Update translations [skip ci] (#6435)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-03-08 12:36:52 +01:00
AlexandreS
408f6f8134 Display notifications to the user (#6432) 2024-03-08 12:15:21 +01:00
github-actions[bot]
c64bac0010 Update translations [skip ci] (#6424)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-03-08 09:54:15 +01:00
Clément Pasteau
f8d5c89ebf Allow using credits to start builds when quota is exhausted (#6430) 2024-03-08 09:26:18 +01:00
Florian Rival
ca3bb40e96 Store an achievement when finishing a tutorial (#6431) 2024-03-07 16:18:50 +01:00
Florian Rival
713d437b70 Fix naming of license file for Electron
Don't show in changelog
2024-03-07 10:15:04 +01:00
Florian Rival
09381c3836 Fix warning
Don't show in changelog
2024-03-06 17:47:08 +01:00
Florian Rival
ea4c2e0827 Fix useless query param
Don't show in changelog
2024-03-06 17:38:31 +01:00
Florian Rival
a5d1149c21 Rework the Get Started page to directly show recommendations for all users (#6428) 2024-03-06 17:35:30 +01:00
Florian Rival
119b0fadce Avoid rendering all asset packs and loading 100+ images in the Shop/Asset Store tab (#6425) 2024-03-06 17:11:32 +01:00
D8H
7111859768 Remove useless import (#6427)
Don't show in changelog
2024-03-05 15:44:07 +01:00
Vladyslav Pohorielov
08dfb4d36b Include PixiJS Spine pre-built files as part of the extension, like other extensions (#6340)
Only show in developer changelog
2024-03-05 15:28:23 +01:00
Florian Rival
cacd482af9 Fix type check for GDJS and documentation generation 2024-03-05 14:34:34 +01:00
D8H
3568a58019 Allow custom objects to be used as 3D objects (#6369)
- Custom objects like the 3D particle emitter can be used as any other 3D object
  - with the same set of actions and conditions thanks to the 3D capability
  - in the scene editor, their Z position and 3D rotations can be changed
- Fix child creation in `doStepPostEvent` function
2024-03-05 13:51:41 +01:00
Florian Rival
a32d5fcd7e Add file to ease detection of GDevelop by tool like SteamDB or Itch (#6423) 2024-03-05 13:12:16 +01:00
github-actions[bot]
1a999fd2dd Update translations [skip ci] (#6413)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2024-03-04 18:13:44 +01:00
Florian Rival
76648764bb Refactor EditorMosaic, add some placeholder for potential screen adaptation (#6421)
Don't show in changelog
2024-03-04 16:47:36 +01:00
Vladyslav Pohorielov
c25803c122 Fix Spine resources not loaded properly in games in the web-app version of GDevelop (#6416) 2024-03-04 16:03:48 +01:00
D8H
ba8d7f4e38 Fix a crash that sometimes happens when deleting links of an object (#6411) 2024-03-01 17:18:01 +01:00
AlexandreS
e1c22f6994 Display QRCode at the end of the gd games export flow (#6410) 2024-03-01 16:36:29 +01:00
Clément Pasteau
7b70f9172f Bump to 5.3.192 (#6408) 2024-03-01 12:58:55 +01:00
github-actions[bot]
a871e0f2ec Update translations [skip ci] (#6406)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-03-01 12:51:58 +01:00
Clément Pasteau
f6499e1163 Fix export flow with gdgames and show warning when game is not owned (#6407) 2024-03-01 12:34:23 +01:00
github-actions[bot]
d03e58636d Update translations [skip ci] (#6404)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-03-01 10:17:35 +01:00
AlexandreS
a7be928f2f Fix icons generating utility not working on macOS (#6405) 2024-02-29 17:07:35 +01:00
AlexandreS
7c83610d28 Fix missing s in Reponsive (#6403)
Don't show in changelog
2024-02-29 10:03:44 +01:00
github-actions[bot]
1a1e92b072 Update translations [skip ci] (#6398)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2024-02-29 09:52:55 +01:00
Florian Rival
5118f13e0b Add support for usages with 30 days period (#6401) 2024-02-28 18:20:41 +01:00
Clément Pasteau
f3ee18cdc7 Fix share tab and other design improvements on mobile + info alert when build is running (#6400)
Do not show in changelog
2024-02-28 15:12:33 +01:00
AlexandreS
c5584f746e Upgrade automatic pull request action (#6399)
Don't show in changelog
2024-02-28 14:34:57 +01:00
AlexandreS
706a6de94c Bump newIDE version (#6389) 2024-02-28 13:54:16 +01:00
github-actions[bot]
8c750b54dd Update translations [skip ci] (#6397)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2024-02-28 13:54:01 +01:00
AlexandreS
649d744664 Remove tree view nav with left and right arrow keys in project manager and events function list (#6396) 2024-02-28 13:53:30 +01:00
Florian Rival
12a6fec18e Ensure special characters like parenthesis are removed from Cordova project name to allow proper iOS app store upload (#6395)
Don't show in changelog
2024-02-28 13:35:13 +01:00
github-actions[bot]
7fc4aa47f6 Update translations [skip ci] (#6394)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-02-28 11:44:06 +01:00
github-actions[bot]
483f78fa75 Update translations [skip ci] (#6393)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-02-28 11:25:10 +01:00
Clément Pasteau
35b80818dc Improve export flow's overall design (#6392) 2024-02-28 11:04:42 +01:00
github-actions[bot]
b3f19726dc Update translations [skip ci] (#6391)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2024-02-28 10:50:47 +01:00
D8H
dd332e3cce Fix boolean property instruction group (#6386) 2024-02-28 10:24:03 +01:00
D8H
dc95a7511f Use a tree view for the project manager (#6346)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-02-27 16:51:59 +01:00
github-actions[bot]
77130c7d2e Update translations [skip ci] (#6382)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2024-02-27 16:25:46 +01:00
AlexandreS
8d1c3e8290 Improve display performance on Project and Team members lists (#6388) 2024-02-27 15:24:10 +01:00
AlexandreS
63570fd3c8 Paginate list of games (#6387) 2024-02-27 12:39:06 +01:00
AlexandreS
6945409280 Fix: Update BBText hitboxes when text changes (#6385)
- Also: Deprecate current font family setting action that uses a string as input and create a new action that uses a selector instead.
2024-02-27 08:57:17 +01:00
AlexandreS
e345b2726f Fix URL opening action on iOS (#6384) 2024-02-26 12:04:52 +01:00
github-actions[bot]
3a2a662f62 Update translations [skip ci] (#6380)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-02-23 19:35:40 +01:00
Clément Pasteau
73d850f933 Fix missing dependencies (#6381) 2024-02-23 19:35:24 +01:00
github-actions[bot]
1f2293e7d6 Update translations [skip ci] (#6371)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2024-02-23 18:54:24 +01:00
Clément Pasteau
b038984097 Display promotions & announcements only on a few pages & make it scrollable (#6379)
Do not show in changelog
2024-02-23 18:53:56 +01:00
AlexandreS
a45a5bfe7e Remove shuffling of data from backend services (#6378) 2024-02-23 17:33:25 +01:00
Florian Rival
8cbefa0cde Add support for iOS Cloud builds and automated publishing to App Store Connect (#6373)
* This allows to build directly from GDevelop, without any additional development tool or access to a Mac, a game for iOS.
* Builds can be done for development (testing on iPhone/iPad) or for the App Store directly.
* An Apple Developer account is required, so that you can generate the required certificate files (and authentication keys for uploading to App Store Connect).
* Using these new "iOS cloud builds" requires a Gold or a Pro subscription.
2024-02-23 16:42:09 +01:00
AlexandreS
1d607d474a Fix: Display audio previews in premium asset pack previews (#6377) 2024-02-23 16:07:17 +01:00
Florian Rival
3cd6e36a73 Fix scrollbars not properly styled in latest Chrome (#6376) 2024-02-23 12:16:51 +01:00
Florian Rival
cfbec2df4a Fix naming 2024-02-23 09:30:26 +01:00
Clément Pasteau
2823fde86a Few game template improvements (#6375)
Do not show in changelog
2024-02-22 14:36:33 +01:00
D8H
f29ae50f2e Fix 3d model resource changes not being detected by the other fields of the object editor (#6374) 2024-02-22 11:40:36 +01:00
Clément Pasteau
ba40c67941 Improve visibility on small tablets and mobiles in landscape (#6372)
* Prevent the screen to be detected as mobile and some layout to shift unnecessarily
2024-02-22 10:23:20 +01:00
D8H
c8ca3c2931 Fix a crash when the camera Z is changed and there is no 3D object in the project (#6370) 2024-02-21 10:36:33 +01:00
github-actions[bot]
0bb0969ab8 Update translations [skip ci] (#6365)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-02-20 12:07:31 +01:00
Clément Pasteau
3b0a7c442f Hide slideshow arrows when not hovering (#6367)
* And handle keyboard navigation

Do not show in changelog
2024-02-20 12:00:56 +01:00
AlexandreS
df3c95c466 Bump newIDE version (#6366) 2024-02-19 16:26:05 +01:00
AlexandreS
482d52ff5a Open example store from all templates list of build section (#6364)
Don't show in changelog
2024-02-19 16:13:19 +01:00
github-actions[bot]
5a78c763ef Update translations [skip ci] (#6358)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2024-02-19 14:15:40 +01:00
AlexandreS
3393af4b3b Add button to see more templates on the build section (#6359) 2024-02-19 13:57:41 +01:00
D8H
c2b77c9df7 Fix platformer character position when its height changes (#6360) 2024-02-19 12:57:26 +01:00
Clément Pasteau
514e4692ab Display promotion items on the homepage (#6353) 2024-02-19 12:45:49 +01:00
AlexandreS
210e59f201 Fix: Auto scroll to instruction item when editing an instruction (#6361) 2024-02-19 12:22:38 +01:00
D8H
e2058052f2 Fix a crash that sometimes happens when several sounds are played at the same time (#6357) 2024-02-19 11:04:01 +01:00
github-actions[bot]
90dca41e20 Update translations [skip ci] (#6351)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2024-02-19 10:29:05 +01:00
AlexandreS
15b463dad1 Add possibility to claim a monthly asset pack free of charge (#6350)
For gold and startup subscribers only
2024-02-16 17:54:45 +01:00
github-actions[bot]
a97a1f8a86 Update translations [skip ci] (#6348)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-02-16 14:15:40 +01:00
D8H
901f84daaa Add bloom and color effects for 3D layers (#6228)
- Add 3D Effects for color adjustment and bloom.
- A lower quality anti-aliasing is used when post-processing is enable (SMAA instead of MSAA).
- Upgrade to Three.js 0.160.0.
2024-02-16 14:12:46 +01:00
Clément Pasteau
6af50ae5c0 Show all products on app stores, purchasable with GDevelop credits (#6349) 2024-02-16 11:19:10 +01:00
AlexandreS
f7bd4bee6e Fix conflict between opening and previewing a private game template from the store section (#6347) 2024-02-15 14:39:11 +01:00
github-actions[bot]
4a4b015242 Update translations [skip ci] (#6345)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-02-15 14:35:19 +01:00
Clément Pasteau
75ee0b68e3 Allow buying asset packs and game templates with credits (#6343) 2024-02-14 17:03:38 +01:00
github-actions[bot]
778104447c Update translations [skip ci] (#6336)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2024-02-13 18:52:45 +01:00
D8H
a4ddc0a6ef Fix parameters drop-down list for properties of type choices (#6342) 2024-02-13 14:32:30 +01:00
D8H
3e713029e2 Fix visibility of hidden extension functions starting with an object parameter from within the extension (#6341) 2024-02-13 11:27:57 +01:00
D8H
7eb5402ee2 Keep original object and resource names when exporting assets (#6329) 2024-02-12 19:16:53 +01:00
AlexandreS
25adb026b8 Fix typo (#6337)
Don't show in changelog
2024-02-09 12:14:48 +01:00
Clément Pasteau
54bd2960ac Fix doublons in example tags search (#6335) 2024-02-09 12:14:34 +01:00
AlexandreS
1fde65b5e3 Fix template opening (#6334)
Don't show in changelog
2024-02-09 12:06:28 +01:00
github-actions[bot]
b4295b4077 Update translations [skip ci] (#6332)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2024-02-09 11:42:05 +01:00
AlexandreS
c7a929374d Fix homepage navigation (#6333)
Don't show in changelog
2024-02-09 10:09:02 +01:00
AlexandreS
7571e64396 Bump newIDE version (#6331) 2024-02-09 10:08:38 +01:00
github-actions[bot]
198267d7cb Update translations [skip ci] (#6325)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2024-02-09 09:36:53 +01:00
Aurélien Vivet
0982424d0d Set a preference for the importation of resources for projects stored on the computer (#6318) 2024-02-09 09:33:58 +01:00
D8H
29747690c2 Remove unused method declaration (#6330)
* don't show in changelog
2024-02-08 19:14:41 +01:00
AlexandreS
ad74532752 Remove unused import (#6328)
Don't show in changelog
2024-02-08 15:56:15 +01:00
AlexandreS
80e5376f74 Make opening screen more stable and reliable (#6327) 2024-02-08 15:40:50 +01:00
D8H
cc24eab2aa Allow to set the text outline and shadow from the object editor (#6246)
* Avoid the shadow to be cut out.
* Fix the shadow angle action.
2024-02-08 13:11:18 +01:00
D8H
423c8165ad Collapse advanced behavior properties (#6326) 2024-02-08 13:08:52 +01:00
D8H
0af00818dc Allow to select instances with a selection rectangle starting from within another instance holding Shift (#6304) 2024-02-07 16:24:37 +01:00
D8H
9f5c783d73 Keep 3D model origin as part of the object when used as the object origin (#6303)
- In case you choose "Model origin" for the "Origin point" property and the model origin is outside of the model, the model size may be smaller than in previous releases. To solve the issue you can:
  - Choose one of the other options for the "Origin point" property
  - Reset the "Scaling factor" to its previous value to get back the same size as before
  - Modify the model to set the origin nearer to the geometry
- Also fix the scaling of flat 3D models
2024-02-07 15:26:07 +01:00
github-actions[bot]
ad65971c01 Update translations [skip ci] (#6320)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-02-07 14:43:44 +01:00
Clément Pasteau
85ef9a9561 Fix casing gdevelop (#6323)
Do not show in changelog
2024-02-07 12:40:24 +01:00
AlexandreS
455d77fcdf Add possibility to cancel edition with Piskel/Jfxr/Yarn from the editor (#6319) 2024-02-05 18:12:43 +01:00
github-actions[bot]
02093fec0f Update translations [skip ci] (#6300)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-02-05 10:51:30 +01:00
Clément Pasteau
8227ab9cad Fix add assets button from shop being always visible (#6316) 2024-02-05 10:43:57 +01:00
D8H
f8eb91f3d2 Fix a crash when exporting assets sharing the same resources (#6314) 2024-02-03 13:41:09 +01:00
AlexandreS
a260aa5e3e Hide play tab for student profiles (#6313) 2024-02-02 11:13:37 +01:00
AlexandreS
4d8d93a550 Put Instances list search bar at the same place as the other panels in Scene editor (#6312) 2024-02-02 10:23:22 +01:00
AlexandreS
cf160bcca1 Improve game templates and examples display on build page (#6309) 2024-02-01 15:25:49 +01:00
D8H
44a0e22f97 Add autocompletion for "input type" action of text input objects (#6308) 2024-02-01 11:54:09 +01:00
D8H
d47d3285b2 Fix particle emitters not deleting itself when all their particles die before being displayed (#6306) 2024-01-31 20:12:28 +01:00
D8H
2ee6590967 Avoid event-functions to be selected when their menu is shown (#6301) 2024-01-30 11:44:16 +01:00
AlexandreS
cd77951e1a Fix game template/example search when there are only game templates matching (#6302) 2024-01-30 11:37:24 +01:00
D8H
fa4efef857 Fix a missing parameter in the sentence of the tween progress condition (#6299) 2024-01-30 10:35:25 +01:00
AlexandreS
bc8204d696 Remove cancel button when not logging in with provider (#6298)
Don't show in changelog
2024-01-30 10:07:18 +01:00
github-actions[bot]
b2eab5a327 Update translations [skip ci] (#6285)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-01-30 09:38:18 +01:00
Clément Pasteau
dba65822dd Avoid showing a loader on profile and user chip on subsequent loads (#6296) 2024-01-30 09:23:09 +01:00
AlexandreS
8f155e4322 Fix version history displaying loader on locally-stored projects (#6295) 2024-01-29 17:34:30 +01:00
D8H
13ebe5c588 Select the highest 3D objects first (#6294) 2024-01-29 16:18:48 +01:00
AlexandreS
bcd8e5608a Fix action/condition selector not updating on object change (#6291) 2024-01-29 14:18:44 +01:00
D8H
14e444413d Use the object name as the default new image name (#6293) 2024-01-29 14:01:53 +01:00
D8H
a2f75bc0dc Show installed version in the extension detail dialog (#6292) 2024-01-29 12:37:30 +01:00
D8H
6914e01a15 Fix behavior function renaming (#6290) 2024-01-29 11:18:23 +01:00
D8H
90b5f7a322 Fix the orthographic camera zoom update and its default Z position (#6288) 2024-01-29 11:17:57 +01:00
AlexandreS
13cf9b1d0b Show error box when there's an error logging in with provider (#6284)
Do not show in changelog
2024-01-26 16:28:48 +01:00
Clément Pasteau
6fc0198298 Bump version to 5.3.188 (#6283) 2024-01-26 16:26:11 +01:00
github-actions[bot]
0999ba611d Update translations [skip ci] (#6265)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-01-26 16:25:56 +01:00
Clément Pasteau
119d1af76a Game template bundles can be displayed and bought (#6275)
Do not show in changelog
2024-01-26 16:17:58 +01:00
AlexandreS
d6b4dacb1e Add possibility to sign up using Google, GitHub and Apple (#6249) 2024-01-26 15:46:18 +01:00
D8H
9edb3cfe91 Fix missing collision masks on new Sprite objects (#6282) 2024-01-26 15:25:12 +01:00
D8H
d44a1c3537 Generate particle textures only once (#6280) 2024-01-26 14:33:52 +01:00
AlexandreS
8cc84e5728 Fix legacy plans not showing in the user profile
Also: adds back the possibility to cancel a legacy subscription
2024-01-26 14:17:59 +01:00
D8H
76130b43d4 Handle export of assets with animations sharing frames (#6274) 2024-01-26 13:46:42 +01:00
D8H
40b0823b91 Fix a memory leak in the 2D particle emitter (#6278) 2024-01-26 12:23:13 +01:00
D8H
9b447e08e2 Forbid life-cycle functions to be renamed (#6269) 2024-01-25 14:53:07 +01:00
Aurélien Vivet
10314a1911 Fix focus not set in deletion confirmation dialog (#6270) 2024-01-25 14:16:17 +01:00
D8H
d0195719c2 Fix the 2 columns layout that may show hidden properties (#6268) 2024-01-25 12:18:52 +01:00
D8H
dd8c040381 Fix a crash when displaying the contextual menu of titles in object group lists (#6263) 2024-01-25 11:45:17 +01:00
D8H
5e0c8a92aa Fix a crash in the sprite editor of a child-object (#6266) 2024-01-25 11:44:31 +01:00
github-actions[bot]
977c102ddc Update translations [skip ci] (#6264)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2024-01-25 10:58:54 +01:00
Clément Pasteau
849d79d5d7 Avoid creating multiple canvas for ThreeJS (#6259) 2024-01-25 10:58:24 +01:00
D8H
28f7c9ae0b Fix a crash when the scale tween action is used in a 2D game (#6260) 2024-01-25 10:49:34 +01:00
github-actions[bot]
38e58327fa Update translations [skip ci] (#6244)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-01-24 11:22:04 +01:00
Clément Pasteau
3cc29efaa2 Remove report button on editor crash to avoid cluttering Github issues (#6247)
Do not show in changelog
2024-01-24 11:14:38 +01:00
Clément Pasteau
65eb76fb57 use newer electron builder (#6227)
Co-authored-by: romw314 <106016361+romw314@users.noreply.github.com>
2024-01-24 11:14:19 +01:00
AlexandreS
9eb721662f Remove useless margins (#6242)
Don't show in changelog
2024-01-23 14:22:39 +01:00
github-actions[bot]
b10b131010 Update translations [skip ci] (#6238)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2024-01-23 14:13:37 +01:00
Tristan Rhodes
b5fd1bb351 Improved wording "Stop music and sounds at the beginning of this scene" (#6240) 2024-01-23 14:05:31 +01:00
Aurélien Vivet
f1c9521625 Fix sentence of the Draw a Torus action (#6239) 2024-01-23 11:00:28 +01:00
github-actions[bot]
6ceb3c2c10 Update translations [skip ci] (#6234)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2024-01-22 15:38:34 +01:00
AlexandreS
43827876cd Fix subscription changes (#6236)
Don't show in changelog
2024-01-22 15:37:15 +01:00
AlexandreS
62b746300a Adapt credit banner button color to each theme (#6237)
Don't show in changelog
2024-01-22 15:29:33 +01:00
AlexandreS
769ebcd91c Fix credit banner text color on light themes (#6235)
Don't show in changelog
2024-01-22 11:12:57 +01:00
Florian Rival
70d5de16bf Fix Windows code signing (#6233) 2024-01-21 22:03:44 +01:00
Clément Pasteau
7fbe1bd23d Bump version to 5.3.187 (#6214) 2024-01-18 17:58:12 +01:00
github-actions[bot]
de8a679e31 Update translations [skip ci] (#6217)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-01-18 17:57:31 +01:00
Clément Pasteau
a1a4029b35 Introduce GDevelop credits and Marketing Campaigns (#6209)
* Credits can now be purchased from your Profile
* They can be used to purchase marketing campaigns, in order to promote your game on gd.games, with GDevelop social networks, or even within GDevelop itself.
* In the upcoming months, credits will unlock more and more exclusive perks in the app
2024-01-18 17:20:28 +01:00
Clément Pasteau
a377467031 Revert "Type JsExtensions files with typescript" (#6220)
Don't show in changelog
2024-01-18 16:01:28 +01:00
AlexandreS
dd090fd1d7 Abort login with providers (#6215)
Don't show in changelog
2024-01-18 12:32:01 +01:00
D8H
26ee9b3891 Avoid to add useless attributes on serialized extensions (#6216)
Don't show in changelog
2024-01-18 09:53:53 +01:00
Arthur Pacaud (arthuro555)
ad18eab4ae Type JsExtension.js files with TypeScript (#6200)
* Also improve typing of the C++ Core classes in TypeScript.

Only show in developer changelog
2024-01-18 09:53:24 +01:00
github-actions[bot]
007fc36a2e Update translations [skip ci] (#6184)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2024-01-18 09:36:40 +01:00
D8H
c8ebfde85b Fix platformer characters turn back speed when the deceleration is greater than the acceleration (#6208) 2024-01-17 18:32:14 +01:00
Vladyslav Pohorielov
d0005ba2cb Add support for Spine objects (2D skeletal animation) (#5927)
* This allows to use animatable 2D objects created with Spine (purchase a licence on [their website](https://esotericsoftware.com/) if you're interested).
* 2D skeletal animation allows for very smooth animations, and keep resource usage low compared to animations made of frames like Sprite objects. It's perfect for 2D games and can be used for animated characters, avatars, UI elements.
* Many thanks to @f0nar and @LousyMolars for their work on this new feature and associated game examples!

---------

Co-authored-by: Vladyslav Pohorielov <vpohorielov@playtika.com>
Co-authored-by: Gleb Volkov <glebusheg@gmail.com>
Co-authored-by: Florian Rival <Florian.rival@gmail.com>
Co-authored-by: Davy Hélard <davy.helard@gmail.com>
2024-01-17 18:19:08 +01:00
D8H
f623b352ee Hide deprecated properties of the platformer character behind a button (#6199) 2024-01-17 10:53:55 +01:00
D8H
16e2d8a005 Move some properties of the platformer character behavior in 2 columns (#6197) 2024-01-16 12:22:13 +01:00
D8H
7654883cb1 Allow to use orthographic camera in 3D layers (#6187) 2024-01-15 23:05:23 +01:00
D8H
3ae5db2a49 Fix scale and camera zoom tweens from setting NaN when the initial value was 0 (#6178) 2024-01-15 14:44:10 +01:00
Arthur Pacaud (arthuro555)
9ed002c879 Add expression to read the authenticated user unique identifier (#6192)
* This is useful if you want to interface with a third party or in-house solution to store user data.
2024-01-15 14:00:14 +01:00
Florian Rival
d4283c2350 Add new icon for 3D model objects (#6186) 2024-01-12 18:24:43 +01:00
AlexandreS
5a176d21e7 Add SSO to login/sign up (#6167)
Feature hidden in live environment.

Don't show in changelog
2024-01-12 14:45:16 +01:00
D8H
bfdfd7f0fb Improve the new UI of the extension editor (#6165)
- Add icons for behavior and object in the function tree
- Move the indicators on the right.
- Remove the big button.
- Fix the effect icon.
- Add checkboxes for the visibility.
- Rename on double click.
- Fix indentation.
- Hide the 3 dots button.

- Don't show in changelog
2024-01-12 13:11:16 +01:00
AlexandreS
aae75f2232 Fix use of Warning icon in tooltips (#6183)
Don't show in changelog
2024-01-12 13:04:20 +01:00
github-actions[bot]
977092c0a3 Update translations [skip ci] (#6179)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2024-01-12 12:38:05 +01:00
D8H
556688cedb Add pagination to asset store results (#6174) 2024-01-11 17:52:46 +01:00
github-actions[bot]
ce93dc5310 Update translations [skip ci] (#6170)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-01-10 15:57:04 +01:00
Clément Pasteau
d6d425db4f Show premium game templates from the same author on a template's page (#6168) 2024-01-10 15:42:51 +01:00
AlexandreS
2737e75639 Fix merge issue (#6169)
Don't show in changelog
2024-01-10 15:35:31 +01:00
github-actions[bot]
36eab18133 Update translations [skip ci] (#6164)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2024-01-09 16:30:55 +01:00
Clément Pasteau
48acbb12ee Decode filenames from any URLs before using them (#6018)
Only show in developer changelog
2024-01-09 16:00:09 +01:00
Florian Rival
21904e46f1 Avoid an extra click to purchase an asset pack or game template (#6161) 2024-01-09 15:32:33 +01:00
AlexandreS
94753ac053 Get subscription plans from server (#6128)
Don't show in changelog
2024-01-09 15:28:28 +01:00
github-actions[bot]
b160ee9b27 Update translations [skip ci] (#6147)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2024-01-09 15:15:24 +01:00
D8H
f76e8a72b6 Make the function property editor better adapt to small size (#6158) 2024-01-08 18:11:18 +01:00
D8H
5bc342688d Fix the cameras position to keep them centered on the object they follow when the game resolution is changed (#3821)
- Also fix the camera position for pixel-art games when a zoom and a game resolution change is used to have big pixels
2024-01-08 12:43:50 +01:00
D8H
3e6204c0eb Remember the size of mosaic nodes when they are uncollapsed (#6156)
- Don't show in changelog
2024-01-08 12:42:31 +01:00
D8H
7b1c340ad0 Fix game crashing at loading when video files are missing (#6155) 2024-01-08 12:08:48 +01:00
D8H
fac724dc3f Fix the indentation of extensions exported from the web-app (#6148) 2024-01-05 11:47:54 +01:00
D8H
9b4151f64c Add a button to export all the objetcs of a scene to submit them to the asset store (#4848)
* The dialog can be reached from the hidden drop-down menu of "Scene objects".
2024-01-05 11:09:27 +01:00
github-actions[bot]
6b5ab6c811 Update translations [skip ci] (#6130)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2024-01-05 10:06:13 +01:00
Clément Pasteau
5943092b0c Fix correctly taking folders into account when adding assets from the Home Store (#6146) 2024-01-05 10:05:44 +01:00
D8H
deb0c5ffc3 Display events functions of extensions in a tree view (#6133) 2024-01-04 18:14:44 +01:00
Clément Pasteau
c56fa03bf6 Fix crash when trying to access a child of a non existing structure child in an expression (#6144) 2024-01-04 16:20:49 +01:00
D8H
0d49d449db Import several animations to a sprite object in one go (#6035)
- Sprite animations are automatically created when several image files are selected and they ends with an animation name and an optional frame index (most naming conversions should work).
2024-01-04 11:02:04 +01:00
Clément Pasteau
fdd702cd09 Fix renaming a layer with external layouts (#6140)
* Instances of external layouts and of the attached scene are now all moved properly to the renamed layer
2024-01-04 10:31:34 +01:00
Clément Pasteau
5a8e4a7ca9 Allow confirming and canceling confirmation dialogs with keyboard shortcuts (#6138) 2024-01-03 14:52:36 +01:00
D8H
2e4e91c21e Add tween actions (position, rotation and depth) for 3D objects (#6122) 2024-01-02 10:03:59 +01:00
AlexandreS
6ece930809 Use axios error extractor where possible (#6132)
Only show in developer changelog
2023-12-27 19:40:16 +01:00
AlexandreS
c5baa81977 Display Game template categories with chips (#6129)
Don't show in changelog
2023-12-27 16:10:19 +01:00
github-actions[bot]
d24be38874 Update translations [skip ci] (#6109)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2023-12-27 10:35:14 +01:00
AlexandreS
1efffbbb78 Fix: Avoid project commit error by retrying call to server (#6127) 2023-12-26 12:12:25 +01:00
D8H
090d76a368 Show a drop-down list for string with choices parameters (#6120) 2023-12-22 15:31:43 +01:00
D8H
d44a9375de Add variables autocompletion for groups in variable fields (#6110) 2023-12-21 12:20:14 +01:00
AlexandreS
5a2a3893f9 Remove dashboard from profile dialog (#6113) 2023-12-21 12:08:23 +01:00
AlexandreS
0a6b0dc785 Use cloud storage provider internal name (#6108)
Don't show in changelog
2023-12-20 12:15:04 +01:00
github-actions[bot]
02e99726dc Update translations [skip ci] (#6103)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2023-12-20 11:55:13 +01:00
AlexandreS
843055d8df Add error message when versions fail to load (#6105)
Don't show in changelog
2023-12-20 11:40:07 +01:00
AlexandreS
f7fda5cb5e Fix placeholder thumbnail in objects lists for prefabs (#6107) 2023-12-20 11:37:28 +01:00
AlexandreS
e529642aec Fix program opening count logic when user has never acknowledged the feature (#6106)
Don't show in changelog
2023-12-20 10:01:35 +01:00
AlexandreS
c8e10d7043 Bump newIDE version (#6104) 2023-12-19 12:25:29 +01:00
AlexandreS
5f0de0e9a7 Adapt extract changelog script to currently used format (#6102)
Only show in developer changelog
2023-12-19 12:18:25 +01:00
github-actions[bot]
e51638ce4b Update translations [skip ci] (#6101)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2023-12-19 12:17:54 +01:00
AlexandreS
0fbd6a606a Add possibility to restore a previous version of a cloud project (#6022)
- Display all the project versions (paginated)
- Allow to open any version and name it for better tracking
- Make it possible to restore a given version
2023-12-19 11:50:28 +01:00
github-actions[bot]
2fa543c3db Update translations [skip ci] (#6083)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2023-12-18 16:56:41 +01:00
AlexandreS
38e35c9695 Prevent crash on new object dialog (#6099) 2023-12-18 16:50:21 +01:00
Florian Rival
ad13a1a101 Fix changes done in an extension not applied when clicking on 'Share' immediately without a preview or navigation in the editor (#6098) 2023-12-18 16:02:27 +01:00
AlexandreS
cb9d98d027 Wait 10 program openings before displaying GamesDashboard info (#6097)
Don't show in changelog
2023-12-18 15:51:23 +01:00
D8H
064c3f1572 Fix warning in the object list (#6091)
- don't show in changelog
2023-12-15 18:46:40 +01:00
D8H
9e5320f9d4 Add an item menu to add sub-folders in object folders (#6090) 2023-12-15 17:56:19 +01:00
Clément Pasteau
a1826d355d Rework subscriptions display in Profile (#6089) 2023-12-15 17:27:48 +01:00
D8H
32a3a094d1 Add new objects under the selected one in the list (#6087) 2023-12-15 16:51:50 +01:00
D8H
ee7dc2654b Fix parameter value choices autocompletion (#6086) 2023-12-15 12:02:24 +01:00
Clément Pasteau
64ffad3c0a Bump to 5.3.185 (#6082) 2023-12-14 14:06:01 +01:00
github-actions[bot]
dcc62f078f Update translations [skip ci] (#6076)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2023-12-14 13:54:43 +01:00
D8H
d920f05dbc Fix animation scanning in the 3D model object editor (#6080) 2023-12-14 13:28:38 +01:00
AlexandreS
ac82be800b Fix: update file metadata when saving cloud project triggers new commit (#6074) 2023-12-13 16:50:50 +01:00
Aurélien Vivet
2c92ae4042 Fix typo (#6075) 2023-12-13 16:35:36 +01:00
Clément Pasteau
465e934605 Bump version to 5.3.184 (#6072) 2023-12-13 14:34:04 +01:00
github-actions[bot]
248ba7675e Update translations [skip ci] (#6070)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2023-12-13 14:19:15 +01:00
AlexandreS
fd2b59ba45 Revert "Use python3 to update bindings (#6068)" (#6071)
This reverts commit 6edf63e98f.

Only show in developer changelog
2023-12-13 12:01:14 +01:00
Clément Pasteau
be54236ece Fix using which from key event when not defined (#6069)
Do not show in changelog
2023-12-13 11:59:57 +01:00
AlexandreS
6edf63e98f Use python3 to update bindings (#6068)
Only show in developer changelog
2023-12-13 11:36:44 +01:00
github-actions[bot]
034f1ad9cc Update translations [skip ci] (#6057)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2023-12-12 15:42:47 +01:00
Florian Rival
94b8c31ac2 Allow to increment position of points of sprites by 0.5 in the editor 2023-12-12 15:11:21 +01:00
Florian Rival
57d1241e2d Allow to increment position of vertices of collision masks by 0.5 2023-12-12 15:08:50 +01:00
github-actions[bot]
eb4708ca87 Update translations [skip ci] (#6051)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2023-12-11 14:47:52 +01:00
D8H
fac710780b Fix a flash of 1 black frame the first time a scene is started (#6048) 2023-12-11 14:41:27 +01:00
github-actions[bot]
d28aac325a Update translations [skip ci] (#6033)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2023-12-11 14:18:26 +01:00
Clément Pasteau
306b341ee5 Education premium users now also have a dedicated Discord channel (#6047) 2023-12-11 14:17:09 +01:00
D8H
1d8e04cb78 Allow to set 3D models dimensions with a scaling factor (#6044)
- It allows to keep consistent dimensions for a set of 3D models
2023-12-11 12:13:17 +01:00
Clément Pasteau
3b2855de59 Fix a possible crash when closing an app window (#6046) 2023-12-11 10:29:10 +01:00
833 changed files with 56474 additions and 45174 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
cache: "npm"
cache-dependency-path: "newIDE/app/package-lock.json"
@@ -58,7 +58,7 @@ jobs:
working-directory: newIDE/app
- name: Create a Pull Request with the changes
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v6
with:
commit-message: Update translations [skip ci]
branch: chore/update-translations

View File

@@ -714,6 +714,8 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
metadata.GetType() == "tilesetResource" ||
metadata.GetType() == "videoResource" ||
metadata.GetType() == "model3DResource" ||
metadata.GetType() == "atlasResource" ||
metadata.GetType() == "spineResource" ||
// Deprecated, old parameter names:
metadata.GetType() == "password" || metadata.GetType() == "musicfile" ||
metadata.GetType() == "soundfile" || metadata.GetType() == "police") {

View File

@@ -1281,8 +1281,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Enable an effect on the object"),
_("Enable effect _PARAM1_ on _PARAM0_: _PARAM2_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("objectEffectName", _("Effect name"))
.AddParameter("yesorno", _("Enable?"))
@@ -1297,8 +1297,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"names) in the effects window."),
_("Set _PARAM2_ to _PARAM3_ for effect _PARAM1_ of _PARAM0_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("objectEffectName", _("Effect name"))
.AddParameter("objectEffectParameterName", _("Property name"))
@@ -1315,8 +1315,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"names) in the effects window."),
_("Set _PARAM2_ to _PARAM3_ for effect _PARAM1_ of _PARAM0_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("objectEffectName", _("Effect name"))
.AddParameter("objectEffectParameterName", _("Property name"))
@@ -1332,8 +1332,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
"names) in the effects window."),
_("Enable _PARAM2_ for effect _PARAM1_ of _PARAM0_: _PARAM3_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("objectEffectName", _("Effect name"))
.AddParameter("objectEffectParameterName", _("Property name"))
@@ -1347,8 +1347,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Check if the effect on an object is enabled."),
_("Effect _PARAM1_ of _PARAM0_ is enabled"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("objectEffectName", _("Effect name"))
.MarkAsSimple()

View File

@@ -27,7 +27,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
extension.AddInstructionOrExpressionGroupMetadata(_("Layers and cameras"))
.SetIcon("res/conditions/camera24.png");
extension.AddInstructionOrExpressionGroupMetadata(_("Effects"))
.SetIcon("res/actions/effect24.png");
.SetIcon("res/actions/effect_black.svg");
extension
.AddExpressionAndConditionAndAction(
@@ -450,8 +450,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
"names) in the effects window."),
_("Set _PARAM3_ to _PARAM4_ for effect _PARAM2_ of layer _PARAM1_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer"), "", true)
.SetDefaultValue("\"\"")
@@ -469,8 +469,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
"names) in the effects window."),
_("Set _PARAM3_ to _PARAM4_ for effect _PARAM2_ of layer _PARAM1_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer"), "", true)
.SetDefaultValue("\"\"")
@@ -488,8 +488,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
"names) in the effects window."),
_("Enable _PARAM3_ for effect _PARAM2_ of layer _PARAM1_: _PARAM4_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer"), "", true)
.SetDefaultValue("\"\"")
@@ -504,8 +504,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
_("The effect on a layer is enabled"),
_("Effect _PARAM2_ on layer _PARAM1_ is enabled"),
_(""),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer"), "", true)
.SetDefaultValue("\"\"")
@@ -518,8 +518,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
_("Enable an effect on a layer"),
_("Enable effect _PARAM2_ on layer _PARAM1_: _PARAM3_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer"), "", true)
.SetDefaultValue("\"\"")

View File

@@ -24,7 +24,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
"Open source (MIT License)")
.SetExtensionHelpPath("/objects");
extension.AddInstructionOrExpressionGroupMetadata(_("Effects"))
.SetIcon("res/actions/effect24.png");
.SetIcon("res/actions/effect_black.svg");
gd::BehaviorMetadata& aut = extension.AddBehavior(
"EffectBehavior",
@@ -32,7 +32,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
"Effect",
_("Apply visual effects to objects."),
"",
"res/actions/effect24.png",
"res/actions/effect_black.svg",
"EffectBehavior",
std::make_shared<gd::Behavior>(),
std::make_shared<gd::BehaviorsSharedData>())
@@ -43,8 +43,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
_("Enable an effect on the object"),
_("Enable effect _PARAM2_ on _PARAM0_: _PARAM3_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "EffectBehavior")
.AddParameter("objectEffectName", _("Effect name"))
@@ -58,8 +58,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
"names) in the effects window."),
_("Set _PARAM3_ to _PARAM4_ for effect _PARAM2_ of _PARAM0_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "EffectBehavior")
.AddParameter("objectEffectName", _("Effect name"))
@@ -75,8 +75,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
"names) in the effects window."),
_("Set _PARAM3_ to _PARAM4_ for effect _PARAM2_ of _PARAM0_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "EffectBehavior")
.AddParameter("objectEffectName", _("Effect name"))
@@ -91,8 +91,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
"names) in the effects window."),
_("Enable _PARAM3_ for effect _PARAM2_ of _PARAM0_: _PARAM4_"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "EffectBehavior")
.AddParameter("objectEffectName", _("Effect name"))
@@ -105,8 +105,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
_("Check if the effect on an object is enabled."),
_("Effect _PARAM2_ of _PARAM0_ is enabled"),
_("Effects"),
"res/actions/effect24.png",
"res/actions/effect.png")
"res/actions/effect_black.svg",
"res/actions/effect_black.svg")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "EffectBehavior")
.AddParameter("objectEffectName", _("Effect name"))

View File

@@ -24,7 +24,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFlippableExtension(
"Open source (MIT License)")
.SetExtensionHelpPath("/objects");
extension.AddInstructionOrExpressionGroupMetadata(_("Effects"))
.SetIcon("res/actions/effect24.png");
.SetIcon("res/actions/effect_black.svg");
gd::BehaviorMetadata& aut = extension.AddBehavior(
"FlippableBehavior",

View File

@@ -114,6 +114,8 @@ public:
/**
* \brief Erase any existing include file and add the specified include.
* \deprecated Use `AddIncludeFile` instead as clearing the list is more
* error prone.
*/
virtual AbstractFunctionMetadata &
SetIncludeFile(const gd::String &includeFile) = 0;

View File

@@ -13,12 +13,15 @@
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/Project/Behavior.h"
#include "GDCore/Project/BehaviorsSharedData.h"
#include "GDCore/Project/PropertyDescriptor.h"
#include "GDCore/Tools/Localization.h"
#include "GDCore/Tools/MakeUnique.h"
#include "GDCore/Tools/Log.h"
namespace gd {
const std::map<gd::String, gd::PropertyDescriptor> BehaviorMetadata::badProperties;
BehaviorMetadata::BehaviorMetadata(
const gd::String& extensionNamespace_,
const gd::String& nameWithNamespace,
@@ -47,8 +50,14 @@ BehaviorMetadata::BehaviorMetadata(
"BehaviorMetadata is valid for: " + nameWithNamespace);
}
if (instance) instance->SetTypeName(nameWithNamespace);
if (sharedDatasInstance) sharedDatasInstance->SetTypeName(nameWithNamespace);
if (instance) {
instance->SetTypeName(nameWithNamespace);
instance->InitializeContent();
}
if (sharedDatasInstance) {
sharedDatasInstance->SetTypeName(nameWithNamespace);
sharedDatasInstance->InitializeContent();
}
}
gd::InstructionMetadata& BehaviorMetadata::AddCondition(
@@ -405,10 +414,30 @@ gd::Behavior& BehaviorMetadata::Get() const {
return *instance;
}
std::map<gd::String, gd::PropertyDescriptor> BehaviorMetadata::GetProperties() const {
if (!instance) {
return badProperties;
}
// TODO Properties should be declared on BehaviorMetadata directly.
// - Add 2 `properties` members (one for shared properties)
// - Add methods to declare new properties
return instance->GetProperties();
}
gd::BehaviorsSharedData* BehaviorMetadata::GetSharedDataInstance() const {
return sharedDatasInstance.get();
}
std::map<gd::String, gd::PropertyDescriptor> BehaviorMetadata::GetSharedProperties() const {
if (!sharedDatasInstance) {
return badProperties;
}
// TODO Properties should be declared on BehaviorMetadata directly.
// - Add 2 `properties` members (one for shared properties)
// - Add methods to declare new properties
return sharedDatasInstance->GetProperties();
}
const std::vector<gd::String>& BehaviorMetadata::GetRequiredBehaviorTypes() const {
requiredBehaviors.clear();
for (auto& property : Get().GetProperties()) {

View File

@@ -18,6 +18,7 @@ class BehaviorsSharedData;
class MultipleInstructionMetadata;
class InstructionMetadata;
class ExpressionMetadata;
class PropertyDescriptor;
} // namespace gd
namespace gd {
@@ -204,6 +205,8 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
* \brief Erase any existing include file and add the specified include.
* \note The requirement may vary depending on the platform: Most of the time,
* the include file contains the declaration of the behavior.
* \deprecated Use `AddIncludeFile` instead as clearing the list is more
* error prone.
*/
BehaviorMetadata& SetIncludeFile(const gd::String& includeFile) override;
@@ -302,6 +305,15 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
*/
gd::Behavior& Get() const;
/**
* \brief Called when the IDE wants to know about the custom properties of the
* behavior.
*
* \return a std::map with properties names as key.
* \see gd::PropertyDescriptor
*/
std::map<gd::String, gd::PropertyDescriptor> GetProperties() const;
/**
* \brief Return the associated gd::BehaviorsSharedData, handling behavior
* shared data, if any (nullptr if none).
@@ -311,6 +323,15 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
*/
gd::BehaviorsSharedData* GetSharedDataInstance() const;
/**
* \brief Called when the IDE wants to know about the custom shared properties
* of the behavior.
*
* \return a std::map with properties names as key.
* \see gd::PropertyDescriptor
*/
std::map<gd::String, gd::PropertyDescriptor> GetSharedProperties() const;
/**
* \brief Return a reference to a map containing the names of the actions
* (as keys) and the metadata associated with (as values).
@@ -357,6 +378,8 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
// TODO: Nitpicking: convert these to std::unique_ptr to clarify ownership.
std::shared_ptr<gd::Behavior> instance;
std::shared_ptr<gd::BehaviorsSharedData> sharedDatasInstance;
static const std::map<gd::String, gd::PropertyDescriptor> badProperties;
};
} // namespace gd

View File

@@ -65,6 +65,8 @@ class GD_CORE_API EffectMetadata {
/**
* \brief Clear any existing include file and add the specified include file.
* \deprecated Use `AddIncludeFile` instead as clearing the list is more
* error prone.
*/
EffectMetadata& SetIncludeFile(const gd::String& includeFile);

View File

@@ -50,11 +50,11 @@ gd::ExpressionMetadata& ExpressionMetadata::AddParameter(
gd::ParameterMetadata::IsBehavior(type))
// Prefix with the namespace if it's not already there.
&& (supplementaryInformation.find(
PlatformExtension::GetNamespaceSeparator()) != gd::String::npos)
? supplementaryInformation
: (supplementaryInformation.empty()
PlatformExtension::GetNamespaceSeparator()) == gd::String::npos)
? (supplementaryInformation.empty()
? ""
: extensionNamespace + supplementaryInformation)));
: extensionNamespace + supplementaryInformation)
: supplementaryInformation));
// TODO: Assert against supplementaryInformation === "emsc" (when running with
// Emscripten), and warn about a missing argument when calling addParameter.

View File

@@ -288,6 +288,8 @@ class GD_CORE_API ExpressionMetadata : public gd::AbstractFunctionMetadata {
/**
* \brief Erase any existing include file and add the specified include.
* \deprecated Use `AddIncludeFile` instead as clearing the list is more
* error prone.
*/
ExpressionMetadata& SetIncludeFile(
const gd::String& includeFile) override {

View File

@@ -68,11 +68,11 @@ InstructionMetadata& InstructionMetadata::AddParameter(
gd::ParameterMetadata::IsBehavior(type))
// Prefix with the namespace if it's not already there.
&& (supplementaryInformation.find(
PlatformExtension::GetNamespaceSeparator()) != gd::String::npos)
? supplementaryInformation
: (supplementaryInformation.empty()
PlatformExtension::GetNamespaceSeparator()) == gd::String::npos)
? (supplementaryInformation.empty()
? ""
: extensionNamespace + supplementaryInformation)));
: extensionNamespace + supplementaryInformation)
: supplementaryInformation));
// TODO: Assert against supplementaryInformation === "emsc" (when running with
// Emscripten), and warn about a missing argument when calling addParameter.

View File

@@ -494,6 +494,8 @@ class GD_CORE_API InstructionMetadata : public gd::AbstractFunctionMetadata {
/**
* \brief Erase any existing include file and add the specified include.
* \deprecated Use `AddIncludeFile` instead as clearing the list is more
* error prone.
*/
InstructionMetadata &SetIncludeFile(const gd::String &includeFile) override {
codeExtraInformation.includeFiles.clear();

View File

@@ -142,6 +142,8 @@ public:
* \brief Erase any existing include file and add the specified include.
* \note The requirement may vary depending on the platform: Most of the time,
* the include file contains the declaration of the behavior.
* \deprecated Use `AddIncludeFile` instead as clearing the list is more
* error prone.
*/
virtual InstructionOrExpressionContainerMetadata &
SetIncludeFile(const gd::String &includeFile) = 0;

View File

@@ -150,6 +150,10 @@ class GD_CORE_API MultipleInstructionMetadata : public AbstractFunctionMetadata
return *this;
}
/**
* \deprecated Use `AddIncludeFile` instead as clearing the list is more
* error prone.
*/
MultipleInstructionMetadata &SetIncludeFile(const gd::String &includeFile) override {
if (expression)
expression->SetIncludeFile(includeFile);

View File

@@ -264,6 +264,8 @@ class GD_CORE_API ObjectMetadata : public InstructionOrExpressionContainerMetada
* \brief Erase any existing include file and add the specified include.
* \note The requirement may vary depending on the platform: Most of the time,
* the include file contains the declaration of the object.
* \deprecated Use `AddIncludeFile` instead as clearing the list is more
* error prone.
*/
ObjectMetadata& SetIncludeFile(const gd::String& includeFile) override;

View File

@@ -217,7 +217,9 @@ class GD_CORE_API ValueTypeMetadata {
parameterType == "jsonResource" ||
parameterType == "tilemapResource" ||
parameterType == "tilesetResource" ||
parameterType == "model3DResource";
parameterType == "model3DResource" ||
parameterType == "atlasResource" ||
parameterType == "spineResource";
}
return false;
}

View File

@@ -285,22 +285,6 @@ class GD_CORE_API PlatformExtension {
std::shared_ptr<gd::Behavior> instance,
std::shared_ptr<gd::BehaviorsSharedData> sharedDatasInstance);
/**
* \brief Declare a new events based behavior as being part of the extension.
*
* \param name The name of the behavior
* \param fullname The user friendly name of the behavior
* \param description The user friendly description of the behavior
* \param group The behavior category label
* \param icon The icon of the behavior.
*/
gd::BehaviorMetadata& AddEventsBasedBehavior(
const gd::String& name_,
const gd::String& fullname_,
const gd::String& description_,
const gd::String& group_,
const gd::String& icon_);
/**
* \brief Declare a new effect as being part of the extension.
* \param name The internal name of the effect (also called effect type).

View File

@@ -39,7 +39,8 @@ struct VariableAndItsParent {
};
/**
* \brief Find the last parent (i.e: the variables container) of a node representing a variable.
* \brief Find the last parent (i.e: the variables container) of a node
* representing a variable.
*
* Useful for completions, to know which variables can be entered in a node.
*
@@ -48,13 +49,12 @@ struct VariableAndItsParent {
class GD_CORE_API ExpressionVariableParentFinder
: public ExpressionParser2NodeWorker {
public:
static VariableAndItsParent GetLastParentOfNode(
const gd::Platform& platform,
const gd::ProjectScopedContainers& projectScopedContainers,
gd::ExpressionNode& node) {
gd::ExpressionVariableParentFinder typeFinder(
platform, projectScopedContainers);
gd::ExpressionVariableParentFinder typeFinder(platform,
projectScopedContainers);
node.Visit(typeFinder);
return typeFinder.variableAndItsParent;
}
@@ -70,7 +70,8 @@ class GD_CORE_API ExpressionVariableParentFinder
variableNode(nullptr),
thisIsALegacyPrescopedVariable(false),
bailOutBecauseEmptyVariableName(false),
legacyPrescopedVariablesContainer(nullptr){};
legacyPrescopedVariablesContainer(nullptr),
variableAndItsParent{} {};
void OnVisitSubExpressionNode(SubExpressionNode& node) override {}
void OnVisitOperatorNode(OperatorNode& node) override {}
@@ -135,10 +136,10 @@ class GD_CORE_API ExpressionVariableParentFinder
}
void OnVisitVariableAccessorNode(VariableAccessorNode& node) override {
if (node.name.empty() && node.child) {
// A variable accessor should always have a name if it has a child (i.e: another accessor).
// While the parser may have generated an empty name,
// flag this so we avoid finding a wrong parent (and so, run the risk of giving
// wrong autocompletions).
// A variable accessor should always have a name if it has a child (i.e:
// another accessor). While the parser may have generated an empty name,
// flag this so we avoid finding a wrong parent (and so, run the risk of
// giving wrong autocompletions).
bailOutBecauseEmptyVariableName = true;
}
childVariableNames.insert(childVariableNames.begin(), node.name);
@@ -300,7 +301,8 @@ class GD_CORE_API ExpressionVariableParentFinder
const std::vector<gd::String>& childVariableNames,
size_t startIndex = 0) {
if (bailOutBecauseEmptyVariableName)
return {}; // Do not even attempt to find the parent if we had an issue when visiting nodes.
return {}; // Do not even attempt to find the parent if we had an issue
// when visiting nodes.
const gd::Variable* currentVariable = &variable;
@@ -332,8 +334,8 @@ class GD_CORE_API ExpressionVariableParentFinder
}
}
// Return the last parent of the chain of variables (so not the last variable
// but the one before it).
// Return the last parent of the chain of variables (so not the last
// variable but the one before it).
return {.parentVariable = currentVariable};
}
@@ -341,14 +343,16 @@ class GD_CORE_API ExpressionVariableParentFinder
const gd::VariablesContainer& variablesContainer,
const std::vector<gd::String>& childVariableNames) {
if (bailOutBecauseEmptyVariableName)
return {}; // Do not even attempt to find the parent if we had an issue when visiting nodes.
return {}; // Do not even attempt to find the parent if we had an issue
// when visiting nodes.
if (childVariableNames.empty())
return {}; // There is no "parent" to the variables container itself.
const gd::String& firstChildName = *childVariableNames.begin();
const gd::Variable* variable = variablesContainer.Has(firstChildName) ?
&variablesContainer.Get(firstChildName) : nullptr;
const gd::Variable* variable = variablesContainer.Has(firstChildName)
? &variablesContainer.Get(firstChildName)
: nullptr;
if (childVariableNames.size() == 1 || !variable)
return {// Only one child: the parent is the variables container itself.
.parentVariablesContainer = &variablesContainer};

View File

@@ -0,0 +1,111 @@
/*
* GDevelop Core
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#include "ObjectAssetSerializer.h"
#include <algorithm>
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteObject.h"
#include "GDCore/Extensions/Metadata/BehaviorMetadata.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/IDE/Project/AssetResourcePathCleaner.h"
#include "GDCore/IDE/Project/ResourcesInUseHelper.h"
#include "GDCore/IDE/Project/ResourcesRenamer.h"
#include "GDCore/Project/Behavior.h"
#include "GDCore/Project/CustomBehavior.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/PropertyDescriptor.h"
#include "GDCore/Serialization/SerializerElement.h"
#include "GDCore/Tools/Log.h"
namespace gd {
gd::String
ObjectAssetSerializer::GetObjectExtensionName(const gd::Object &object) {
const gd::String &type = object.GetType();
const auto separatorIndex =
type.find(PlatformExtension::GetNamespaceSeparator());
return separatorIndex != std::string::npos ? type.substr(0, separatorIndex)
: "";
}
void ObjectAssetSerializer::SerializeTo(
gd::Project &project, const gd::Object &object,
const gd::String &objectFullName, SerializerElement &element,
std::vector<gd::String> &usedResourceNames) {
auto cleanObject = object.Clone();
cleanObject->GetVariables().Clear();
cleanObject->GetEffects().Clear();
for (auto &&behaviorName : cleanObject->GetAllBehaviorNames()) {
cleanObject->RemoveBehavior(behaviorName);
}
gd::String extensionName = GetObjectExtensionName(*cleanObject);
element.SetAttribute("id", "");
element.SetAttribute("name", "");
element.SetAttribute("license", "");
if (project.HasEventsFunctionsExtensionNamed(extensionName)) {
auto &extension = project.GetEventsFunctionsExtension(extensionName);
element.SetAttribute("description", extension.GetShortDescription());
}
element.SetAttribute("gdevelopVersion", "");
element.SetAttribute("version", "");
element.SetIntAttribute("animationsCount", 1);
element.SetIntAttribute("maxFramesCount", 1);
// TODO Find the right object dimensions.
element.SetIntAttribute("width", 0);
element.SetIntAttribute("height", 0);
SerializerElement &authorsElement = element.AddChild("authors");
authorsElement.ConsiderAsArrayOf("author");
SerializerElement &tagsElement = element.AddChild("tags");
tagsElement.ConsiderAsArrayOf("tag");
SerializerElement &objectAssetsElement = element.AddChild("objectAssets");
objectAssetsElement.ConsiderAsArrayOf("objectAsset");
SerializerElement &objectAssetElement =
objectAssetsElement.AddChild("objectAsset");
cleanObject->SerializeTo(objectAssetElement.AddChild("object"));
SerializerElement &resourcesElement =
objectAssetElement.AddChild("resources");
resourcesElement.ConsiderAsArrayOf("resource");
auto &resourcesManager = project.GetResourcesManager();
gd::ResourcesInUseHelper resourcesInUse(resourcesManager);
cleanObject->GetConfiguration().ExposeResources(resourcesInUse);
for (auto &&resourceName : resourcesInUse.GetAllResources()) {
if (resourceName.length() == 0) {
continue;
}
usedResourceNames.push_back(resourceName);
auto &resource = resourcesManager.GetResource(resourceName);
SerializerElement &resourceElement = resourcesElement.AddChild("resource");
resource.SerializeTo(resourceElement);
resourceElement.SetAttribute("kind", resource.GetKind());
resourceElement.SetAttribute("name", resource.GetName());
}
SerializerElement &requiredExtensionsElement =
objectAssetElement.AddChild("requiredExtensions");
requiredExtensionsElement.ConsiderAsArrayOf("requiredExtension");
if (project.HasEventsFunctionsExtensionNamed(extensionName)) {
SerializerElement &requiredExtensionElement =
requiredExtensionsElement.AddChild("requiredExtension");
requiredExtensionElement.SetAttribute("extensionName", extensionName);
requiredExtensionElement.SetAttribute("extensionVersion", "1.0.0");
}
// TODO This can be removed when the asset script no longer require it.
SerializerElement &customizationElement =
objectAssetElement.AddChild("customization");
customizationElement.ConsiderAsArrayOf("empty");
}
} // namespace gd

View File

@@ -0,0 +1,57 @@
/*
* GDevelop Core
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#pragma once
#include <map>
#include <vector>
#include "GDCore/String.h"
namespace gd {
class Object;
class ExtensionDependency;
class PropertyDescriptor;
class Project;
class Layout;
class ArbitraryResourceWorker;
class InitialInstance;
class SerializerElement;
class EffectsContainer;
class AbstractFileSystem;
} // namespace gd
namespace gd {
/**
* \brief Serialize objects into an asset for the store.
*
* \ingroup IDE
*/
class GD_CORE_API ObjectAssetSerializer {
public:
/**
* \brief Serialize an object into an asset.
*
* \param project The project that contains the object and its resources.
* It's not actually modified.
* \param object The object to serialize as an asset.
* \param objectFullName The object name with spaces instead of PascalCase.
* \param element The element where the asset is serialize.
* \param usedResourceNames Return the names of the resources used by the asset.
*/
static void
SerializeTo(gd::Project &project, const gd::Object &object,
const gd::String &objectFullName, SerializerElement &element,
std::vector<gd::String> &usedResourceNames);
~ObjectAssetSerializer(){};
private:
ObjectAssetSerializer(){};
static gd::String GetObjectExtensionName(const gd::Object &object);
};
} // namespace gd

View File

@@ -52,6 +52,16 @@ void ArbitraryResourceWorker::ExposeModel3D(gd::String& resourceName){
// do.
};
void ArbitraryResourceWorker::ExposeAtlas(gd::String& resourceName){
// Nothing to do by default - each child class can define here the action to
// do.
};
void ArbitraryResourceWorker::ExposeSpine(gd::String& resourceName){
// 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.
@@ -120,6 +130,7 @@ void ArbitraryResourceWorker::ExposeEmbeddeds(gd::String& resourceName) {
gd::String potentiallyUpdatedTargetResourceName = targetResourceName;
ExposeResourceWithType(targetResource.GetKind(), potentiallyUpdatedTargetResourceName);
ExposeEmbeddeds(potentiallyUpdatedTargetResourceName);
if (potentiallyUpdatedTargetResourceName != targetResourceName) {
// The resource name was renamed. Also update the mapping.
@@ -176,6 +187,14 @@ void ArbitraryResourceWorker::ExposeResourceWithType(
ExposeVideo(resourceName);
return;
}
if (resourceType == "atlas") {
ExposeAtlas(resourceName);
return;
}
if (resourceType == "spine") {
ExposeSpine(resourceName);
return;
}
gd::LogError("Unexpected resource type: " + resourceType + " for: " + resourceName);
return;
}
@@ -244,6 +263,14 @@ bool ResourceWorkerInEventsWorker::DoVisitInstruction(gd::Instruction& instructi
gd::String updatedParameterValue = parameterValue;
worker.ExposeModel3D(updatedParameterValue);
instruction.SetParameter(parameterIndex, updatedParameterValue);
} else if (parameterMetadata.GetType() == "atlasResource") {
gd::String updatedParameterValue = parameterValue;
worker.ExposeAtlas(updatedParameterValue);
instruction.SetParameter(parameterIndex, updatedParameterValue);
} else if (parameterMetadata.GetType() == "spineResource") {
gd::String updatedParameterValue = parameterValue;
worker.ExposeSpine(updatedParameterValue);
instruction.SetParameter(parameterIndex, updatedParameterValue);
}
});

View File

@@ -96,6 +96,16 @@ public:
* \brief Expose a 3D model, which is always a reference to a "model3D" resource.
*/
virtual void ExposeModel3D(gd::String &resourceName);
/**
* \brief Expose an atlas, which is always a reference to a "atlas" resource.
*/
virtual void ExposeAtlas(gd::String &resourceName);
/**
* \brief Expose an spine, which is always a reference to a "spine" resource.
*/
virtual void ExposeSpine(gd::String &resourceName);
/**
* \brief Expose a video, which is always a reference to a "video" resource.
@@ -123,14 +133,15 @@ public:
*/
virtual void ExposeEmbeddeds(gd::String &resourceName);
protected:
gd::ResourcesManager * resourcesManager;
private:
/**
* \brief Expose a resource: resources that have a file are
* exposed as file (see ExposeFile).
*/
void ExposeResource(gd::Resource &resource);
gd::ResourcesManager * resourcesManager;
};
/**

View File

@@ -0,0 +1,74 @@
/*
* GDevelop Core
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#include "AssetResourcePathCleaner.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/ResourcesManager.h"
#include "GDCore/String.h"
namespace gd {
void AssetResourcePathCleaner::ExposeImage(gd::String &imageName) {
ExposeResourceAsFile(imageName);
}
void AssetResourcePathCleaner::ExposeAudio(gd::String &audioName) {
ExposeResourceAsFile(audioName);
}
void AssetResourcePathCleaner::ExposeFont(gd::String &fontName) {
ExposeResourceAsFile(fontName);
}
void AssetResourcePathCleaner::ExposeJson(gd::String &jsonName) {
ExposeResourceAsFile(jsonName);
}
void AssetResourcePathCleaner::ExposeTilemap(gd::String &tilemapName) {
ExposeResourceAsFile(tilemapName);
}
void AssetResourcePathCleaner::ExposeTileset(gd::String &tilesetName) {
ExposeResourceAsFile(tilesetName);
}
void AssetResourcePathCleaner::ExposeVideo(gd::String &videoName) {
ExposeResourceAsFile(videoName);
}
void AssetResourcePathCleaner::ExposeBitmapFont(gd::String &bitmapFontName) {
ExposeResourceAsFile(bitmapFontName);
}
void AssetResourcePathCleaner::ExposeResourceAsFile(gd::String &resourceName) {
auto &resource = resourcesManager->GetResource(resourceName);
gd::String file = resource.GetFile();
ExposeFile(file);
resourcesNameReverseMap[file] = resourceName;
resourceName = file;
}
void AssetResourcePathCleaner::ExposeFile(gd::String &resourceFilePath) {
size_t slashPos = resourceFilePath.find_last_of("/");
size_t antiSlashPos = resourceFilePath.find_last_of("\\");
size_t baseNamePos =
slashPos == String::npos
? antiSlashPos == String::npos ? 0 : (antiSlashPos + 1)
: antiSlashPos == String::npos ? (slashPos + 1)
: slashPos > antiSlashPos ? (slashPos + 1)
: (antiSlashPos + 1);
gd::String baseName =
baseNamePos != 0
? resourceFilePath.substr(baseNamePos, resourceFilePath.length())
: resourceFilePath;
resourcesFileNameMap[resourceFilePath] = baseName;
resourceFilePath = baseName;
}
} // namespace gd

View File

@@ -0,0 +1,65 @@
/*
* GDevelop Core
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#pragma once
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
#include "GDCore/IDE/Project/ResourcesMergingHelper.h"
#include "GDCore/String.h"
#include <map>
#include <memory>
#include <vector>
namespace gd {
class AbstractFileSystem;
class Project;
} // namespace gd
namespace gd {
/**
* \brief AssetResourcePathCleaner is used when exporting an object as an asset.
* It removes the folder from the path.
*
* \see ArbitraryResourceWorker
*
* \ingroup IDE
*/
class GD_CORE_API AssetResourcePathCleaner : public ArbitraryResourceWorker {
public:
AssetResourcePathCleaner(
gd::ResourcesManager &resourcesManager,
std::map<gd::String, gd::String> &resourcesFileNameMap_,
std::map<gd::String, gd::String> &resourcesNameReverseMap_)
: ArbitraryResourceWorker(resourcesManager),
resourcesFileNameMap(resourcesFileNameMap_),
resourcesNameReverseMap(resourcesNameReverseMap_){};
virtual ~AssetResourcePathCleaner(){};
void ExposeImage(gd::String &imageName) override;
void ExposeAudio(gd::String &audioName) override;
void ExposeFont(gd::String &fontName) override;
void ExposeJson(gd::String &jsonName) override;
void ExposeTilemap(gd::String &tilemapName) override;
void ExposeTileset(gd::String &tilesetName) override;
void ExposeVideo(gd::String &videoName) override;
void ExposeBitmapFont(gd::String &bitmapFontName) override;
void ExposeFile(gd::String &resource) override;
protected:
void ExposeResourceAsFile(gd::String &resourceName);
/**
* New file names that can be accessed by their original name.
*/
std::map<gd::String, gd::String> &resourcesFileNameMap;
/**
* Original resource names that can be accessed by their new name.
*/
std::map<gd::String, gd::String> &resourcesNameReverseMap;
};
} // namespace gd

View File

@@ -79,6 +79,12 @@ public:
virtual void ExposeModel3D(gd::String& otherResourceName) override {
MatchResourceName(otherResourceName);
};
virtual void ExposeAtlas(gd::String& otherResourceName) override {
MatchResourceName(otherResourceName);
};
virtual void ExposeSpine(gd::String& otherResourceName) override {
MatchResourceName(otherResourceName);
};
void MatchResourceName(gd::String& otherResourceName) {
if (otherResourceName == resourceName) matchesResourceName = true;

View File

@@ -3,9 +3,10 @@
* Copyright 2008-present Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#ifndef PROJECTRESOURCESCOPIER_H
#define PROJECTRESOURCESCOPIER_H
#pragma once
#include "GDCore/String.h"
namespace gd {
class Project;
class AbstractFileSystem;
@@ -47,6 +48,7 @@ class GD_CORE_API ProjectResourcesCopier {
bool updateOriginalProject,
bool preserveAbsoluteFilenames = true,
bool preserveDirectoryStructure = true);
private:
static bool CopyAllResourcesTo(gd::Project& originalProject,
gd::Project& clonedProject,
@@ -57,5 +59,3 @@ private:
};
} // namespace gd
#endif // PROJECTRESOURCESCOPIER_H

View File

@@ -0,0 +1,25 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#include "ResourcesInUseHelper.h"
namespace gd {
const std::vector<gd::String> ResourcesInUseHelper::resourceTypes = {
"image", "audio", "font", "json", "tilemap",
"tileset", "video", "bitmapFont", "model3D"};
const std::vector<gd::String> &ResourcesInUseHelper::GetAllResources() {
allResources.clear();
for (auto &&resourceType : gd::ResourcesInUseHelper::resourceTypes) {
for (auto &&resourceName : GetAll(resourceType)) {
allResources.push_back(resourceName);
}
}
return allResources;
}
} // namespace gd

View File

@@ -4,9 +4,7 @@
* reserved. This project is released under the MIT License.
*/
#if defined(GD_IDE_ONLY)
#ifndef IMAGESUSEDINVENTORIZER_H
#define IMAGESUSEDINVENTORIZER_H
#pragma once
#include <set>
#include <vector>
@@ -38,6 +36,7 @@ public:
: gd::ArbitraryResourceWorker(resourcesManager){};
virtual ~ResourcesInUseHelper(){};
const std::vector<gd::String>& GetAllResources();
std::set<gd::String>& GetAllImages() { return GetAll("image"); };
std::set<gd::String>& GetAllAudios() { return GetAll("audio"); };
std::set<gd::String>& GetAllFonts() { return GetAll("font"); };
@@ -47,6 +46,8 @@ public:
std::set<gd::String>& GetAllVideos() { return GetAll("video"); };
std::set<gd::String>& GetAllBitmapFonts() { return GetAll("bitmapFont"); };
std::set<gd::String>& GetAll3DModels() { return GetAll("model3D"); };
std::set<gd::String>& GetAllAtlases() { return GetAll("atlas"); };
std::set<gd::String>& GetAllSpines() { return GetAll("spine"); };
std::set<gd::String>& GetAll(const gd::String& resourceType) {
if (resourceType == "image") return allImages;
if (resourceType == "audio") return allAudios;
@@ -57,6 +58,8 @@ public:
if (resourceType == "video") return allVideos;
if (resourceType == "bitmapFont") return allBitmapFonts;
if (resourceType == "model3D") return allModel3Ds;
if (resourceType == "atlas") return allAtlases;
if (resourceType == "spine") return allSpines;
return emptyResources;
};
@@ -64,35 +67,42 @@ public:
virtual void ExposeFile(gd::String& resource) override{
/*Don't care, we just list resource names*/
};
virtual void ExposeImage(gd::String& imageResourceName) override {
allImages.insert(imageResourceName);
virtual void ExposeImage(gd::String& resourceName) override {
allImages.insert(resourceName);
};
virtual void ExposeAudio(gd::String& audioResourceName) override {
allAudios.insert(audioResourceName);
virtual void ExposeAudio(gd::String& resourceName) override {
allAudios.insert(resourceName);
};
virtual void ExposeFont(gd::String& fontResourceName) override {
allFonts.insert(fontResourceName);
virtual void ExposeFont(gd::String& resourceName) override {
allFonts.insert(resourceName);
};
virtual void ExposeJson(gd::String& jsonResourceName) override {
allJsons.insert(jsonResourceName);
virtual void ExposeJson(gd::String& resourceName) override {
allJsons.insert(resourceName);
};
virtual void ExposeTilemap(gd::String& tilemapResourceName) override {
allTilemaps.insert(tilemapResourceName);
virtual void ExposeTilemap(gd::String& resourceName) override {
allTilemaps.insert(resourceName);
};
virtual void ExposeTileset(gd::String& tilesetResourceName) override {
allTilesets.insert(tilesetResourceName);
virtual void ExposeTileset(gd::String& resourceName) override {
allTilesets.insert(resourceName);
};
virtual void ExposeVideo(gd::String& videoResourceName) override {
allVideos.insert(videoResourceName);
virtual void ExposeVideo(gd::String& resourceName) override {
allVideos.insert(resourceName);
};
virtual void ExposeBitmapFont(gd::String& bitmapFontResourceName) override {
allBitmapFonts.insert(bitmapFontResourceName);
virtual void ExposeBitmapFont(gd::String& resourceName) override {
allBitmapFonts.insert(resourceName);
};
virtual void ExposeModel3D(gd::String& resourceName) override {
allModel3Ds.insert(resourceName);
};
virtual void ExposeAtlas(gd::String& resourceName) override {
allAtlases.insert(resourceName);
};
virtual void ExposeSpine(gd::String& resourceName) override {
allSpines.insert(resourceName);
};
protected:
std::vector<gd::String> allResources;
std::set<gd::String> allImages;
std::set<gd::String> allAudios;
std::set<gd::String> allFonts;
@@ -102,10 +112,11 @@ public:
std::set<gd::String> allVideos;
std::set<gd::String> allBitmapFonts;
std::set<gd::String> allModel3Ds;
std::set<gd::String> allAtlases;
std::set<gd::String> allSpines;
std::set<gd::String> emptyResources;
static const std::vector<gd::String> resourceTypes;
};
} // namespace gd
#endif // IMAGESUSEDINVENTORIZER_H
#endif

View File

@@ -28,7 +28,7 @@ void ResourcesMergingHelper::ExposeFile(gd::String& resourceFilename) {
auto stripToFilenameOnly = [&]() {
fs.MakeAbsolute(resourceFullFilename, baseDirectory);
SetNewFilename(resourceFullFilename, fs.FileNameFrom(resourceFullFilename));
resourceFilename = oldFilenames[resourceFullFilename];
resourceFilename = newFilenames[resourceFullFilename];
};
// if we do not want to preserve the folders at all,
@@ -45,7 +45,7 @@ void ResourcesMergingHelper::ExposeFile(gd::String& resourceFilename) {
gd::String relativeFilename = resourceFullFilename;
if (fs.MakeRelative(relativeFilename, baseDirectory)) {
SetNewFilename(resourceFullFilename, relativeFilename);
resourceFilename = oldFilenames[resourceFullFilename];
resourceFilename = newFilenames[resourceFullFilename];
} else {
// The filename cannot be made relative. Consider that it is absolute.
// Just strip the filename to its file part
@@ -63,7 +63,7 @@ void ResourcesMergingHelper::ExposeFile(gd::String& resourceFilename) {
void ResourcesMergingHelper::SetNewFilename(gd::String oldFilename,
gd::String newFilename) {
if (oldFilenames.find(oldFilename) != oldFilenames.end()) return;
if (newFilenames.find(oldFilename) != newFilenames.end()) return;
// Extract baseName and extension from the new filename
size_t extensionPos = newFilename.find_last_of(".");
@@ -80,13 +80,13 @@ void ResourcesMergingHelper::SetNewFilename(gd::String oldFilename,
gd::NewNameGenerator::Generate(
baseName,
[this, extension](const gd::String& newBaseName) {
return newFilenames.find(newBaseName + extension) !=
newFilenames.end();
return oldFilenames.find(newBaseName + extension) !=
oldFilenames.end();
}) +
extension;
oldFilenames[oldFilename] = finalFilename;
newFilenames[finalFilename] = oldFilename;
newFilenames[oldFilename] = finalFilename;
oldFilenames[finalFilename] = oldFilename;
}
void ResourcesMergingHelper::SetBaseDirectory(

View File

@@ -64,19 +64,25 @@ public:
* the Base Directory.
*/
std::map<gd::String, gd::String>& GetAllResourcesOldAndNewFilename() {
return oldFilenames;
return newFilenames;
};
/**
* Resources merging helper collects all resources filenames and update these
* filenames.
*/
virtual void ExposeFile(gd::String& resource) override;
void ExposeFile(gd::String& resource) override;
protected:
void SetNewFilename(gd::String oldFilename, gd::String newFilename);
/**
* Original file names that can be accessed by their new name.
*/
std::map<gd::String, gd::String> oldFilenames;
/**
* New file names that can be accessed by their original name.
*/
std::map<gd::String, gd::String> newFilenames;
gd::String baseDirectory;
bool preserveDirectoriesStructure; ///< If set to true, the directory

View File

@@ -65,6 +65,12 @@ class ResourcesRenamer : public gd::ArbitraryResourceWorker {
virtual void ExposeModel3D(gd::String& resourceName) override {
RenameIfNeeded(resourceName);
};
virtual void ExposeAtlas(gd::String& resourceName) override {
RenameIfNeeded(resourceName);
};
virtual void ExposeSpine(gd::String& resourceName) override {
RenameIfNeeded(resourceName);
};
private:
void RenameIfNeeded(gd::String& resourceName) {

View File

@@ -80,6 +80,12 @@ private:
void ExposeModel3D(gd::String &resourceName) override {
AddUsedResource(resourceName);
};
void ExposeAtlas(gd::String &resourceName) override {
AddUsedResource(resourceName);
};
void ExposeSpine(gd::String &resourceName) override {
AddUsedResource(resourceName);
};
std::set<gd::String> resourceNames;
};

View File

@@ -16,8 +16,8 @@
#include "GDCore/IDE/Events/BehaviorTypeRenamer.h"
#include "GDCore/IDE/Events/CustomObjectTypeRenamer.h"
#include "GDCore/IDE/Events/EventsBehaviorRenamer.h"
#include "GDCore/IDE/Events/EventsRefactorer.h"
#include "GDCore/IDE/Events/EventsPropertyReplacer.h"
#include "GDCore/IDE/Events/EventsRefactorer.h"
#include "GDCore/IDE/Events/EventsVariableReplacer.h"
#include "GDCore/IDE/Events/ExpressionsParameterMover.h"
#include "GDCore/IDE/Events/ExpressionsRenamer.h"
@@ -138,7 +138,8 @@ void WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
}
}
VariablesChangeset WholeProjectRefactorer::ComputeChangesetForVariablesContainer(
VariablesChangeset
WholeProjectRefactorer::ComputeChangesetForVariablesContainer(
gd::Project &project,
const gd::SerializerElement &oldSerializedVariablesContainer,
const gd::VariablesContainer &newVariablesContainer) {
@@ -149,9 +150,9 @@ VariablesChangeset WholeProjectRefactorer::ComputeChangesetForVariablesContainer
if (oldVariablesContainer.GetPersistentUuid() !=
newVariablesContainer.GetPersistentUuid()) {
gd::LogWarning(
_("Called ComputeChangesetForVariablesContainer on variables containers "
"that are different - they can't be compared."));
gd::LogWarning(_(
"Called ComputeChangesetForVariablesContainer on variables containers "
"that are different - they can't be compared."));
return changeset;
}
@@ -192,14 +193,11 @@ VariablesChangeset WholeProjectRefactorer::ComputeChangesetForVariablesContainer
}
void WholeProjectRefactorer::ApplyRefactoringForVariablesContainer(
gd::Project &project,
const gd::VariablesContainer &newVariablesContainer,
const gd::VariablesChangeset& changeset) {
gd::Project &project, const gd::VariablesContainer &newVariablesContainer,
const gd::VariablesChangeset &changeset) {
gd::EventsVariableReplacer eventsVariableReplacer(
project.GetCurrentPlatform(),
newVariablesContainer,
changeset.oldToNewVariableNames,
changeset.removedVariableNames);
project.GetCurrentPlatform(), newVariablesContainer,
changeset.oldToNewVariableNames, changeset.removedVariableNames);
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
eventsVariableReplacer);
}
@@ -743,14 +741,14 @@ void WholeProjectRefactorer::RenameEventsBasedBehaviorProperty(
EventsBasedBehavior::GetPropertyExpressionName(newPropertyName));
gd::ProjectBrowserHelper::ExposeProjectEvents(project, expressionRenamer);
std::unordered_map<gd::String, gd::String> oldToNewPropertyNames = {{oldPropertyName, newPropertyName}};
std::unordered_map<gd::String, gd::String> oldToNewPropertyNames = {
{oldPropertyName, newPropertyName}};
std::unordered_set<gd::String> removedPropertyNames;
gd::EventsPropertyReplacer eventsPropertyReplacer(
project.GetCurrentPlatform(),
properties,
oldToNewPropertyNames,
removedPropertyNames);
gd::ProjectBrowserHelper::ExposeProjectEvents(project, eventsPropertyReplacer);
project.GetCurrentPlatform(), properties, oldToNewPropertyNames,
removedPropertyNames);
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
eventsPropertyReplacer);
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
@@ -813,14 +811,14 @@ void WholeProjectRefactorer::RenameEventsBasedBehaviorSharedProperty(
EventsBasedBehavior::GetSharedPropertyExpressionName(newPropertyName));
gd::ProjectBrowserHelper::ExposeProjectEvents(project, expressionRenamer);
std::unordered_map<gd::String, gd::String> oldToNewPropertyNames = {{oldPropertyName, newPropertyName}};
std::unordered_map<gd::String, gd::String> oldToNewPropertyNames = {
{oldPropertyName, newPropertyName}};
std::unordered_set<gd::String> removedPropertyNames;
gd::EventsPropertyReplacer eventsPropertyReplacer(
project.GetCurrentPlatform(),
properties,
oldToNewPropertyNames,
removedPropertyNames);
gd::ProjectBrowserHelper::ExposeProjectEvents(project, eventsPropertyReplacer);
project.GetCurrentPlatform(), properties, oldToNewPropertyNames,
removedPropertyNames);
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
eventsPropertyReplacer);
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
@@ -870,14 +868,14 @@ void WholeProjectRefactorer::RenameEventsBasedObjectProperty(
EventsBasedObject::GetPropertyExpressionName(newPropertyName));
gd::ProjectBrowserHelper::ExposeProjectEvents(project, expressionRenamer);
std::unordered_map<gd::String, gd::String> oldToNewPropertyNames = {{oldPropertyName, newPropertyName}};
std::unordered_map<gd::String, gd::String> oldToNewPropertyNames = {
{oldPropertyName, newPropertyName}};
std::unordered_set<gd::String> removedPropertyNames;
gd::EventsPropertyReplacer eventsPropertyReplacer(
project.GetCurrentPlatform(),
properties,
oldToNewPropertyNames,
removedPropertyNames);
gd::ProjectBrowserHelper::ExposeProjectEvents(project, eventsPropertyReplacer);
project.GetCurrentPlatform(), properties, oldToNewPropertyNames,
removedPropertyNames);
gd::ProjectBrowserHelper::ExposeProjectEvents(project,
eventsPropertyReplacer);
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
@@ -1351,7 +1349,6 @@ void WholeProjectRefactorer::DoRenameBehavior(
gd::Project &project, const gd::String &oldBehaviorType,
const gd::String &newBehaviorType,
const gd::ProjectBrowser &projectBrowser) {
// Rename behavior in required behavior properties
auto requiredBehaviorRenamer =
gd::RequiredBehaviorRenamer(oldBehaviorType, newBehaviorType);
@@ -1378,7 +1375,6 @@ void WholeProjectRefactorer::DoRenameBehavior(
void WholeProjectRefactorer::DoRenameObject(
gd::Project &project, const gd::String &oldObjectType,
const gd::String &newObjectType, const gd::ProjectBrowser &projectBrowser) {
// Rename object type in objects lists.
auto customObjectTypeRenamer =
gd::CustomObjectTypeRenamer(oldObjectType, newObjectType);
@@ -1398,7 +1394,8 @@ void WholeProjectRefactorer::DoRenameObject(
void WholeProjectRefactorer::ObjectOrGroupRemovedInLayout(
gd::Project &project, gd::Layout &layout, const gd::String &objectName,
bool isObjectGroup, bool removeEventsAndGroups) {
auto projectScopedContainers = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout);
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForProjectAndLayout(project, layout);
// Remove object in the current layout
if (removeEventsAndGroups) {
@@ -1447,7 +1444,8 @@ void WholeProjectRefactorer::ObjectOrGroupRenamedInLayout(
if (oldName == newName || newName.empty() || oldName.empty())
return;
auto projectScopedContainers = gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project, layout);
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForProjectAndLayout(project, layout);
// Rename object in the current layout
gd::EventsRefactorer::RenameObjectInEvents(
@@ -1536,10 +1534,19 @@ void WholeProjectRefactorer::RenameLayer(gd::Project &project,
const gd::String &newName) {
if (oldName == newName || newName.empty() || oldName.empty())
return;
gd::ProjectElementRenamer projectElementRenamer(project.GetCurrentPlatform(),
"layer", oldName, newName);
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(project, layout,
projectElementRenamer);
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
layout.GetInitialInstances().MoveInstancesToLayer(oldName, newName);
std::vector<gd::String> externalLayoutsNames =
GetAssociatedExternalLayouts(project, layout);
for (gd::String name : externalLayoutsNames) {
auto &externalLayout = project.GetExternalLayout(name);
externalLayout.GetInitialInstances().MoveInstancesToLayer(oldName, newName);
}
}
void WholeProjectRefactorer::RenameLayerEffect(gd::Project &project,
@@ -1552,8 +1559,8 @@ void WholeProjectRefactorer::RenameLayerEffect(gd::Project &project,
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "layerEffectName", oldName, newName);
projectElementRenamer.SetLayerConstraint(layer.GetName());
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(project, layout,
projectElementRenamer);
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectAnimation(gd::Project &project,
@@ -1566,8 +1573,8 @@ void WholeProjectRefactorer::RenameObjectAnimation(gd::Project &project,
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectAnimationName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(project, layout,
projectElementRenamer);
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectPoint(gd::Project &project,
@@ -1580,8 +1587,8 @@ void WholeProjectRefactorer::RenameObjectPoint(gd::Project &project,
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectPointName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(project, layout,
projectElementRenamer);
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
}
void WholeProjectRefactorer::RenameObjectEffect(gd::Project &project,
@@ -1594,8 +1601,8 @@ void WholeProjectRefactorer::RenameObjectEffect(gd::Project &project,
gd::ProjectElementRenamer projectElementRenamer(
project.GetCurrentPlatform(), "objectEffectName", oldName, newName);
projectElementRenamer.SetObjectConstraint(object.GetName());
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(project, layout,
projectElementRenamer);
gd::ProjectBrowserHelper::ExposeLayoutEventsAndExternalEvents(
project, layout, projectElementRenamer);
}
void WholeProjectRefactorer::ObjectOrGroupRemovedInEventsBasedObject(
@@ -1617,9 +1624,12 @@ void WholeProjectRefactorer::ObjectOrGroupRemovedInEventsFunction(
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer, const gd::String &objectName,
bool isObjectGroup, bool removeEventsAndGroups) {
// In theory we should pass a ProjectScopedContainers to this function so it does not have to construct one.
// In practice, this is ok because we only deal with objects.
auto projectScopedContainers = gd::ProjectScopedContainers::MakeNewProjectScopedContainersFor(globalObjectsContainer, objectsContainer);
// In theory we should pass a ProjectScopedContainers to this function so it
// does not have to construct one. In practice, this is ok because we only
// deal with objects.
auto projectScopedContainers =
gd::ProjectScopedContainers::MakeNewProjectScopedContainersFor(
globalObjectsContainer, objectsContainer);
if (removeEventsAndGroups) {
gd::EventsRefactorer::RemoveObjectInEvents(
@@ -1655,9 +1665,12 @@ void WholeProjectRefactorer::ObjectOrGroupRenamedInEventsFunction(
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer, const gd::String &oldName,
const gd::String &newName, bool isObjectGroup) {
// In theory we should pass a ProjectScopedContainers to this function so it does not have to construct one.
// In practice, this is ok because we only deal with objects.
auto projectScopedContainers = gd::ProjectScopedContainers::MakeNewProjectScopedContainersFor(globalObjectsContainer, objectsContainer);
// In theory we should pass a ProjectScopedContainers to this function so it
// does not have to construct one. In practice, this is ok because we only
// deal with objects.
auto projectScopedContainers =
gd::ProjectScopedContainers::MakeNewProjectScopedContainersFor(
globalObjectsContainer, objectsContainer);
gd::EventsRefactorer::RenameObjectInEvents(
project.GetCurrentPlatform(), projectScopedContainers,
@@ -1705,7 +1718,7 @@ void WholeProjectRefactorer::GlobalObjectOrGroupRemoved(
if (layout.HasObjectNamed(objectName))
continue;
ObjectOrGroupRemovedInLayout(project, layout, objectName, isObjectGroup,
ObjectOrGroupRemovedInLayout(project, layout, objectName, isObjectGroup,
removeEventsAndGroups);
}
}
@@ -1753,7 +1766,8 @@ size_t WholeProjectRefactorer::GetLayoutAndExternalLayoutLayerInstancesCount(
GetAssociatedExternalLayouts(project, layout);
for (gd::String name : externalLayoutsNames) {
auto &externalLayout = project.GetExternalLayout(name);
count += externalLayout.GetInitialInstances().GetLayerInstancesCount(layerName);
count +=
externalLayout.GetInitialInstances().GetLayerInstancesCount(layerName);
}
return count;
}

View File

@@ -4,8 +4,6 @@
* reserved. This project is released under the MIT License.
*/
#include "GDCore/Project/Behavior.h"
#include <iostream>
#include "GDCore/Project/PropertyDescriptor.h"
namespace gd {

View File

@@ -155,6 +155,10 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor
worker.ExposeBitmapFont(newPropertyValue);
} else if (resourceType == "model3D") {
worker.ExposeModel3D(newPropertyValue);
} else if (resourceType == "atlas") {
worker.ExposeAtlas(newPropertyValue);
} else if (resourceType == "spine") {
worker.ExposeSpine(newPropertyValue);
}
if (newPropertyValue != oldPropertyValue) {

View File

@@ -13,7 +13,8 @@ EventsBasedObject::EventsBasedObject()
: AbstractEventsBasedEntity(
"MyObject",
gd::EventsFunctionsContainer::FunctionOwner::Object),
ObjectsContainer() {
ObjectsContainer(),
isRenderedIn3D(false) {
}
EventsBasedObject::~EventsBasedObject() {}

View File

@@ -39,6 +39,7 @@ void Layer::SetCameraCount(std::size_t n) {
void Layer::SerializeTo(SerializerElement& element) const {
element.SetAttribute("name", GetName());
element.SetAttribute("renderingType", GetRenderingType());
element.SetAttribute("cameraType", GetCameraType());
element.SetAttribute("visibility", GetVisibility());
element.SetAttribute("isLocked", IsLocked());
element.SetAttribute("isLightingLayer", IsLightingLayer());
@@ -78,6 +79,7 @@ void Layer::SerializeTo(SerializerElement& element) const {
void Layer::UnserializeFrom(const SerializerElement& element) {
SetName(element.GetStringAttribute("name", "", "Name"));
SetRenderingType(element.GetStringAttribute("renderingType", ""));
SetCameraType(element.GetStringAttribute("cameraType", "perspective"));
SetVisibility(element.GetBoolAttribute("visibility", true, "Visibility"));
SetLocked(element.GetBoolAttribute("isLocked", false));
SetLightingLayer(element.GetBoolAttribute("isLightingLayer", false));

View File

@@ -104,10 +104,17 @@ class GD_CORE_API Layer {
const gd::String& GetName() const { return name; }
const gd::String& GetRenderingType() const { return renderingType; }
void SetRenderingType(const gd::String& renderingType_) {
renderingType = renderingType_;
}
const gd::String& GetCameraType() const { return cameraType; }
void SetCameraType(const gd::String& cameraType_) {
cameraType = cameraType_;
}
/**
* \brief Change if layer is displayed or not
*/
@@ -268,6 +275,7 @@ class GD_CORE_API Layer {
gd::String name; ///< The name of the layer
gd::String renderingType; ///< The rendering type: "" (empty), "2d", "3d" or
///< "2d+3d".
gd::String cameraType;
bool isVisible; ///< True if the layer is visible
bool isLocked; ///< True if the layer is locked
bool isLightingLayer; ///< True if the layer is used to display lights and

View File

@@ -120,9 +120,6 @@ class GD_CORE_API Object {
*/
const gd::String& GetType() const { return configuration->GetType(); }
/** \brief Shortcut to check if the object is a 3D object.
*/
bool Is3DObject() const { return configuration->Is3DObject(); }
///@}
/** \name Behaviors management

View File

@@ -5,11 +5,8 @@
*/
#include "GDCore/Project/ObjectConfiguration.h"
#include "GDCore/Extensions/Metadata/BehaviorMetadata.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Project/Behavior.h"
#include "GDCore/Project/CustomBehavior.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Serialization/SerializerElement.h"
@@ -20,7 +17,7 @@ namespace gd {
ObjectConfiguration::~ObjectConfiguration() {}
ObjectConfiguration::ObjectConfiguration(): is3DObject(false) {}
ObjectConfiguration::ObjectConfiguration() {}
std::map<gd::String, gd::PropertyDescriptor> ObjectConfiguration::GetProperties() const {
std::map<gd::String, gd::PropertyDescriptor> nothing;

View File

@@ -63,20 +63,12 @@ class GD_CORE_API ObjectConfiguration {
*/
void SetType(const gd::String& type_) {
type = type_;
// For now, as a shortcut, consider only the objects from the built-in 3D extension
// to be 3D object.
is3DObject = type.find("Scene3D::") == 0;
}
/** \brief Return the type of the object.
*/
const gd::String& GetType() const { return type; }
/** \brief Shortcut to check if the object is a 3D object.
*/
bool Is3DObject() const { return is3DObject; }
/** \name Object properties
* Reading and updating object configuration properties
*/
@@ -180,7 +172,6 @@ class GD_CORE_API ObjectConfiguration {
protected:
gd::String type; ///< Which type of object is represented by this
///< configuration.
bool is3DObject;
/**
* \brief Derived object configuration can redefine this method to load

View File

@@ -102,11 +102,20 @@ std::unique_ptr<gd::Object> Project::CreateObject(
behavior->SetDefaultBehavior(true);
};
if (Project::HasEventsBasedObject(objectType)) {
if (project.HasEventsBasedObject(objectType)) {
// During project deserialization, event-based object metadata are not yet
// generated.
addDefaultBehavior("EffectCapability::EffectBehavior");
addDefaultBehavior("ResizableCapability::ResizableBehavior");
addDefaultBehavior("ScalableCapability::ScalableBehavior");
addDefaultBehavior("FlippableCapability::FlippableBehavior");
auto& eventBasedObject = project.GetEventsBasedObject(objectType);
if (eventBasedObject.IsRenderedIn3D()) {
addDefaultBehavior("Scene3D::Base3DBehavior");
}
else {
addDefaultBehavior("OpacityCapability::OpacityBehavior");
}
} else {
auto& objectMetadata =
gd::MetadataProvider::GetObjectMetadata(platform, objectType);

View File

@@ -29,7 +29,15 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
for (const gd::String& information : extraInformation) {
extraInformationElement.AddChild("").SetStringValue(information);
}
element.AddChild("hidden").SetBoolValue(hidden);
if (hidden) {
element.AddChild("hidden").SetBoolValue(hidden);
}
if (deprecated) {
element.AddChild("deprecated").SetBoolValue(deprecated);
}
if (advanced) {
element.AddChild("advanced").SetBoolValue(advanced);
}
}
void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
@@ -58,6 +66,12 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
hidden = element.HasChild("hidden")
? element.GetChild("hidden").GetBoolValue()
: false;
deprecated = element.HasChild("deprecated")
? element.GetChild("deprecated").GetBoolValue()
: false;
advanced = element.HasChild("advanced")
? element.GetChild("advanced").GetBoolValue()
: false;
}
void PropertyDescriptor::SerializeValuesTo(SerializerElement& element) const {

View File

@@ -30,12 +30,16 @@ class GD_CORE_API PropertyDescriptor {
* \param propertyValue The value of the property.
*/
PropertyDescriptor(gd::String propertyValue)
: currentValue(propertyValue), type("string"), label(""), hidden(false), measurementUnit(gd::MeasurementUnit::GetUndefined()) {}
: currentValue(propertyValue), type("string"), label(""), hidden(false),
deprecated(false), advanced(false),
measurementUnit(gd::MeasurementUnit::GetUndefined()) {}
/**
* \brief Empty constructor creating an empty property to be displayed.
*/
PropertyDescriptor() : hidden(false), measurementUnit(gd::MeasurementUnit::GetUndefined()) {};
PropertyDescriptor()
: hidden(false), deprecated(false), advanced(false),
measurementUnit(gd::MeasurementUnit::GetUndefined()){};
/**
* \brief Destructor
@@ -142,6 +146,32 @@ class GD_CORE_API PropertyDescriptor {
*/
bool IsHidden() const { return hidden; }
/**
* \brief Set if the property is deprecated.
*/
PropertyDescriptor& SetDeprecated(bool enable = true) {
deprecated = enable;
return *this;
}
/**
* \brief Check if the property is deprecated.
*/
bool IsDeprecated() const { return deprecated; }
/**
* \brief Set if the property is marked as advanced.
*/
PropertyDescriptor& SetAdvanced(bool enable = true) {
advanced = enable;
return *this;
}
/**
* \brief Check if the property is marked as advanced.
*/
bool IsAdvanced() const { return advanced; }
/** \name Serialization
*/
///@{
@@ -179,6 +209,8 @@ class GD_CORE_API PropertyDescriptor {
///< choices, if a property is a displayed as a combo
///< box.
bool hidden;
bool deprecated;
bool advanced;
gd::MeasurementUnit measurementUnit; //< The unit of measurement of the property vale.
};

View File

@@ -93,6 +93,10 @@ std::shared_ptr<Resource> ResourcesManager::CreateResource(
return std::make_shared<BitmapFontResource>();
else if (kind == "model3D")
return std::make_shared<Model3DResource>();
else if (kind == "atlas")
return std::make_shared<AtlasResource>();
else if (kind == "spine")
return std::make_shared<SpineResource>();
std::cout << "Bad resource created (type: " << kind << ")" << std::endl;
return std::make_shared<Resource>();
@@ -756,6 +760,20 @@ void Model3DResource::SerializeTo(SerializerElement& element) const {
element.SetAttribute("file", GetFile());
}
void AtlasResource::SetFile(const gd::String& newFile) {
file = NormalizePathSeparator(newFile);
}
void AtlasResource::UnserializeFrom(const SerializerElement& element) {
SetUserAdded(element.GetBoolAttribute("userAdded"));
SetFile(element.GetStringAttribute("file"));
}
void AtlasResource::SerializeTo(SerializerElement& element) const {
element.SetAttribute("userAdded", IsUserAdded());
element.SetAttribute("file", GetFile());
}
ResourceFolder::ResourceFolder(const ResourceFolder& other) { Init(other); }
ResourceFolder& ResourceFolder::operator=(const ResourceFolder& other) {

View File

@@ -373,6 +373,21 @@ class GD_CORE_API JsonResource : public Resource {
gd::String file;
};
/**
* \brief Describe a spine json file used by a project.
*
* \see Resource
* \ingroup ResourcesManagement
*/
class GD_CORE_API SpineResource : public JsonResource {
public:
SpineResource() : JsonResource() { SetKind("spine"); };
virtual ~SpineResource(){};
virtual SpineResource* Clone() const override {
return new SpineResource(*this);
}
};
/**
* \brief Describe a tilemap file used by a project.
*
@@ -507,6 +522,32 @@ class GD_CORE_API Model3DResource : public Resource {
gd::String file;
};
/**
* \brief Describe an atlas file used by a project.
*
* \see Resource
* \ingroup ResourcesManagement
*/
class GD_CORE_API AtlasResource : public Resource {
public:
AtlasResource() : Resource() { SetKind("atlas"); };
virtual ~AtlasResource(){};
virtual AtlasResource* Clone() const override {
return new AtlasResource(*this);
}
virtual const gd::String& GetFile() const override { return file; };
virtual void SetFile(const gd::String& newFile) override;
virtual bool UseFile() const override { return true; }
void SerializeTo(SerializerElement& element) const override;
void UnserializeFrom(const SerializerElement& element) override;
private:
gd::String file;
};
/**
* \brief Inventory all resources used by a project
*

View File

@@ -489,7 +489,7 @@ void SetupProjectWithDummyPlatform(gd::Project& project,
"Effect",
_("Apply visual effects to objects."),
"",
"res/actions/effect24.png", "EffectBehavior",
"res/actions/effect_black.svg", "EffectBehavior",
std::make_shared<gd::Behavior>(),
std::make_shared<gd::BehaviorsSharedData>())
.SetHidden();

View File

@@ -203,6 +203,23 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
auto node = parser.ParseExpression("abcd[0]");
REQUIRE(node != nullptr);
// Also check that if we try to find the last parent of node, it is not defined.
auto lastParentOfNode = gd::ExpressionVariableParentFinder::GetLastParentOfNode(
platform, projectScopedContainers, *node);
REQUIRE(lastParentOfNode.parentVariable == nullptr);
REQUIRE(lastParentOfNode.parentVariablesContainer == nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"No object, variable or property with this name found.");
REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 0);
}
{
auto node = parser.ParseExpression("abcd[0].efg");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
@@ -214,6 +231,12 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
auto node = parser.ParseExpression("abcd.efg.hij");
REQUIRE(node != nullptr);
// Also check that if we try to find the last parent of node, it is not defined.
auto lastParentOfNode = gd::ExpressionVariableParentFinder::GetLastParentOfNode(
platform, projectScopedContainers, *node);
REQUIRE(lastParentOfNode.parentVariable == nullptr);
REQUIRE(lastParentOfNode.parentVariablesContainer == nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
@@ -762,6 +785,12 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
auto node = parser.ParseExpression("abcd.efg.hij");
REQUIRE(node != nullptr);
// Also check that if we try to find the last parent of node, it is not defined.
auto lastParentOfNode = gd::ExpressionVariableParentFinder::GetLastParentOfNode(
platform, projectScopedContainers, *node);
REQUIRE(lastParentOfNode.parentVariable == nullptr);
REQUIRE(lastParentOfNode.parentVariablesContainer == nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
@@ -1495,6 +1524,12 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
auto node =
parser.ParseExpression("MyNonExistingSceneVariable");
// Also check that if we try to find the last parent of node, it is not defined.
auto lastParentOfNode = gd::ExpressionVariableParentFinder::GetLastParentOfNode(
platform, projectScopedContainers, *node);
REQUIRE(lastParentOfNode.parentVariable == nullptr);
REQUIRE(lastParentOfNode.parentVariablesContainer == nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
@@ -1516,6 +1551,25 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
}
}
SECTION("Invalid scene variables (2 levels, variable and child do not exist)") {
{
auto node =
parser.ParseExpression("MyNonExistingSceneVariable.MyNonExistingChild");
// Also check that if we try to find the last parent of node, it is not defined.
auto lastParentOfNode = gd::ExpressionVariableParentFinder::GetLastParentOfNode(
platform, projectScopedContainers, *node);
REQUIRE(lastParentOfNode.parentVariable == nullptr);
REQUIRE(lastParentOfNode.parentVariablesContainer == nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"You must enter a number or a text, wrapped inside double quotes (example: \"Hello world\"), or a variable name.");
}
}
SECTION("Valid object variables (1 level)") {
{
auto node =
@@ -1586,6 +1640,12 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
auto node =
parser.ParseExpression("MyNonExistingSpriteObject.MyVariable");
// Also check that if we try to find the last parent of node, it is not defined.
auto lastParentOfNode = gd::ExpressionVariableParentFinder::GetLastParentOfNode(
platform, projectScopedContainers, *node);
REQUIRE(lastParentOfNode.parentVariable == nullptr);
REQUIRE(lastParentOfNode.parentVariablesContainer == nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 1);

View File

@@ -0,0 +1,147 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
/**
* @file Tests covering common features of GDevelop Core.
*/
#include "GDCore/IDE/ObjectAssetSerializer.h"
#include "DummyPlatform.h"
#include "GDCore/CommonTools.h"
#include "GDCore/Events/Builtin/StandardEvent.h"
#include "GDCore/Events/Event.h"
#include "GDCore/Events/EventsList.h"
#include "GDCore/Events/Serialization.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteObject.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Project/CustomObjectConfiguration.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/ObjectsContainer.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/Variable.h"
#include "GDCore/Serialization/Serializer.h"
#include "GDCore/Tools/SystemStats.h"
#include "GDCore/Tools/VersionWrapper.h"
#include "catch.hpp"
#include <string>
using namespace gd;
TEST_CASE("ObjectAssetSerializer", "[common]") {
SECTION("Can serialize custom objects as assets") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
"MyEventsBasedObject", 0);
eventsBasedObject.SetFullName("My events based object");
eventsBasedObject.SetDescription("An events based object for test");
eventsBasedObject.InsertNewObject(project, "MyExtension::Sprite", "MyChild",
0);
auto &resourceManager = project.GetResourcesManager();
gd::ImageResource imageResource;
imageResource.SetName("assets/Idle.png");
imageResource.SetFile("assets/Idle.png");
imageResource.SetSmooth(true);
resourceManager.AddResource(imageResource);
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &object = layout.InsertNewObject(
project, "MyEventsExtension::MyEventsBasedObject", "MyObject", 0);
auto &configuration = object.GetConfiguration();
auto *customObjectConfiguration =
dynamic_cast<gd::CustomObjectConfiguration *>(&configuration);
auto *spriteConfiguration = dynamic_cast<gd::SpriteObject *>(
&customObjectConfiguration->GetChildObjectConfiguration("MyChild"));
REQUIRE(spriteConfiguration != nullptr);
{
gd::Animation animation;
animation.SetName("Idle");
animation.SetDirectionsCount(1);
auto &direction = animation.GetDirection(0);
gd::Sprite frame;
frame.SetImageName("assets/Idle.png");
direction.AddSprite(frame);
spriteConfiguration->AddAnimation(animation);
}
SerializerElement assetElement;
std::vector<gd::String> usedResourceNames;
ObjectAssetSerializer::SerializeTo(project, object, "My Object",
assetElement, usedResourceNames);
// This list is used to copy resource files.
REQUIRE(usedResourceNames.size() == 1);
REQUIRE(usedResourceNames[0] == "assets/Idle.png");
// Check that the project is left untouched.
REQUIRE(resourceManager.HasResource("assets/Idle.png"));
REQUIRE(resourceManager.GetResource("assets/Idle.png").GetFile() ==
"assets/Idle.png");
REQUIRE(!resourceManager.HasResource("Idle.png"));
REQUIRE(assetElement.HasChild("objectAssets"));
auto &objectAssetsElement = assetElement.GetChild("objectAssets");
objectAssetsElement.ConsiderAsArrayOf("objectAsset");
REQUIRE(objectAssetsElement.GetChildrenCount() == 1);
auto &objectAssetElement = objectAssetsElement.GetChild(0);
REQUIRE(objectAssetElement.HasChild("requiredExtensions"));
auto &requiredExtensionsElement =
objectAssetElement.GetChild("requiredExtensions");
requiredExtensionsElement.ConsiderAsArrayOf("requiredExtension");
REQUIRE(requiredExtensionsElement.GetChildrenCount() == 1);
auto &requiredExtensionElement = requiredExtensionsElement.GetChild(0);
REQUIRE(requiredExtensionElement.GetStringAttribute("extensionName") ==
"MyEventsExtension");
// Resources are renamed according to asset script naming conventions.
REQUIRE(objectAssetElement.HasChild("resources"));
auto &resourcesElement = objectAssetElement.GetChild("resources");
resourcesElement.ConsiderAsArrayOf("resource");
REQUIRE(resourcesElement.GetChildrenCount() == 1);
{
auto &resourceElement = resourcesElement.GetChild(0);
REQUIRE(resourceElement.GetStringAttribute("name") == "assets/Idle.png");
REQUIRE(resourceElement.GetStringAttribute("file") == "assets/Idle.png");
REQUIRE(resourceElement.GetStringAttribute("kind") == "image");
REQUIRE(resourceElement.GetBoolAttribute("smoothed") == true);
}
// Resources used in object configuration are updated.
REQUIRE(objectAssetElement.HasChild("object"));
auto &objectElement = objectAssetElement.GetChild("object");
REQUIRE(objectElement.GetStringAttribute("name") == "MyObject");
REQUIRE(objectElement.GetStringAttribute("type") ==
"MyEventsExtension::MyEventsBasedObject");
auto &childrenContentElement = objectElement.GetChild("childrenContent");
REQUIRE(childrenContentElement.HasChild("MyChild"));
auto &childElement = childrenContentElement.GetChild("MyChild");
REQUIRE(childElement.HasChild("animations"));
auto &animationsElement = childElement.GetChild("animations");
animationsElement.ConsiderAsArrayOf("animation");
REQUIRE(animationsElement.GetChildrenCount() == 1);
auto &animationElement = animationsElement.GetChild(0);
REQUIRE(animationElement.GetStringAttribute("name") == "Idle");
auto &directionsElement = animationElement.GetChild("directions");
directionsElement.ConsiderAsArrayOf("direction");
REQUIRE(directionsElement.GetChildrenCount() == 1);
auto &directionElement = directionsElement.GetChild(0);
auto &spritesElement = directionElement.GetChild("sprites");
spritesElement.ConsiderAsArrayOf("sprite");
REQUIRE(spritesElement.GetChildrenCount() == 1);
auto &spriteElement = spritesElement.GetChild(0);
REQUIRE(spriteElement.GetStringAttribute("image") == "assets/Idle.png");
}
}

View File

@@ -3474,6 +3474,72 @@ TEST_CASE("RenameLayer", "[common]") {
"MyExtension::CameraCenterX(\"layerA\")");
}
SECTION("Renaming a layer also moves the instances on this layer and of the associated external layouts") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &layout = project.InsertNewLayout("My layout", 0);
auto &otherLayout = project.InsertNewLayout("My other layout", 1);
layout.InsertNewLayer("My layer", 0);
otherLayout.InsertNewLayer("My layer", 0);
auto &externalLayout =
project.InsertNewExternalLayout("My external layout", 0);
auto &otherExternalLayout =
project.InsertNewExternalLayout("My other external layout", 0);
externalLayout.SetAssociatedLayout("My layout");
otherExternalLayout.SetAssociatedLayout("My other layout");
auto &initialInstances = layout.GetInitialInstances();
auto &initialInstance1 = initialInstances.InsertNewInitialInstance();
initialInstance1.SetLayer("My layer");
auto &initialInstance2 = initialInstances.InsertNewInitialInstance();
initialInstance2.SetLayer("My layer");
auto &initialInstance3 = initialInstances.InsertNewInitialInstance();
initialInstance3.SetLayer("");
auto &externalInitialInstances = externalLayout.GetInitialInstances();
auto &externalInitialInstance1 = externalInitialInstances.InsertNewInitialInstance();
externalInitialInstance1.SetLayer("My layer");
auto &externalInitialInstance2 = externalInitialInstances.InsertNewInitialInstance();
externalInitialInstance2.SetLayer("My layer");
auto &externalInitialInstance3 = externalInitialInstances.InsertNewInitialInstance();
externalInitialInstance3.SetLayer("");
auto &otherInitialInstances = otherLayout.GetInitialInstances();
auto &otherInitialInstance1 = otherInitialInstances.InsertNewInitialInstance();
otherInitialInstance1.SetLayer("My layer");
auto &otherExternalInitialInstances = otherExternalLayout.GetInitialInstances();
auto &otherExternalInitialInstance1 = otherExternalInitialInstances.InsertNewInitialInstance();
otherExternalInitialInstance1.SetLayer("My layer");
REQUIRE(initialInstance1.GetLayer() == "My layer");
REQUIRE(initialInstance2.GetLayer() == "My layer");
REQUIRE(initialInstance3.GetLayer() == "");
REQUIRE(externalInitialInstance1.GetLayer() == "My layer");
REQUIRE(externalInitialInstance2.GetLayer() == "My layer");
REQUIRE(externalInitialInstance3.GetLayer() == "");
REQUIRE(otherInitialInstance1.GetLayer() == "My layer");
REQUIRE(otherExternalInitialInstance1.GetLayer() == "My layer");
gd::WholeProjectRefactorer::RenameLayer(project, layout, "My layer", "My new layer");
// Instances on the renamed layer are moved to the new layer.
REQUIRE(initialInstance1.GetLayer() == "My new layer");
REQUIRE(initialInstance2.GetLayer() == "My new layer");
REQUIRE(initialInstance3.GetLayer() == "");
// Instances on the renamed layer of external layouts are moved to the new layer.
REQUIRE(externalInitialInstance1.GetLayer() == "My new layer");
REQUIRE(externalInitialInstance2.GetLayer() == "My new layer");
REQUIRE(externalInitialInstance3.GetLayer() == "");
// Instances on the renamed layer of other layouts & external layouts are not moved.
REQUIRE(otherInitialInstance1.GetLayer() == "My layer");
REQUIRE(otherExternalInitialInstance1.GetLayer() == "My layer");
}
SECTION("Can rename a layer when a layer parameter is empty") {
gd::Project project;
gd::Platform platform;

View File

@@ -154,7 +154,23 @@ namespace gdjs {
* @return The Z position of the rendered object.
*/
getDrawableZ(): float {
return this.getZ();
return this._z;
}
/**
* Return the bottom Z of the object.
* Rotations around X and Y are not taken into account.
*/
getUnrotatedAABBMinZ(): number {
return this.getDrawableZ();
}
/**
* Return the top Z of the object.
* Rotations around X and Y are not taken into account.
*/
getUnrotatedAABBMaxZ(): number {
return this.getDrawableZ() + this.getDepth();
}
/**

View File

@@ -24,8 +24,8 @@ namespace gdjs {
updatePosition() {
this._threeObject3D.position.set(
this._object.x + this._object.getWidth() / 2,
this._object.y + this._object.getHeight() / 2,
this._object.getX() + this._object.getWidth() / 2,
this._object.getY() + this._object.getHeight() / 2,
this._object.getZ() + this._object.getDepth() / 2
);
}

View File

@@ -103,6 +103,27 @@ namespace gdjs {
flipZ(enable: boolean): void;
isFlippedZ(): boolean;
/**
* Return the bottom Z of the object.
* Rotations around X and Y are not taken into account.
*/
getUnrotatedAABBMinZ(): number;
/**
* Return the top Z of the object.
* Rotations around X and Y are not taken into account.
*/
getUnrotatedAABBMaxZ(): number;
}
export namespace Base3DHandler {
export const is3D = (
object: gdjs.RuntimeObject
): object is gdjs.RuntimeObject & gdjs.Base3DHandler => {
//@ts-ignore We are checking if the methods are present.
return object.getZ && object.setZ;
};
}
/**
@@ -202,6 +223,14 @@ namespace gdjs {
isFlippedZ(): boolean {
return this.object.isFlippedZ();
}
getUnrotatedAABBMinZ(): number {
return this.object.getUnrotatedAABBMinZ();
}
getUnrotatedAABBMaxZ(): number {
return this.object.getUnrotatedAABBMaxZ();
}
}
gdjs.registerBehavior('Scene3D::Base3DBehavior', gdjs.Base3DBehavior);

View File

@@ -0,0 +1,89 @@
namespace gdjs {
gdjs.PixiFiltersTools.registerFilterCreator(
'Scene3D::Bloom',
new (class implements gdjs.PixiFiltersTools.FilterCreator {
makeFilter(
target: EffectsTarget,
effectData: EffectData
): gdjs.PixiFiltersTools.Filter {
if (typeof THREE === 'undefined') {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
shaderPass: THREE_ADDONS.UnrealBloomPass;
_isEnabled: boolean;
constructor() {
this.shaderPass = new THREE_ADDONS.UnrealBloomPass(
new THREE.Vector2(256, 256),
1,
0,
0
);
this._isEnabled = false;
}
isEnabled(target: EffectsTarget): boolean {
return this._isEnabled;
}
setEnabled(target: EffectsTarget, enabled: boolean): boolean {
if (this._isEnabled === enabled) {
return true;
}
if (enabled) {
return this.applyEffect(target);
} else {
return this.removeEffect(target);
}
}
applyEffect(target: EffectsTarget): boolean {
if (!(target instanceof gdjs.Layer)) {
return false;
}
target.getRenderer().addPostProcessingPass(this.shaderPass);
this._isEnabled = true;
return true;
}
removeEffect(target: EffectsTarget): boolean {
if (!(target instanceof gdjs.Layer)) {
return false;
}
target.getRenderer().removePostProcessingPass(this.shaderPass);
this._isEnabled = false;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'strength') {
this.shaderPass.strength = value;
}
if (parameterName === 'radius') {
this.shaderPass.radius = value;
}
if (parameterName === 'threshold') {
this.shaderPass.threshold = value;
}
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'strength') {
return this.shaderPass.strength;
}
if (parameterName === 'radius') {
return this.shaderPass.radius;
}
if (parameterName === 'threshold') {
return this.shaderPass.threshold;
}
return 0;
}
updateStringParameter(parameterName: string, value: string): void {}
updateColorParameter(parameterName: string, value: number): void {}
getColorParameter(parameterName: string): number {
return 0;
}
updateBooleanParameter(parameterName: string, value: boolean): void {}
})();
}
})()
);
}

View File

@@ -0,0 +1,80 @@
namespace gdjs {
gdjs.PixiFiltersTools.registerFilterCreator(
'Scene3D::BrightnessAndContrast',
new (class implements gdjs.PixiFiltersTools.FilterCreator {
makeFilter(
target: EffectsTarget,
effectData: EffectData
): gdjs.PixiFiltersTools.Filter {
if (typeof THREE === 'undefined') {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
shaderPass: THREE_ADDONS.ShaderPass;
_isEnabled: boolean;
constructor() {
this.shaderPass = new THREE_ADDONS.ShaderPass(
THREE_ADDONS.BrightnessContrastShader
);
this._isEnabled = false;
}
isEnabled(target: EffectsTarget): boolean {
return this._isEnabled;
}
setEnabled(target: EffectsTarget, enabled: boolean): boolean {
if (this._isEnabled === enabled) {
return true;
}
if (enabled) {
return this.applyEffect(target);
} else {
return this.removeEffect(target);
}
}
applyEffect(target: EffectsTarget): boolean {
if (!(target instanceof gdjs.Layer)) {
return false;
}
target.getRenderer().addPostProcessingPass(this.shaderPass);
this._isEnabled = true;
return true;
}
removeEffect(target: EffectsTarget): boolean {
if (!(target instanceof gdjs.Layer)) {
return false;
}
target.getRenderer().removePostProcessingPass(this.shaderPass);
this._isEnabled = false;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'brightness') {
this.shaderPass.uniforms[parameterName].value = value;
}
if (parameterName === 'contrast') {
this.shaderPass.uniforms[parameterName].value = value;
}
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'brightness') {
return this.shaderPass.uniforms[parameterName].value;
}
if (parameterName === 'contrast') {
return this.shaderPass.uniforms[parameterName].value;
}
return 0;
}
updateStringParameter(parameterName: string, value: string): void {}
updateColorParameter(parameterName: string, value: number): void {}
getColorParameter(parameterName: string): number {
return 0;
}
updateBooleanParameter(parameterName: string, value: boolean): void {}
})();
}
})()
);
}

View File

@@ -0,0 +1,373 @@
namespace gdjs {
export interface Object3DDataContent {
width: float;
height: float;
depth: float;
}
/** Base parameters for {@link gdjs.RuntimeObject3D} */
export interface Object3DData extends ObjectData {
/** The base parameters of the RuntimeObject3D */
content: Object3DDataContent;
}
/**
* Base class for 3D custom objects.
*/
export class CustomRuntimeObject3D
extends gdjs.CustomRuntimeObject
implements gdjs.Base3DHandler {
/**
* Position on the Z axis.
*/
private _z: float = 0;
private _minZ: float = 0;
private _maxZ: float = 0;
private _scaleZ: float = 1;
private _flippedZ: boolean = false;
/**
* Euler angle with the `ZYX` order.
*
* Note that `_rotationZ` is `angle` from `gdjs.RuntimeObject`.
*/
private _rotationX: float = 0;
/**
* Euler angle with the `ZYX` order.
*
* Note that `_rotationZ` is `angle` from `gdjs.RuntimeObject`.
*/
private _rotationY: float = 0;
private _customCenterZ: float = 0;
private static _temporaryVector = new THREE.Vector3();
constructor(
parent: gdjs.RuntimeInstanceContainer,
objectData: Object3DData & CustomObjectConfiguration
) {
super(parent, objectData);
this._renderer.reinitialize(this, parent);
}
protected _createRender() {
const parent = this._runtimeScene;
return new gdjs.CustomRuntimeObject3DRenderer(
this,
this._instanceContainer,
parent
);
}
protected _reinitializeRenderer(): void {
this.getRenderer().reinitialize(this, this.getParent());
}
getRenderer(): gdjs.CustomRuntimeObject3DRenderer {
return super.getRenderer() as gdjs.CustomRuntimeObject3DRenderer;
}
get3DRendererObject() {
// It can't be null because Three.js is always loaded
// when a custom 3D object is used.
return this.getRenderer().get3DRendererObject()!;
}
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
super.extraInitializationFromInitialInstance(initialInstanceData);
if (initialInstanceData.depth !== undefined)
this.setDepth(initialInstanceData.depth);
}
/**
* Set the object position on the Z axis.
*/
setZ(z: float): void {
if (z === this._z) return;
this._z = z;
this.getRenderer().updatePosition();
}
/**
* Get the object position on the Z axis.
*/
getZ(): float {
return this._z;
}
/**
* Get the Z position of the rendered object.
*
* For most objects, this will returns the same value as getZ(). But if the
* object has an origin that is not the same as the point (0,0,0) of the
* object displayed, getDrawableZ will differ.
*
* @return The Z position of the rendered object.
*/
getDrawableZ(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._minZ;
}
/**
* Return the Z position of the object center, **relative to the object Z
* position** (`getDrawableX`).
*
* Use `getCenterZInScene` to get the position of the center in the scene.
*
* @return the Z position of the object center, relative to
* `getDrawableZ()`.
*/
getCenterZ(): float {
return this.getDepth() / 2;
}
getCenterZInScene(): float {
return this.getDrawableZ() + this.getCenterZ();
}
setCenterZInScene(z: float): void {
this.setZ(z + this._z - (this.getDrawableZ() + this.getCenterZ()));
}
/**
* Return the bottom Z of the object.
* Rotations around X and Y are not taken into account.
*/
getUnrotatedAABBMinZ(): number {
return this.getDrawableZ();
}
/**
* Return the top Z of the object.
* Rotations around X and Y are not taken into account.
*/
getUnrotatedAABBMaxZ(): number {
return this.getDrawableZ() + this.getDepth();
}
/**
* Set the object rotation on the X axis.
*
* This is an Euler angle. Objects use the `ZYX` order.
*/
setRotationX(angle: float): void {
this._rotationX = angle;
this.getRenderer().updateRotation();
}
/**
* Set the object rotation on the Y axis.
*
* This is an Euler angle. Objects use the `ZYX` order.
*/
setRotationY(angle: float): void {
this._rotationY = angle;
this.getRenderer().updateRotation();
}
/**
* Get the object rotation on the X axis.
*
* This is an Euler angle. Objects use the `ZYX` order.
*/
getRotationX(): float {
return this._rotationX;
}
/**
* Get the object rotation on the Y axis.
*
* This is an Euler angle. Objects use the `ZYX` order.
*/
getRotationY(): float {
return this._rotationY;
}
/**
* Turn the object around the scene x axis at its center.
* @param deltaAngle the rotation angle
*/
turnAroundX(deltaAngle: float): void {
const axisX = gdjs.CustomRuntimeObject3D._temporaryVector;
axisX.set(1, 0, 0);
const mesh = this.get3DRendererObject();
mesh.rotateOnWorldAxis(axisX, gdjs.toRad(deltaAngle));
this._rotationX = gdjs.toDegrees(mesh.rotation.x);
this._rotationY = gdjs.toDegrees(mesh.rotation.y);
this.setAngle(gdjs.toDegrees(mesh.rotation.z));
}
/**
* Turn the object around the scene y axis at its center.
* @param deltaAngle the rotation angle
*/
turnAroundY(deltaAngle: float): void {
const axisY = gdjs.CustomRuntimeObject3D._temporaryVector;
axisY.set(0, 1, 0);
const mesh = this.get3DRendererObject();
mesh.rotateOnWorldAxis(axisY, gdjs.toRad(deltaAngle));
this._rotationX = gdjs.toDegrees(mesh.rotation.x);
this._rotationY = gdjs.toDegrees(mesh.rotation.y);
this.setAngle(gdjs.toDegrees(mesh.rotation.z));
}
/**
* Turn the object around the scene z axis at its center.
* @param deltaAngle the rotation angle
*/
turnAroundZ(deltaAngle: float): void {
const axisZ = gdjs.CustomRuntimeObject3D._temporaryVector;
axisZ.set(0, 0, 1);
const mesh = this.get3DRendererObject();
mesh.rotateOnWorldAxis(axisZ, gdjs.toRad(deltaAngle));
this._rotationX = gdjs.toDegrees(mesh.rotation.x);
this._rotationY = gdjs.toDegrees(mesh.rotation.y);
this.setAngle(gdjs.toDegrees(mesh.rotation.z));
}
/**
* @return the internal width of the object according to its children.
*/
getUnscaledDepth(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._maxZ - this._minZ;
}
_updateUntransformedHitBoxes(): void {
super._updateUntransformedHitBoxes();
let minZ = Number.MAX_VALUE;
let maxZ = -Number.MAX_VALUE;
for (const childInstance of this._instanceContainer.getAdhocListOfAllInstances()) {
if (!childInstance.isIncludedInParentCollisionMask()) {
continue;
}
if (!gdjs.Base3DHandler.is3D(childInstance)) {
continue;
}
minZ = Math.min(minZ, childInstance.getUnrotatedAABBMinZ());
maxZ = Math.max(maxZ, childInstance.getUnrotatedAABBMaxZ());
}
if (minZ === Number.MAX_VALUE) {
// The unscaled size can't be 0 because setWidth and setHeight wouldn't
// have any effect.
minZ = 0;
maxZ = 1;
}
this._minZ = minZ;
this._maxZ = maxZ;
}
/**
* @returns the center Z from the local origin (0;0).
*/
getUnscaledCenterZ(): float {
if (this.hasCustomRotationCenter()) {
return this._customCenterZ;
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return (this._minZ + this._maxZ) / 2;
}
/**
* The center of rotation is defined relatively to the origin (the object
* position).
* This avoids the center to move when children push the bounds.
*
* When no custom center is defined, it will move
* to stay at the center of the children bounds.
*
* @param x coordinate of the custom center
* @param y coordinate of the custom center
*/
setRotationCenter3D(x: float, y: float, z: float) {
this._customCenterZ = z;
this.setRotationCenter(x, y);
}
/**
* Get the object size on the Z axis (called "depth").
*/
getDepth(): float {
return this.getUnscaledDepth() * this.getScaleZ();
}
/**
* Set the object size on the Z axis (called "depth").
*/
setDepth(depth: float): void {
const unscaledDepth = this.getUnscaledDepth();
if (unscaledDepth !== 0) {
this.setScaleZ(depth / unscaledDepth);
}
}
/**
* Change the scale on X, Y and Z axis of the object.
*
* @param newScale The new scale (must be greater than 0).
*/
setScale(newScale: number): void {
super.setScale(newScale);
this.setScaleZ(newScale);
}
/**
* Change the scale on Z axis of the object (changing its height).
*
* @param newScale The new scale (must be greater than 0).
*/
setScaleZ(newScale: number): void {
if (newScale < 0) {
newScale = 0;
}
if (newScale === Math.abs(this._scaleZ)) {
return;
}
this._scaleZ = newScale * (this._flippedZ ? -1 : 1);
this.getRenderer().updateSize();
}
/**
* Get the scale of the object (or the geometric average of X, Y and Z scale in case they are different).
*
* @return the scale of the object (or the geometric average of X, Y and Z scale in case they are different).
*/
getScale(): number {
const scaleX = this.getScaleX();
const scaleY = this.getScaleY();
const scaleZ = this.getScaleZ();
return scaleX === scaleY && scaleX === scaleZ
? scaleX
: Math.pow(scaleX * scaleY * scaleZ, 1 / 3);
}
/**
* Get the scale of the object on Z axis.
*
* @return the scale of the object on Z axis
*/
getScaleZ(): float {
return Math.abs(this._scaleZ);
}
flipZ(enable: boolean) {
if (enable === this._flippedZ) {
return;
}
this._flippedZ = enable;
this.getRenderer().updateSize();
}
isFlippedZ(): boolean {
return this._flippedZ;
}
}
}

View File

@@ -0,0 +1,135 @@
namespace gdjs {
/**
* The renderer for a {@link gdjs.CustomRuntimeObject3D} using Three.js.
*/
export class CustomRuntimeObject3DRenderer
implements gdjs.RuntimeInstanceContainerRenderer {
_object: gdjs.CustomRuntimeObject3D;
_instanceContainer: gdjs.CustomRuntimeObjectInstanceContainer;
_isContainerDirty: boolean = true;
_threeGroup: THREE.Group;
constructor(
object: gdjs.CustomRuntimeObject3D,
instanceContainer: gdjs.CustomRuntimeObjectInstanceContainer,
parent: gdjs.RuntimeInstanceContainer
) {
this._object = object;
this._instanceContainer = instanceContainer;
this._threeGroup = new THREE.Group();
this._threeGroup.rotation.order = 'ZYX';
const layer = parent.getLayer('');
if (layer) {
layer.getRenderer().add3DRendererObject(this._threeGroup);
}
}
get3DRendererObject(): THREE.Object3D {
return this._threeGroup;
}
getRendererObject() {
return null;
}
reinitialize(
object: gdjs.CustomRuntimeObject3D,
parent: gdjs.RuntimeInstanceContainer
) {
this._object = object;
this._isContainerDirty = true;
const layer = parent.getLayer('');
if (layer) {
layer.getRenderer().add3DRendererObject(this._threeGroup);
}
}
_updateThreeGroup() {
const threeObject3D = this.get3DRendererObject();
const scaleX = this._object.getScaleX();
const scaleY = this._object.getScaleY();
const scaleZ = this._object.getScaleZ();
const pivotX = this._object.getUnscaledCenterX() * scaleX;
const pivotY = this._object.getUnscaledCenterY() * scaleY;
const pivotZ = this._object.getUnscaledCenterZ() * scaleZ;
threeObject3D.rotation.set(
gdjs.toRad(this._object.getRotationX()),
gdjs.toRad(this._object.getRotationY()),
gdjs.toRad(this._object.angle)
);
threeObject3D.position.set(
this._object.isFlippedX() ? pivotX : -pivotX,
this._object.isFlippedY() ? pivotY : -pivotY,
this._object.isFlippedZ() ? pivotZ : -pivotZ
);
threeObject3D.position.applyEuler(threeObject3D.rotation);
threeObject3D.position.x += this._object.getX() + pivotX;
threeObject3D.position.y += this._object.getY() + pivotY;
threeObject3D.position.z += this._object.getZ() + pivotZ;
threeObject3D.scale.set(
this._object.isFlippedX() ? -scaleX : scaleX,
this._object.isFlippedY() ? -scaleY : scaleY,
this._object.isFlippedZ() ? -scaleZ : scaleZ
);
threeObject3D.visible = !this._object.hidden;
this._isContainerDirty = true;
}
/**
* Call this to make sure the object is ready to be rendered.
*/
ensureUpToDate() {
if (this._isContainerDirty) {
this._updateThreeGroup();
}
}
update(): void {
this._isContainerDirty = true;
}
updateX(): void {
this._isContainerDirty = true;
}
updateY(): void {
this._isContainerDirty = true;
}
updateAngle(): void {
this._isContainerDirty = true;
}
updatePosition() {
this._isContainerDirty = true;
}
updateRotation() {
this._isContainerDirty = true;
}
updateSize() {
this._isContainerDirty = true;
}
updateVisibility(): void {
this._threeGroup.visible = !this._object.hidden;
}
updateOpacity(): void {
// Opacity is not handled by 3D custom objects.
}
setLayerIndex(layer: gdjs.RuntimeLayer, index: float): void {
// Layers are not handled for 3D custom objects.
}
}
}

View File

@@ -0,0 +1,74 @@
namespace gdjs {
gdjs.PixiFiltersTools.registerFilterCreator(
'Scene3D::Exposure',
new (class implements gdjs.PixiFiltersTools.FilterCreator {
makeFilter(
target: EffectsTarget,
effectData: EffectData
): gdjs.PixiFiltersTools.Filter {
if (typeof THREE === 'undefined') {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
shaderPass: THREE_ADDONS.ShaderPass;
_isEnabled: boolean;
constructor() {
this.shaderPass = new THREE_ADDONS.ShaderPass(
THREE_ADDONS.ExposureShader
);
this._isEnabled = false;
}
isEnabled(target: EffectsTarget): boolean {
return this._isEnabled;
}
setEnabled(target: EffectsTarget, enabled: boolean): boolean {
if (this._isEnabled === enabled) {
return true;
}
if (enabled) {
return this.applyEffect(target);
} else {
return this.removeEffect(target);
}
}
applyEffect(target: EffectsTarget): boolean {
if (!(target instanceof gdjs.Layer)) {
return false;
}
target.getRenderer().addPostProcessingPass(this.shaderPass);
this._isEnabled = true;
return true;
}
removeEffect(target: EffectsTarget): boolean {
if (!(target instanceof gdjs.Layer)) {
return false;
}
target.getRenderer().removePostProcessingPass(this.shaderPass);
this._isEnabled = false;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'exposure') {
this.shaderPass.uniforms[parameterName].value = value;
}
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'exposure') {
return this.shaderPass.uniforms[parameterName].value;
}
return 0;
}
updateStringParameter(parameterName: string, value: string): void {}
updateColorParameter(parameterName: string, value: number): void {}
getColorParameter(parameterName: string): number {
return 0;
}
updateBooleanParameter(parameterName: string, value: boolean): void {}
})();
}
})()
);
}

View File

@@ -0,0 +1,80 @@
namespace gdjs {
gdjs.PixiFiltersTools.registerFilterCreator(
'Scene3D::HueAndSaturation',
new (class implements gdjs.PixiFiltersTools.FilterCreator {
makeFilter(
target: EffectsTarget,
effectData: EffectData
): gdjs.PixiFiltersTools.Filter {
if (typeof THREE === 'undefined') {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
shaderPass: THREE_ADDONS.ShaderPass;
_isEnabled: boolean;
constructor() {
this.shaderPass = new THREE_ADDONS.ShaderPass(
THREE_ADDONS.HueSaturationShader
);
this._isEnabled = false;
}
isEnabled(target: EffectsTarget): boolean {
return this._isEnabled;
}
setEnabled(target: EffectsTarget, enabled: boolean): boolean {
if (this._isEnabled === enabled) {
return true;
}
if (enabled) {
return this.applyEffect(target);
} else {
return this.removeEffect(target);
}
}
applyEffect(target: EffectsTarget): boolean {
if (!(target instanceof gdjs.Layer)) {
return false;
}
target.getRenderer().addPostProcessingPass(this.shaderPass);
this._isEnabled = true;
return true;
}
removeEffect(target: EffectsTarget): boolean {
if (!(target instanceof gdjs.Layer)) {
return false;
}
target.getRenderer().removePostProcessingPass(this.shaderPass);
this._isEnabled = false;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'hue') {
this.shaderPass.uniforms[parameterName].value = value / 180;
}
if (parameterName === 'saturation') {
this.shaderPass.uniforms[parameterName].value = value;
}
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'hue') {
return this.shaderPass.uniforms[parameterName].value * 180;
}
if (parameterName === 'saturation') {
return this.shaderPass.uniforms[parameterName].value;
}
return 0;
}
updateStringParameter(parameterName: string, value: string): void {}
updateColorParameter(parameterName: string, value: number): void {}
getColorParameter(parameterName: string): number {
return 0;
}
updateBooleanParameter(parameterName: string, value: boolean): void {}
})();
}
})()
);
}

View File

@@ -247,7 +247,7 @@ module.exports = {
'Model3DObject',
_('3D Model'),
_('An animated 3D model.'),
'JsPlatform/Extensions/3d_box.svg',
'JsPlatform/Extensions/3d_model.svg',
new gd.Model3DObjectConfiguration()
)
.setCategoryFullName(_('General'))
@@ -1939,6 +1939,102 @@ module.exports = {
.setType('number')
.setGroup(_('Orientation'));
}
{
const effect = extension
.addEffect('HueAndSaturation')
.setFullName(_('Hue and saturation'))
.setDescription(
_(
'Adjust hue and saturation.'
)
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
.addIncludeFile('Extensions/3D/HueAndSaturationEffect.js');
const properties = effect.getProperties();
properties
.getOrCreate('hue')
.setValue('0')
.setLabel(_('Hue in degrees (between -180 and 180)'))
.setType('number');
properties
.getOrCreate('saturation')
.setValue('0')
.setLabel(_('Saturation (between -1 and 1)'))
.setType('number');
}
{
const effect = extension
.addEffect('Exposure')
.setFullName(_('Exposure'))
.setDescription(
_(
'Adjust exposure.'
)
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
.addIncludeFile('Extensions/3D/ExposureEffect.js');
const properties = effect.getProperties();
properties
.getOrCreate('exposure')
.setValue('1')
.setLabel(_('Exposure (positive value)'))
.setType('number');
}
{
const effect = extension
.addEffect('Bloom')
.setFullName(_('Bloom'))
.setDescription(
_(
'Apply a bloom effect.'
)
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
.addIncludeFile('Extensions/3D/BloomEffect.js');
const properties = effect.getProperties();
properties
.getOrCreate('strength')
.setValue('1')
.setLabel(_('Strength (between 0 and 3)'))
.setType('number');
properties
.getOrCreate('radius')
.setValue('0')
.setLabel(_('Radius (between 0 and 1)'))
.setType('number');
properties
.getOrCreate('threshold')
.setValue('0')
.setLabel(_('Threshold (between 0 and 1)'))
.setType('number');
}
{
const effect = extension
.addEffect('BrightnessAndContrast')
.setFullName(_('Brightness and contrast'))
.setDescription(
_(
'Adjust brightness and contrast.'
)
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
.addIncludeFile('Extensions/3D/BrightnessAndContrastEffect.js');
const properties = effect.getProperties();
properties
.getOrCreate('brightness')
.setValue('0')
.setLabel(_('Brightness (between -1 and 1)'))
.setType('number');
properties
.getOrCreate('contrast')
.setValue('0')
.setLabel(_('Contrast (between -1 and 1)'))
.setType('number');
}
// Don't forget to update the alert condition in Model3DEditor.js when
// adding a new light.
@@ -2624,6 +2720,8 @@ module.exports = {
RenderedCube3DObject3DInstance
);
const epsilon = 1 / (1 << 16);
class Model3DRendered2DInstance extends RenderedInstance {
_modelOriginPoint = [0, 0, 0];
@@ -2695,7 +2793,7 @@ module.exports = {
}
static getThumbnail(project, resourcesLoader, objectConfiguration) {
return 'JsPlatform/Extensions/3d_box.svg';
return 'JsPlatform/Extensions/3d_model.svg';
}
getOriginX() {
@@ -2736,6 +2834,9 @@ module.exports = {
originalDepth,
keepAspectRatio
) {
// These formulas are also used in:
// - gdjs.Model3DRuntimeObject3DRenderer._updateDefaultTransformation
// - Model3DEditor.modelSize
threeObject.rotation.set(
(rotationX * Math.PI) / 180,
(rotationY * Math.PI) / 180,
@@ -2743,13 +2844,24 @@ module.exports = {
);
threeObject.updateMatrixWorld(true);
const boundingBox = new THREE.Box3().setFromObject(threeObject);
const shouldKeepModelOrigin = !this._originPoint;
if (shouldKeepModelOrigin) {
// Keep the origin as part of the model.
// For instance, a model can be 1 face of a cube and we want to keep the
// inside as part of the object even if it's just void.
// It also avoids to have the origin outside of the object box.
boundingBox.expandByPoint(new THREE.Vector3(0, 0, 0));
}
const modelWidth = boundingBox.max.x - boundingBox.min.x;
const modelHeight = boundingBox.max.y - boundingBox.min.y;
const modelDepth = boundingBox.max.z - boundingBox.min.z;
this._modelOriginPoint[0] = -boundingBox.min.x / modelWidth;
this._modelOriginPoint[1] = -boundingBox.min.y / modelHeight;
this._modelOriginPoint[2] = -boundingBox.min.z / modelDepth;
this._modelOriginPoint[0] =
modelWidth < epsilon ? 0 : -boundingBox.min.x / modelWidth;
this._modelOriginPoint[1] =
modelHeight < epsilon ? 0 : -boundingBox.min.y / modelHeight;
this._modelOriginPoint[2] =
modelDepth < epsilon ? 0 : -boundingBox.min.z / modelDepth;
// The model is flipped on Y axis.
this._modelOriginPoint[1] = 1 - this._modelOriginPoint[1];
@@ -2758,19 +2870,10 @@ module.exports = {
const centerPoint = this._centerPoint;
if (centerPoint) {
threeObject.position.set(
-(
boundingBox.min.x +
(boundingBox.max.x - boundingBox.min.x) * centerPoint[0]
),
-(boundingBox.min.x + modelWidth * centerPoint[0]),
// The model is flipped on Y axis.
-(
boundingBox.min.y +
(boundingBox.max.y - boundingBox.min.y) * (1 - centerPoint[1])
),
-(
boundingBox.min.z +
(boundingBox.max.z - boundingBox.min.z) * centerPoint[2]
)
-(boundingBox.min.y + modelHeight * (1 - centerPoint[1])),
-(boundingBox.min.z + modelDepth * centerPoint[2])
);
}
@@ -2783,9 +2886,9 @@ module.exports = {
);
// Stretch the model in a 1x1x1 cube.
const scaleX = 1 / modelWidth;
const scaleY = 1 / modelHeight;
const scaleZ = 1 / modelDepth;
const scaleX = modelWidth < epsilon ? 1 : 1 / modelWidth;
const scaleY = modelHeight < epsilon ? 1 : 1 / modelHeight;
const scaleZ = modelDepth < epsilon ? 1 : 1 / modelDepth;
const scaleMatrix = new THREE.Matrix4();
// Flip on Y because the Y axis is on the opposite side of direct basis.
@@ -2796,10 +2899,22 @@ module.exports = {
if (keepAspectRatio) {
// Reduce the object dimensions to keep aspect ratio.
const widthRatio = originalWidth / modelWidth;
const heightRatio = originalHeight / modelHeight;
const depthRatio = originalDepth / modelDepth;
const scaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
const widthRatio =
modelWidth < epsilon
? Number.POSITIVE_INFINITY
: originalWidth / modelWidth;
const heightRatio =
modelHeight < epsilon
? Number.POSITIVE_INFINITY
: originalHeight / modelHeight;
const depthRatio =
modelDepth < epsilon
? Number.POSITIVE_INFINITY
: originalDepth / modelDepth;
let scaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
if (!Number.isFinite(scaleRatio)) {
scaleRatio = 1;
}
this._defaultWidth = scaleRatio * modelWidth;
this._defaultHeight = scaleRatio * modelHeight;
@@ -2951,6 +3066,11 @@ module.exports = {
return this.getHeight() * originPoint[1];
}
getOriginZ() {
const originPoint = this.getOriginPoint();
return this.getDepth() * originPoint[2];
}
getCenterX() {
const centerPoint = this.getCenterPoint();
return this.getWidth() * centerPoint[0];
@@ -2961,6 +3081,11 @@ module.exports = {
return this.getHeight() * centerPoint[1];
}
getCenterZ() {
const centerPoint = this.getCenterPoint();
return this.getDepth() * centerPoint[2];
}
getOriginPoint() {
return this._originPoint || this._modelOriginPoint;
}
@@ -2986,13 +3111,25 @@ module.exports = {
);
threeObject.updateMatrixWorld(true);
const boundingBox = new THREE.Box3().setFromObject(threeObject);
const shouldKeepModelOrigin = !this._originPoint;
if (shouldKeepModelOrigin) {
// Keep the origin as part of the model.
// For instance, a model can be 1 face of a cube and we want to keep the
// inside as part of the object even if it's just void.
// It also avoids to have the origin outside of the object box.
boundingBox.expandByPoint(new THREE.Vector3(0, 0, 0));
}
const modelWidth = boundingBox.max.x - boundingBox.min.x;
const modelHeight = boundingBox.max.y - boundingBox.min.y;
const modelDepth = boundingBox.max.z - boundingBox.min.z;
this._modelOriginPoint[0] = -boundingBox.min.x / modelWidth;
this._modelOriginPoint[1] = -boundingBox.min.y / modelHeight;
this._modelOriginPoint[2] = -boundingBox.min.z / modelDepth;
this._modelOriginPoint[0] =
modelWidth < epsilon ? 0 : -boundingBox.min.x / modelWidth;
this._modelOriginPoint[1] =
modelHeight < epsilon ? 0 : -boundingBox.min.y / modelHeight;
this._modelOriginPoint[2] =
modelDepth < epsilon ? 0 : -boundingBox.min.z / modelDepth;
// The model is flipped on Y axis.
this._modelOriginPoint[1] = 1 - this._modelOriginPoint[1];
@@ -3001,19 +3138,10 @@ module.exports = {
const centerPoint = this._centerPoint;
if (centerPoint) {
threeObject.position.set(
-(
boundingBox.min.x +
(boundingBox.max.x - boundingBox.min.x) * centerPoint[0]
),
-(boundingBox.min.x + modelWidth * centerPoint[0]),
// The model is flipped on Y axis.
-(
boundingBox.min.y +
(boundingBox.max.y - boundingBox.min.y) * (1 - centerPoint[1])
),
-(
boundingBox.min.z +
(boundingBox.max.z - boundingBox.min.z) * centerPoint[2]
)
-(boundingBox.min.y + modelHeight * (1 - centerPoint[1])),
-(boundingBox.min.z + modelDepth * centerPoint[2])
);
}
@@ -3026,9 +3154,9 @@ module.exports = {
);
// Stretch the model in a 1x1x1 cube.
const scaleX = 1 / modelWidth;
const scaleY = 1 / modelHeight;
const scaleZ = 1 / modelDepth;
const scaleX = modelWidth < epsilon ? 1 : 1 / modelWidth;
const scaleY = modelHeight < epsilon ? 1 : 1 / modelHeight;
const scaleZ = modelDepth < epsilon ? 1 : 1 / modelDepth;
const scaleMatrix = new THREE.Matrix4();
// Flip on Y because the Y axis is on the opposite side of direct basis.
@@ -3039,10 +3167,22 @@ module.exports = {
if (keepAspectRatio) {
// Reduce the object dimensions to keep aspect ratio.
const widthRatio = originalWidth / modelWidth;
const heightRatio = originalHeight / modelHeight;
const depthRatio = originalDepth / modelDepth;
const scaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
const widthRatio =
modelWidth < epsilon
? Number.POSITIVE_INFINITY
: originalWidth / modelWidth;
const heightRatio =
modelHeight < epsilon
? Number.POSITIVE_INFINITY
: originalHeight / modelHeight;
const depthRatio =
modelDepth < epsilon
? Number.POSITIVE_INFINITY
: originalDepth / modelDepth;
let scaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
if (!Number.isFinite(scaleRatio)) {
scaleRatio = 1;
}
this._defaultWidth = scaleRatio * modelWidth;
this._defaultHeight = scaleRatio * modelHeight;

View File

@@ -1,6 +1,8 @@
namespace gdjs {
type FloatPoint3D = [float, float, float];
const epsilon = 1 / (1 << 16);
const removeMetalness = (material: THREE.Material): void => {
//@ts-ignore
if (material.metalness) {
@@ -9,7 +11,7 @@ namespace gdjs {
}
};
const removeMetalnessFromMesh = (node: THREE.Object3D<THREE.Event>) => {
const removeMetalnessFromMesh = (node: THREE.Object3D) => {
const mesh = node as THREE.Mesh;
if (!mesh.material) {
return;
@@ -23,9 +25,8 @@ namespace gdjs {
}
};
const traverseToRemoveMetalnessFromMeshes = (
node: THREE.Object3D<THREE.Event>
) => node.traverse(removeMetalnessFromMesh);
const traverseToRemoveMetalnessFromMeshes = (node: THREE.Object3D) =>
node.traverse(removeMetalnessFromMesh);
const convertToBasicMaterial = (
material: THREE.Material
@@ -44,7 +45,7 @@ namespace gdjs {
return basicMaterial;
};
const setBasicMaterialTo = (node: THREE.Object3D<THREE.Event>): void => {
const setBasicMaterialTo = (node: THREE.Object3D): void => {
const mesh = node as THREE.Mesh;
if (!mesh.material) {
return;
@@ -59,9 +60,8 @@ namespace gdjs {
}
};
const traverseToSetBasicMaterialFromMeshes = (
node: THREE.Object3D<THREE.Event>
) => node.traverse(setBasicMaterialTo);
const traverseToSetBasicMaterialFromMeshes = (node: THREE.Object3D) =>
node.traverse(setBasicMaterialTo);
class Model3DRuntimeObject3DRenderer extends gdjs.RuntimeObject3DRenderer {
private _model3DRuntimeObject: gdjs.Model3DRuntimeObject;
@@ -147,6 +147,9 @@ namespace gdjs {
originalDepth: float,
keepAspectRatio: boolean
) {
// These formulas are also used in:
// - Model3DEditor.modelSize
// - Model3DRendered2DInstance
threeObject.rotation.set(
gdjs.toRad(rotationX),
gdjs.toRad(rotationY),
@@ -155,12 +158,24 @@ namespace gdjs {
threeObject.updateMatrixWorld(true);
const boundingBox = new THREE.Box3().setFromObject(threeObject);
const shouldKeepModelOrigin = !this._model3DRuntimeObject._originPoint;
if (shouldKeepModelOrigin) {
// Keep the origin as part of the model.
// For instance, a model can be 1 face of a cube and we want to keep the
// inside as part of the object even if it's just void.
// It also avoids to have the origin outside of the object box.
boundingBox.expandByPoint(new THREE.Vector3(0, 0, 0));
}
const modelWidth = boundingBox.max.x - boundingBox.min.x;
const modelHeight = boundingBox.max.y - boundingBox.min.y;
const modelDepth = boundingBox.max.z - boundingBox.min.z;
this._modelOriginPoint[0] = -boundingBox.min.x / modelWidth;
this._modelOriginPoint[1] = -boundingBox.min.y / modelHeight;
this._modelOriginPoint[2] = -boundingBox.min.z / modelDepth;
this._modelOriginPoint[0] =
modelWidth < epsilon ? 0 : -boundingBox.min.x / modelWidth;
this._modelOriginPoint[1] =
modelHeight < epsilon ? 0 : -boundingBox.min.y / modelHeight;
this._modelOriginPoint[2] =
modelDepth < epsilon ? 0 : -boundingBox.min.z / modelDepth;
// The model is flipped on Y axis.
this._modelOriginPoint[1] = 1 - this._modelOriginPoint[1];
@@ -169,19 +184,10 @@ namespace gdjs {
const centerPoint = this._model3DRuntimeObject._centerPoint;
if (centerPoint) {
threeObject.position.set(
-(
boundingBox.min.x +
(boundingBox.max.x - boundingBox.min.x) * centerPoint[0]
),
-(boundingBox.min.x + modelWidth * centerPoint[0]),
// The model is flipped on Y axis.
-(
boundingBox.min.y +
(boundingBox.max.y - boundingBox.min.y) * (1 - centerPoint[1])
),
-(
boundingBox.min.z +
(boundingBox.max.z - boundingBox.min.z) * centerPoint[2]
)
-(boundingBox.min.y + modelHeight * (1 - centerPoint[1])),
-(boundingBox.min.z + modelDepth * centerPoint[2])
);
}
@@ -194,9 +200,9 @@ namespace gdjs {
);
// Stretch the model in a 1x1x1 cube.
const scaleX = 1 / modelWidth;
const scaleY = 1 / modelHeight;
const scaleZ = 1 / modelDepth;
const scaleX = modelWidth < epsilon ? 1 : 1 / modelWidth;
const scaleY = modelHeight < epsilon ? 1 : 1 / modelHeight;
const scaleZ = modelDepth < epsilon ? 1 : 1 / modelDepth;
const scaleMatrix = new THREE.Matrix4();
// Flip on Y because the Y axis is on the opposite side of direct basis.
@@ -207,10 +213,22 @@ namespace gdjs {
if (keepAspectRatio) {
// Reduce the object dimensions to keep aspect ratio.
const widthRatio = originalWidth / modelWidth;
const heightRatio = originalHeight / modelHeight;
const depthRatio = originalDepth / modelDepth;
const scaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
const widthRatio =
modelWidth < epsilon
? Number.POSITIVE_INFINITY
: originalWidth / modelWidth;
const heightRatio =
modelHeight < epsilon
? Number.POSITIVE_INFINITY
: originalHeight / modelHeight;
const depthRatio =
modelDepth < epsilon
? Number.POSITIVE_INFINITY
: originalDepth / modelDepth;
let scaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
if (!Number.isFinite(scaleRatio)) {
scaleRatio = 1;
}
this._object._setOriginalWidth(scaleRatio * modelWidth);
this._object._setOriginalHeight(scaleRatio * modelHeight);

View File

@@ -11,7 +11,11 @@ namespace gdjs {
const layer = runtimeScene.getLayer(layerName);
const layerRenderer = layer.getRenderer();
const threeCamera = layerRenderer.getThreeCamera();
const fov = threeCamera ? threeCamera.fov : assumedFovIn2D;
const fov = threeCamera
? threeCamera instanceof THREE.OrthographicCamera
? null
: threeCamera.fov
: assumedFovIn2D;
return layer.getCameraZ(fov, cameraIndex);
};
@@ -24,7 +28,11 @@ namespace gdjs {
const layer = runtimeScene.getLayer(layerName);
const layerRenderer = layer.getRenderer();
const threeCamera = layerRenderer.getThreeCamera();
const fov = threeCamera ? threeCamera.fov : assumedFovIn2D;
const fov = threeCamera
? threeCamera instanceof THREE.OrthographicCamera
? null
: threeCamera.fov
: assumedFovIn2D;
layer.setCameraZ(z, fov, cameraIndex);
};
@@ -213,8 +221,11 @@ namespace gdjs {
const layerRenderer = layer.getRenderer();
const threeCamera = layerRenderer.getThreeCamera();
if (!threeCamera) return 45;
return threeCamera.fov;
return threeCamera
? threeCamera instanceof THREE.OrthographicCamera
? 0
: threeCamera.fov
: assumedFovIn2D;
};
export const setFov = (
@@ -227,7 +238,8 @@ namespace gdjs {
const layerRenderer = layer.getRenderer();
const threeCamera = layerRenderer.getThreeCamera();
if (!threeCamera) return;
if (!threeCamera || threeCamera instanceof THREE.OrthographicCamera)
return;
threeCamera.fov = Math.min(Math.max(angle, 0), 180);
layerRenderer.setThreeCameraDirty(true);

View File

@@ -436,6 +436,21 @@ module.exports = {
addSettersAndGettersToObject(object, setterAndGetterProperties, 'BBText');
object
.addAction(
`SetFontFamily2`,
_('Font family'),
_('Set font family'),
_('Set the font of _PARAM0_ to _PARAM1_'),
'',
'res/actions/font24.png',
'res/actions/font24.png'
)
.addParameter('object', 'BBText', 'BBText', false)
.addParameter('fontResource', _('Font family'), '', false)
.getCodeExtraInformation()
.setFunctionName(`setFontFamily`);
const actions = object.getAllActions();
const conditions = object.getAllConditions();
const expressions = object.getAllExpressions();
@@ -443,6 +458,9 @@ module.exports = {
actions.get('BBText::SetOpacity').setHidden();
conditions.get('BBText::IsOpacity').setHidden();
expressions.get('GetOpacity').setHidden();
// Action deprecated because it's using the `string` type instead of the more
// user-friendly `fontResource` type.
actions.get('BBText::SetFontFamily').setHidden();
return extension;
},

View File

@@ -141,6 +141,7 @@ namespace gdjs {
setBBText(text): void {
this._text = text;
this._renderer.updateText();
this.invalidateHitboxes();
}
/**

View File

@@ -26,6 +26,7 @@ set(
TextEntryObject
TextObject
TiledSpriteObject
Spine
TopDownMovementBehavior)
# Automatically add all listed extensions

View File

@@ -3,6 +3,7 @@ GDevelop - LinkedObjects Extension
Copyright (c) 2013-2016 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
const logger = new gdjs.Logger('LinkedObjects');
/**
* Manages the links between objects.
*/
@@ -114,7 +115,14 @@ namespace gdjs {
if (this._links.has(linkedObject.id)) {
const otherObjList = this._links
.get(linkedObject.id)!
.linkedObjectMap.get(removedObject.getName())!;
.linkedObjectMap.get(removedObject.getName());
if (!otherObjList) {
logger.error(
`Can't find link from ${linkedObject.id} (${linkedObject.name}) to ${removedObject.id} (${removedObject.name})`
);
return;
}
const index = otherObjList.indexOf(removedObject);
if (index !== -1) {

View File

@@ -24,58 +24,41 @@ namespace gdjs {
runtimeObject: gdjs.RuntimeObject,
objectData: any
) {
let texture = null;
const graphics = new PIXI.Graphics();
graphics.lineStyle(0, 0, 0);
graphics.beginFill(gdjs.rgbToHexNumber(255, 255, 255), 1);
if (objectData.rendererType === 'Point') {
graphics.drawCircle(0, 0, objectData.rendererParam1);
} else if (objectData.rendererType === 'Line') {
graphics.drawRect(
0,
0,
objectData.rendererParam1,
objectData.rendererParam2
);
// Draw an almost-invisible rectangle in the left hand to force PIXI to take a full texture with our line at the right hand
graphics.beginFill(gdjs.rgbToHexNumber(255, 255, 255), 0.001);
graphics.drawRect(
0,
0,
objectData.rendererParam1,
objectData.rendererParam2
);
} else if (objectData.textureParticleName) {
const sprite = new PIXI.Sprite(
(instanceContainer
.getGame()
.getImageManager() as gdjs.PixiImageManager).getPIXITexture(
objectData.textureParticleName
)
);
sprite.width = objectData.rendererParam1;
sprite.height = objectData.rendererParam2;
graphics.addChild(sprite);
} else {
graphics.drawRect(
0,
0,
objectData.rendererParam1,
objectData.rendererParam2
);
}
graphics.endFill();
// Render the texture from graphics using the PIXI Renderer.
// TODO: could be optimized by generating the texture only once per object type,
// instead of at each object creation.
const pixiRenderer = instanceContainer
.getGame()
.getRenderer()
.getPIXIRenderer();
//@ts-expect-error Pixi has wrong type definitions for this method
texture = pixiRenderer.generateTexture(graphics);
const imageManager = instanceContainer
.getGame()
.getImageManager() as gdjs.PixiImageManager;
let particleTexture: PIXI.Texture = PIXI.Texture.WHITE;
if (pixiRenderer) {
if (objectData.rendererType === 'Point') {
particleTexture = imageManager.getOrCreateDiskTexture(
objectData.rendererParam1,
pixiRenderer
);
} else if (objectData.rendererType === 'Line') {
particleTexture = imageManager.getOrCreateRectangleTexture(
objectData.rendererParam1,
objectData.rendererParam2,
pixiRenderer
);
} else if (objectData.textureParticleName) {
particleTexture = imageManager.getOrCreateScaledTexture(
objectData.textureParticleName,
objectData.rendererParam1,
objectData.rendererParam2,
pixiRenderer
);
} else {
particleTexture = imageManager.getOrCreateRectangleTexture(
objectData.rendererParam1,
objectData.rendererParam2,
pixiRenderer
);
}
}
const configuration = {
ease: undefined,
@@ -198,7 +181,7 @@ namespace gdjs {
{
type: 'textureSingle',
config: {
texture: texture,
texture: particleTexture,
},
},
{
@@ -236,8 +219,9 @@ namespace gdjs {
}
update(delta: float): void {
const wasEmitting = this.emitter.emit;
this.emitter.update(delta);
if (!this.started && this.getParticleCount() > 0) {
if (!this.started && wasEmitting) {
this.started = true;
}
}

View File

@@ -37,6 +37,7 @@ std::map<gd::String, gd::PropertyDescriptor> PathfindingBehavior::GetProperties(
.SetValue(behaviorContent.GetBoolAttribute("allowDiagonals") ? "true"
: "false")
.SetGroup(_("Path smoothing"))
.SetAdvanced()
.SetType("Boolean");
properties["Acceleration"]
.SetLabel(_("Acceleration"))
@@ -99,6 +100,7 @@ std::map<gd::String, gd::PropertyDescriptor> PathfindingBehavior::GetProperties(
properties["ExtraBorder"]
.SetDescription(_("Extra border size"))
.SetGroup(_("Collision"))
.SetAdvanced()
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetValue(
@@ -108,6 +110,7 @@ std::map<gd::String, gd::PropertyDescriptor> PathfindingBehavior::GetProperties(
.SetValue(gd::String::From(
behaviorContent.GetDoubleAttribute("smoothingMaxCellGap")))
.SetGroup(_("Path smoothing"))
.SetAdvanced()
.SetDescription(_("It's recommended to leave a max gap of 1 cell. "
"Setting it to 0 disable the smoothing."));

View File

@@ -150,7 +150,8 @@ PlatformerObjectBehavior::GetProperties(
properties["UseLegacyTrajectory"]
.SetLabel(_("Use frame rate dependent trajectories (deprecated, it's "
"recommended to leave this unchecked)"))
.SetGroup(_("Deprecated options (advanced)"))
.SetGroup(_("Deprecated options"))
.SetDeprecated()
.SetValue(behaviorContent.GetBoolAttribute("useLegacyTrajectory", true)
? "true"
: "false")

View File

@@ -62,7 +62,7 @@ namespace gdjs {
private _yGrabOffset: any;
private _xGrabTolerance: any;
_useLegacyTrajectory: boolean = true;
_useLegacyTrajectory: boolean;
_canGoDownFromJumpthru: boolean = false;
@@ -355,13 +355,25 @@ namespace gdjs {
private _updateSpeed(timeDelta: float): float {
const previousSpeed = this._currentSpeed;
//Change the speed according to the player's input.
// @ts-ignore
if (this._leftKey) {
this._currentSpeed -= this._acceleration * timeDelta;
}
if (this._rightKey) {
this._currentSpeed += this._acceleration * timeDelta;
// Change the speed according to the player's input.
// TODO Give priority to the last key for faster reaction time.
if (this._leftKey !== this._rightKey) {
if (this._leftKey) {
if (this._currentSpeed <= 0) {
this._currentSpeed -= this._acceleration * timeDelta;
} else {
// Turn back at least as fast as it would stop.
this._currentSpeed -=
Math.max(this._acceleration, this._deceleration) * timeDelta;
}
} else if (this._rightKey) {
if (this._currentSpeed >= 0) {
this._currentSpeed += this._acceleration * timeDelta;
} else {
this._currentSpeed +=
Math.max(this._acceleration, this._deceleration) * timeDelta;
}
}
}
//Take deceleration into account only if no key is pressed.
@@ -1680,13 +1692,15 @@ namespace gdjs {
beforeUpdatingObstacles(timeDelta: float) {
const object = this._behavior.owner;
//Stick the object to the floor if its height has changed.
// Stick the object to the floor if its height has changed.
if (this._oldHeight !== object.getHeight()) {
object.setY(
this._floorLastY -
object.getHeight() +
(object.getY() - object.getDrawableY())
);
// TODO This should probably be done after the events because
// the character stays at the wrong place during 1 frame.
const deltaY =
((this._oldHeight - object.getHeight()) *
(object.getHeight() + object.getDrawableY() - object.getY())) /
object.getHeight();
object.setY(object.getY() + deltaY);
}
// Directly follow the floor movement on the Y axis by moving the character.
// For the X axis, we follow the floor movement using `_requestedDeltaX`

View File

@@ -501,6 +501,22 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
);
});
});
it('can stay on a rotated platform when its height changes', function () {
const platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
platform.setAngle(-45);
object.setPosition(30, -32);
// Ensure the object falls on the platform
fallOnPlatform(10);
const oldY = object.getY();
expect(object.getY()).to.be.within(-40, -39);
object.setHeight(object.getHeight() - 8);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be(oldY + 8);
});
});
[0, 25].forEach((slopeMaxAngle) => {

View File

@@ -169,6 +169,23 @@ module.exports = {
)
.setFunctionName('gdjs.playerAuthentication.getUsername');
extension
.addStrExpression(
'UserID',
_('User ID'),
_('Get the unique user ID of the authenticated player.'),
'',
'JsPlatform/Extensions/authentication.svg'
)
.getCodeExtraInformation()
.setIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.setFunctionName('gdjs.playerAuthentication.getUserId');
extension
.addCondition(
'IsPlayerAuthenticated',

View File

@@ -171,7 +171,7 @@ namespace gdjs {
if (!_checkedLocalStorage) {
readAuthenticatedUserFromLocalStorage();
}
return _userId || null;
return _userId || '';
};
/**

View File

@@ -163,9 +163,9 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
obj.AddAction("Torus",
_("Torus"),
_("Draw a torus on screen"),
_("Draw at _PARAM1_;_PARAM2_ a torus with inner radius"
"_PARAM3_ and outer radius _PARAM4_ and "
"with start arc _PARAM5_° and end arc _PARAM6_°"
_("Draw at _PARAM1_;_PARAM2_ a torus with "
"inner radius: _PARAM3_, outer radius: _PARAM4_ and "
"with start arc angle: _PARAM5_°, end angle: _PARAM6_° "
"with _PARAM0_"),
_("Drawing"),
"res/actions/torus24.png",

View File

@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 2.6)
cmake_policy(SET CMP0015 NEW)
project(SpineObject)
gd_add_extension_includes()
#Defines
###
gd_add_extension_definitions(SpineObject)
#The targets
###
include_directories(.)
file(GLOB source_files *.cpp *.h)
gd_add_clang_utils(SpineObject "${source_files}")
gd_add_extension_target(SpineObject "${source_files}")
#Linker files for the IDE extension
###
gd_extension_link_libraries(SpineObject)

View File

@@ -0,0 +1,323 @@
// @flow
/**
* This is a declaration of an extension for GDevelop 5.
*
* Changes in this file are watched and automatically imported if the editor
* is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts).
*
* The file must be named "JsExtension.js", otherwise GDevelop won't load it.
* ⚠️ If you make a change and the extension is not loaded, open the developer console
* and search for any errors.
*
* More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
*/
/*::
// Import types to allow Flow to do static type checking on this file.
// Extensions declaration are typed using Flow (like the editor), but the files
// for the game engine are checked with TypeScript annotations.
import { type ObjectsRenderingService, type ObjectsEditorService } from '../JsExtensionTypes.flow.js'
*/
module.exports = {
createExtension: function (
_ /*: (string) => string */,
gd /*: libGDevelop */
) {
const extension = new gd.PlatformExtension();
extension
.setExtensionInformation(
'SpineObject',
_('Spine (experimental)'),
_('Displays a Spine animation.'),
'Vladyslav Pohorielov',
'Open source (MIT License)'
)
.setExtensionHelpPath('/objects/spine')
.setCategory('Advanced');
extension
.addInstructionOrExpressionGroupMetadata(_('Spine'))
.setIcon('JsPlatform/Extensions/spine.svg');
const object = extension
.addObject(
'SpineObject',
_('Spine (experimental)'),
_(
'Display and smoothly animate a 2D object with skeletal animations made with Spine. Use files exported from Spine (json, atlas and image).'
),
'JsPlatform/Extensions/spine.svg',
new gd.SpineObjectConfiguration()
)
.addDefaultBehavior('EffectCapability::EffectBehavior')
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
.addDefaultBehavior('FlippableCapability::FlippableBehavior')
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
.addDefaultBehavior('AnimatableCapability::AnimatableBehavior')
.setIncludeFile('Extensions/Spine/spineruntimeobject.js')
.addIncludeFile('Extensions/Spine/spineruntimeobject-pixi-renderer.js')
.addIncludeFile('Extensions/Spine/pixi-spine/pixi-spine.js')
.addIncludeFile('Extensions/Spine/managers/pixi-spine-atlas-manager.js')
.addIncludeFile('Extensions/Spine/managers/pixi-spine-manager.js')
.setCategoryFullName(_('Advanced'));
object
.addExpressionAndConditionAndAction(
'number',
'Animation',
_('Animation mixing duration'),
_(
'the duration of the smooth transition between 2 animations (in second)'
),
_('the animation mixing duration'),
_('Animations and images'),
'JsPlatform/Extensions/spine.svg'
)
.addParameter('object', _('Spine'), 'SpineObject')
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('setAnimationMixingDuration')
.setGetter('getAnimationMixingDuration');
return extension;
},
/**
* You can optionally add sanity tests that will check the basic working
* of your extension behaviors/objects by instantiating behaviors/objects
* and setting the property to a given value.
*
* If you don't have any tests, you can simply return an empty array.
*
* But it is recommended to create tests for the behaviors/objects properties you created
* to avoid mistakes.
*/
runExtensionSanityTests: function (
gd /*: libGDevelop */,
extension /*: gdPlatformExtension*/
) {
return [];
},
/**
* Register editors for objects.
*
* Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
*/
registerEditorConfigurations: function (
objectsEditorService /*: ObjectsEditorService */
) {},
/**
* Register renderers for instance of objects on the scene editor.
*
* Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
*/
registerInstanceRenderers: function (
objectsRenderingService /*: ObjectsRenderingService */
) {
const { PIXI, RenderedInstance, gd } = objectsRenderingService;
class RenderedSpineInstance extends RenderedInstance {
_spine = null;
_rect = new PIXI.Graphics();
_initialWidth = null;
_initialHeight = null;
_animationIndex = -1;
_spineOriginOffsetX = 0;
_spineOriginOffsetY = 0;
constructor(
project,
layout,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
) {
super(
project,
layout,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
);
// there is issue with spine selection. mouse events are not triggering during interaction.
// create the invisible background rectangle to fill spine range.
this._rect.alpha = 0;
this._pixiObject = new PIXI.Container();
this._pixiObject.addChild(this._rect);
this._pixiContainer.addChild(this._pixiObject);
this._loadSpine();
}
static getThumbnail(project, resourcesLoader, objectConfiguration) {
return 'JsPlatform/Extensions/spine.svg';
}
update() {
this._pixiObject.position.set(
this._instance.getX(),
this._instance.getY()
);
this.setAnimation(this._instance.getRawDoubleProperty('animation'));
const width = this.getWidth();
const height = this.getHeight();
const { _spine: spine } = this;
if (spine) {
spine.width = width;
spine.height = height;
const localBounds = spine.getLocalBounds(undefined, true);
this._spineOriginOffsetX = localBounds.x * spine.scale.x;
this._spineOriginOffsetY = localBounds.y * spine.scale.y;
this._rect.position.set(
this._spineOriginOffsetX,
this._spineOriginOffsetY
);
}
this._rect.clear();
this._rect.beginFill(0xffffff);
this._rect.lineStyle(1, 0xff0000);
this._rect.drawRect(0, 0, width, height);
}
/**
* @returns x coordinate of this spine origin offset
*/
getOriginX() {
return -this._spineOriginOffsetX;
}
/**
* @returns y coordinate of this spine origin offset
*/
getOriginY() {
return -this._spineOriginOffsetY;
}
/**
* @param {number} index - animation index
*/
setAnimation(index) {
const { _spine: spine } = this;
const configuration = this._getConfiguration();
if (
!spine ||
configuration.hasNoAnimations() ||
index === this._animationIndex
) {
return;
}
if (!Number.isInteger(index) || index < 0) {
index = 0;
} else if (configuration.getAnimationsCount() <= index) {
index = configuration.getAnimationsCount() - 1;
}
this._animationIndex = index;
const animation = configuration.getAnimation(index);
const source = animation.getSource();
const shouldLoop = animation.shouldLoop();
const scale = this.getScale();
// reset scale to track new animation range
// if custom size is set it will be reinitialized in update method
spine.scale.set(1, 1);
spine.state.setAnimation(0, source, shouldLoop);
spine.state.tracks[0].trackTime = 0;
spine.update(0);
spine.autoUpdate = false;
this._initialWidth = spine.width * this.getScale();
this._initialHeight = spine.height * this.getScale();
}
/**
* @returns {number} default width
*/
getDefaultWidth() {
return this._initialWidth !== null ? this._initialWidth : 256;
}
/**
* @returns {number} default height
*/
getDefaultHeight() {
return this._initialHeight !== null ? this._initialHeight : 256;
}
/**
* @returns {number} defined scale
*/
getScale() {
return Number(this._getProperties().get('scale').getValue()) || 1;
}
onRemovedFromScene() {
super.onRemovedFromScene();
this._pixiObject.destroy({ children: true });
}
/**
* @returns this spine object configuration
*/
_getConfiguration() {
return gd.asSpineConfiguration(this._associatedObjectConfiguration);
}
/**
* @returns this object properties container
*/
_getProperties() {
return this._associatedObjectConfiguration.getProperties();
}
_loadSpine() {
const properties = this._getProperties();
const spineResourceName = properties
.get('spineResourceName')
.getValue();
this._pixiResourcesLoader
.getSpineData(this._project, spineResourceName)
.then((spineDataOrLoadingError) => {
if (!spineDataOrLoadingError.skeleton) {
console.error(
'Unable to load Spine (' +
(spineDataOrLoadingError.loadingErrorReason ||
'Unknown reason') +
')',
spineDataOrLoadingError.loadingError
);
this._spine = null;
return;
}
try {
this._spine = new PIXI.Spine(spineDataOrLoadingError.skeleton);
} catch (error) {
console.error('Exception while loading Spine.', error);
this._spine = null;
return;
}
this._pixiObject.addChild(this._spine);
this.update();
});
}
}
objectsRenderingService.registerInstanceRenderer(
'SpineObject::SpineObject',
RenderedSpineInstance
);
},
};

View File

@@ -0,0 +1,165 @@
/**
GDevelop - Spine Extension
Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
This project is released under the MIT License.
*/
#include "SpineObjectConfiguration.h"
#include "GDCore/CommonTools.h"
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
#include "GDCore/Project/InitialInstance.h"
#include "GDCore/Project/MeasurementUnit.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/PropertyDescriptor.h"
#include "GDCore/Serialization/SerializerElement.h"
#include "GDCore/Tools/Localization.h"
using namespace std;
SpineAnimation SpineObjectConfiguration::badAnimation;
SpineObjectConfiguration::SpineObjectConfiguration()
: scale(1), spineResourceName("") {};
bool SpineObjectConfiguration::UpdateProperty(const gd::String &propertyName, const gd::String &newValue) {
if (propertyName == "scale") {
scale = newValue.To<double>();
return true;
}
if (propertyName == "spineResourceName") {
spineResourceName = newValue;
return true;
}
return false;
}
std::map<gd::String, gd::PropertyDescriptor>
SpineObjectConfiguration::GetProperties() const {
std::map<gd::String, gd::PropertyDescriptor> objectProperties;
objectProperties["scale"]
.SetValue(gd::String::From(scale))
.SetType("number")
.SetLabel(_("Scale"))
.SetGroup(_("Default size"));
objectProperties["spineResourceName"]
.SetValue(spineResourceName)
.SetType("resource")
.AddExtraInfo("spine")
.SetLabel(_("Spine json"));
return objectProperties;
}
bool SpineObjectConfiguration::UpdateInitialInstanceProperty(
gd::InitialInstance &instance, const gd::String &propertyName,
const gd::String &newValue, gd::Project &project, gd::Layout &layout
) {
if (propertyName == "animation") {
instance.SetRawDoubleProperty("animation", std::max(0, newValue.empty() ? 0 : newValue.To<int>()));
}
return true;
}
std::map<gd::String, gd::PropertyDescriptor>
SpineObjectConfiguration::GetInitialInstanceProperties(const gd::InitialInstance &instance, gd::Project &project, gd::Layout &layout) {
std::map<gd::String, gd::PropertyDescriptor> properties;
properties["animation"] =
gd::PropertyDescriptor(gd::String::From(instance.GetRawDoubleProperty("animation")))
.SetLabel(_("Animation"))
.SetType("number");
return properties;
}
void SpineObjectConfiguration::DoUnserializeFrom(gd::Project &project, const gd::SerializerElement &element) {
auto &content = element.GetChild("content");
scale = content.GetDoubleAttribute("scale");
spineResourceName = content.GetStringAttribute("spineResourceName");
RemoveAllAnimations();
auto &animationsElement = content.GetChild("animations");
animationsElement.ConsiderAsArrayOf("animation");
for (std::size_t i = 0; i < animationsElement.GetChildrenCount(); ++i) {
auto &animationElement = animationsElement.GetChild(i);
SpineAnimation animation;
animation.SetName(animationElement.GetStringAttribute("name", ""));
animation.SetSource(animationElement.GetStringAttribute("source", ""));
animation.SetShouldLoop(animationElement.GetBoolAttribute("loop", false));
AddAnimation(animation);
}
}
void SpineObjectConfiguration::DoSerializeTo(gd::SerializerElement &element) const {
auto &content = element.AddChild("content");
content.SetAttribute("scale", scale);
content.SetAttribute("spineResourceName", spineResourceName);
auto &animationsElement = content.AddChild("animations");
animationsElement.ConsiderAsArrayOf("animation");
for (auto &animation : animations) {
auto &animationElement = animationsElement.AddChild("animation");
animationElement.SetAttribute("name", animation.GetName());
animationElement.SetAttribute("source", animation.GetSource());
animationElement.SetAttribute("loop", animation.ShouldLoop());
}
}
void SpineObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker &worker) {
worker.ExposeSpine(spineResourceName);
worker.ExposeEmbeddeds(spineResourceName);
}
const SpineAnimation &
SpineObjectConfiguration::GetAnimation(std::size_t nb) const {
if (nb >= animations.size()) return badAnimation;
return animations[nb];
}
SpineAnimation &SpineObjectConfiguration::GetAnimation(std::size_t nb) {
if (nb >= animations.size()) return badAnimation;
return animations[nb];
}
bool SpineObjectConfiguration::HasAnimationNamed(const gd::String &name) const {
return !name.empty() && (find_if(animations.begin(), animations.end(),
[&name](const SpineAnimation &animation) {
return animation.GetName() == name;
}) != animations.end());
}
void SpineObjectConfiguration::AddAnimation(const SpineAnimation &animation) {
animations.push_back(animation);
}
bool SpineObjectConfiguration::RemoveAnimation(std::size_t nb) {
if (nb >= GetAnimationsCount())
return false;
animations.erase(animations.begin() + nb);
return true;
}
void SpineObjectConfiguration::SwapAnimations(std::size_t firstIndex, std::size_t secondIndex) {
if (firstIndex < animations.size() && secondIndex < animations.size() && firstIndex != secondIndex) {
std::swap(animations[firstIndex], animations[secondIndex]);
}
}
void SpineObjectConfiguration::MoveAnimation(std::size_t oldIndex, std::size_t newIndex) {
if (oldIndex >= animations.size() || newIndex >= animations.size()) {
return;
}
auto animation = animations[oldIndex];
animations.erase(animations.begin() + oldIndex);
animations.insert(animations.begin() + newIndex, animation);
}

View File

@@ -0,0 +1,162 @@
/**
GDevelop - Spine Extension
Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
This project is released under the MIT License.
*/
#pragma once
#include "GDCore/Project/ObjectConfiguration.h"
namespace gd {
class InitialInstance;
class Project;
} // namespace gd
class GD_EXTENSION_API SpineAnimation {
public:
SpineAnimation() : shouldLoop(false) {};
virtual ~SpineAnimation(){};
/**
* \brief Return the name of the animation
*/
const gd::String &GetName() const { return name; }
/**
* \brief Change the name of the animation
*/
void SetName(const gd::String &name_) { name = name_; }
/**
* \brief Return the name of the animation from the spine file.
*/
const gd::String &GetSource() const { return source; }
/**
* \brief Change the name of the animation from the spine file.
*/
void SetSource(const gd::String &source_) { source = source_; }
/**
* \brief Return true if the animation should loop.
*/
const bool ShouldLoop() const { return shouldLoop; }
/**
* \brief Change whether the animation should loop or not.
*/
void SetShouldLoop(bool shouldLoop_) { shouldLoop = shouldLoop_; }
private:
gd::String name;
gd::String source;
bool shouldLoop;
};
/**
* \brief Spine object configuration is used for storage and for the IDE.
*/
class GD_EXTENSION_API SpineObjectConfiguration : public gd::ObjectConfiguration {
public:
SpineObjectConfiguration();
virtual ~SpineObjectConfiguration(){};
virtual std::unique_ptr<gd::ObjectConfiguration> Clone() const override {
return gd::make_unique<SpineObjectConfiguration>(*this);
}
virtual void ExposeResources(gd::ArbitraryResourceWorker &worker) override;
virtual std::map<gd::String, gd::PropertyDescriptor>GetProperties() const override;
virtual bool UpdateProperty(const gd::String &name, const gd::String &value) override;
virtual std::map<gd::String, gd::PropertyDescriptor>
GetInitialInstanceProperties(const gd::InitialInstance &instance,
gd::Project &project,
gd::Layout &layout) override;
virtual bool UpdateInitialInstanceProperty(gd::InitialInstance &instance,
const gd::String &name,
const gd::String &value,
gd::Project &project,
gd::Layout &layout) override;
/** \name Animations
* Methods related to animations management
*/
///@{
/**
* \brief Return the animation at the specified index.
* If the index is out of bound, a "bad animation" object is returned.
*/
const SpineAnimation &GetAnimation(std::size_t nb) const;
/**
* \brief Return the animation at the specified index.
* If the index is out of bound, a "bad animation" object is returned.
*/
SpineAnimation &GetAnimation(std::size_t nb);
/**
* \brief Return the number of animations this object has.
*/
std::size_t GetAnimationsCount() const { return animations.size(); };
/**
* \brief Return true if the animation called "name" exists.
*/
bool HasAnimationNamed(const gd::String& name) const;
/**
* \brief Add an animation at the end of the existing ones.
*/
void AddAnimation(const SpineAnimation &animation);
/**
* \brief Remove an animation.
*/
bool RemoveAnimation(std::size_t nb);
/**
* \brief Remove all animations.
*/
void RemoveAllAnimations() { animations.clear(); }
/**
* \brief Return true if the object hasn't any animation.
*/
bool HasNoAnimations() const { return animations.empty(); }
/**
* \brief Swap the position of two animations
*/
void SwapAnimations(std::size_t firstIndex, std::size_t secondIndex);
/**
* \brief Change the position of the specified animation
*/
void MoveAnimation(std::size_t oldIndex, std::size_t newIndex);
/**
* \brief Return a read-only reference to the vector containing all the
* animation of the object.
*/
const std::vector<SpineAnimation> &GetAllAnimations() const {
return animations;
}
///@}
protected:
virtual void DoUnserializeFrom(gd::Project &project, const gd::SerializerElement &element) override;
virtual void DoSerializeTo(gd::SerializerElement &element) const override;
private:
double scale;
gd::String spineResourceName;
std::vector<SpineAnimation> animations;
static SpineAnimation badAnimation;
};

View File

@@ -0,0 +1,198 @@
/*
* GDevelop JS Platform
* Copyright 2013-present Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
/** The callback called when a text that was requested is loaded (or an error occurred). */
export type SpineAtlasManagerRequestCallback = (
error: Error | null,
content?: pixi_spine.TextureAtlas
) => void;
const atlasKinds: ResourceKind[] = ['atlas'];
/**
* AtlasManager loads atlas files with pixi loader, using the "atlas" resources
* registered in the game resources and process them to Pixi TextureAtlas.
*
* Contrary to audio/fonts, text files are loaded asynchronously, when requested.
* You should properly handle errors, and give the developer/player a way to know
* that loading failed.
*/
export class SpineAtlasManager implements gdjs.ResourceManager {
private _imageManager: ImageManager;
private _resourceLoader: ResourceLoader;
private _loadedSpineAtlases = new gdjs.ResourceCache<
pixi_spine.TextureAtlas
>();
private _loadingSpineAtlases = new gdjs.ResourceCache<
Promise<pixi_spine.TextureAtlas>
>();
/**
* @param resources The resources data of the game.
* @param resourcesLoader The resources loader of the game.
*/
constructor(
resourceLoader: gdjs.ResourceLoader,
imageManager: ImageManager
) {
this._resourceLoader = resourceLoader;
this._imageManager = imageManager;
}
getResourceKinds(): ResourceKind[] {
return atlasKinds;
}
async processResource(resourceName: string): Promise<void> {
// Do nothing because pixi-spine parses resources by itself.
}
async loadResource(resourceName: string): Promise<void> {
await this.getOrLoad(resourceName);
}
/**
* Returns promisified loaded atlas resource if it is availble, loads it otherwise.
*
* @param resources The data of resource to load.
*/
getOrLoad(resourceName: string): Promise<pixi_spine.TextureAtlas> {
const resource = this._getAtlasResource(resourceName);
if (!resource) {
return Promise.reject(
`Unable to find atlas for resource '${resourceName}'.`
);
}
let loadingPromise = this._loadingSpineAtlases.get(resource);
if (!loadingPromise) {
loadingPromise = new Promise<pixi_spine.TextureAtlas>(
(resolve, reject) => {
const onLoad: SpineAtlasManagerRequestCallback = (
error,
content
) => {
if (error) {
return reject(
`Error while preloading a spine atlas resource: ${error}`
);
}
if (!content) {
return reject(
`Cannot reach texture atlas for resource '${resourceName}'.`
);
}
resolve(content);
};
this.load(resource, onLoad);
}
);
this._loadingSpineAtlases.set(resource, loadingPromise);
}
return loadingPromise;
}
/**
* Load specified atlas resource and pass it to callback once it is loaded.
*
* @param resources The data of resource to load.
* @param callback The callback to pass atlas to it once it is loaded.
*/
load(
resource: ResourceData,
callback: SpineAtlasManagerRequestCallback
): void {
const game = this._resourceLoader.getRuntimeGame();
const embeddedResourcesNames = game.getEmbeddedResourcesNames(
resource.name
);
if (!embeddedResourcesNames.length)
return callback(
new Error(`${resource.name} do not have image metadata!`)
);
const images = embeddedResourcesNames.reduce<{
[key: string]: PIXI.Texture;
}>((imagesMap, embeddedResourceName) => {
const mappedResourceName = game.resolveEmbeddedResource(
resource.name,
embeddedResourceName
);
imagesMap[
embeddedResourceName
] = this._imageManager.getOrLoadPIXITexture(mappedResourceName);
return imagesMap;
}, {});
const onLoad = (atlas: pixi_spine.TextureAtlas) => {
this._loadedSpineAtlases.set(resource, atlas);
callback(null, atlas);
};
const url = this._resourceLoader.getFullUrl(resource.file);
PIXI.Assets.setPreferences({
preferWorkers: false,
crossOrigin: this._resourceLoader.checkIfCredentialsRequired(url)
? 'use-credentials'
: 'anonymous',
});
PIXI.Assets.add(resource.name, url, { images });
PIXI.Assets.load<pixi_spine.TextureAtlas | string>(resource.name).then(
(atlas) => {
/**
* Ideally atlas of TextureAtlas should be passed here
* but there is known issue in case of preloaded images (see https://github.com/pixijs/spine/issues/537)
*
* Here covered all possible ways to make it work fine if issue is fixed in pixi-spine or after migration to spine-pixi
*/
if (typeof atlas === 'string') {
new pixi_spine.TextureAtlas(
atlas,
(textureName, textureCb) =>
textureCb(images[textureName].baseTexture),
onLoad
);
} else {
onLoad(atlas);
}
}
);
}
/**
* Check if the given atlas resource was loaded (preloaded or loaded with `load`).
* @param resourceName The name of the atlas resource.
* @returns true if the content of the atlas resource is loaded, false otherwise.
*/
isLoaded(resourceName: string): boolean {
return !!this._loadedSpineAtlases.getFromName(resourceName);
}
/**
* Get the Pixi TextureAtlas for the given resource that is already loaded (preloaded or loaded with `load`).
* If the resource is not loaded, `null` will be returned.
* @param resourceName The name of the atlas resource.
* @returns the TextureAtlas of the atlas if loaded, `null` otherwise.
*/
getAtlasTexture(resourceName: string): pixi_spine.TextureAtlas | null {
return this._loadedSpineAtlases.getFromName(resourceName);
}
private _getAtlasResource(resourceName: string): ResourceData | null {
const resource = this._resourceLoader.getResource(resourceName);
return resource && this.getResourceKinds().includes(resource.kind)
? resource
: null;
}
}
}

View File

@@ -0,0 +1,119 @@
/*
* GDevelop JS Platform
* Copyright 2013-present Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
const logger = new gdjs.Logger('Spine Manager');
const resourceKinds: ResourceKind[] = ['spine'];
/**
* SpineManager manages pixi spine skeleton data.
*/
export class SpineManager implements gdjs.ResourceManager {
private _spineAtlasManager: SpineAtlasManager;
private _resourceLoader: ResourceLoader;
private _loadedSpines = new gdjs.ResourceCache<pixi_spine.ISkeletonData>();
/**
* @param resourceDataArray The resources data of the game.
* @param resourcesLoader The resources loader of the game.
*/
constructor(
resourceLoader: gdjs.ResourceLoader,
spineAtlasManager: SpineAtlasManager
) {
this._resourceLoader = resourceLoader;
this._spineAtlasManager = spineAtlasManager;
}
getResourceKinds(): ResourceKind[] {
return resourceKinds;
}
async processResource(resourceName: string): Promise<void> {
// Do nothing because pixi-spine parses resources by itself.
}
async loadResource(resourceName: string): Promise<void> {
const resource = this._getSpineResource(resourceName);
if (!resource) {
return logger.error(
`Unable to find spine json for resource ${resourceName}.`
);
}
try {
const game = this._resourceLoader.getRuntimeGame();
const embeddedResourcesNames = game.getEmbeddedResourcesNames(
resource.name
);
// there should be exactly one file which is pointing to atlas
if (embeddedResourcesNames.length !== 1) {
return logger.error(
`Unable to find atlas metadata for resource spine json ${resourceName}.`
);
}
const atlasResourceName = game.resolveEmbeddedResource(
resource.name,
embeddedResourcesNames[0]
);
const spineAtlas = await this._spineAtlasManager.getOrLoad(
atlasResourceName
);
const url = this._resourceLoader.getFullUrl(resource.file);
PIXI.Assets.setPreferences({
preferWorkers: false,
crossOrigin: this._resourceLoader.checkIfCredentialsRequired(url)
? 'use-credentials'
: 'anonymous',
});
PIXI.Assets.add(resource.name, url, { spineAtlas });
const loadedJson = await PIXI.Assets.load(resource.name);
if (loadedJson.spineData) {
this._loadedSpines.set(resource, loadedJson.spineData);
} else {
logger.error(
`Loader cannot process spine resource ${resource.name} correctly.`
);
}
} catch (error) {
logger.error(
`Error while preloading spine resource ${resource.name}: ${error}`
);
}
}
/**
* Get the object for the given resource that is already loaded (preloaded or loaded with `loadJson`).
* If the resource is not loaded, `null` will be returned.
*
* @param resourceName The name of the spine skeleton.
* @returns the spine skeleton if loaded, `null` otherwise.
*/
getSpine(resourceName: string): pixi_spine.ISkeletonData | null {
return this._loadedSpines.getFromName(resourceName);
}
/**
* Check if the given spine skeleton was loaded.
* @param resourceName The name of the spine skeleton.
* @returns true if the content of the spine skeleton is loaded, false otherwise.
*/
isSpineLoaded(resourceName: string): boolean {
return !!this._loadedSpines.getFromName(resourceName);
}
private _getSpineResource(resourceName: string): ResourceData | null {
const resource = this._resourceLoader.getResource(resourceName);
return resource && this.getResourceKinds().includes(resource.kind)
? resource
: null;
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,202 @@
namespace gdjs {
const isSpine = (obj: any): obj is pixi_spine.Spine =>
obj instanceof pixi_spine.Spine;
export class SpineRuntimeObjectPixiRenderer {
private _object: gdjs.SpineRuntimeObject;
private _rendererObject: pixi_spine.Spine | PIXI.Container;
private _isAnimationComplete = true;
/**
* @param runtimeObject The object to render
* @param instanceContainer The container in which the object is
*/
constructor(
runtimeObject: gdjs.SpineRuntimeObject,
private instanceContainer: gdjs.RuntimeInstanceContainer
) {
this._object = runtimeObject;
this._rendererObject = this.constructRendererObject();
if (isSpine(this._rendererObject)) {
this._rendererObject.autoUpdate = false;
}
this.updatePosition();
this.updateAngle();
this.updateOpacity();
this.updateScale();
instanceContainer
.getLayer('')
.getRenderer()
.addRendererObject(this._rendererObject, runtimeObject.getZOrder());
}
updateAnimation(timeDelta: float) {
if (!isSpine(this._rendererObject)) {
return;
}
this._rendererObject.update(timeDelta);
}
getRendererObject(): pixi_spine.Spine | PIXI.Container {
return this._rendererObject;
}
getOriginOffset(): PIXI.Point {
if (!isSpine(this._rendererObject)) return new PIXI.Point(0, 0);
const localBounds = this._rendererObject.getLocalBounds(undefined, true);
return new PIXI.Point(
localBounds.x * this._rendererObject.scale.x,
localBounds.y * this._rendererObject.scale.y
);
}
onDestroy(): void {
this._rendererObject.destroy();
}
updateScale(): void {
const scaleX = Math.max(
this._object._originalScale * this._object.getScaleX(),
0
);
const scaleY = Math.max(
this._object._originalScale * this._object.getScaleY(),
0
);
this._rendererObject.scale.x = this._object.isFlippedX()
? -scaleX
: scaleX;
this._rendererObject.scale.y = this._object.isFlippedY()
? -scaleY
: scaleY;
}
updatePosition(): void {
this._rendererObject.position.x = this._object.x;
this._rendererObject.position.y = this._object.y;
}
updateAngle(): void {
this._rendererObject.rotation = gdjs.toRad(this._object.angle);
}
updateOpacity(): void {
this._rendererObject.alpha = this._object.getOpacity() / 255;
}
getWidth(): float {
return this._rendererObject.width;
}
getHeight(): float {
return this._rendererObject.height;
}
setWidth(width: float): void {
this._rendererObject.width = width;
}
setHeight(height: float): void {
this._rendererObject.height = height;
}
getUnscaledWidth(): float {
return Math.abs(
(this._rendererObject.width * this._object._originalScale) /
this._rendererObject.scale.x
);
}
getUnscaledHeight(): float {
return Math.abs(
(this._rendererObject.height * this._object._originalScale) /
this._rendererObject.scale.y
);
}
setMixing(from: string, to: string, duration: number): void {
if (!isSpine(this._rendererObject)) return;
this._rendererObject.stateData.setMix(from, to, duration);
}
setAnimation(animation: string, loop: boolean): void {
if (isSpine(this._rendererObject)) {
const onCompleteListener: pixi_spine.IAnimationStateListener = {
complete: () => {
this._isAnimationComplete = true;
(this._rendererObject as pixi_spine.Spine).state.removeListener(
onCompleteListener
);
},
};
this._isAnimationComplete = false;
this._rendererObject.state.addListener(onCompleteListener);
this._rendererObject.state.setAnimation(0, animation, loop);
this._rendererObject.update(0);
}
}
getAnimationDuration(sourceAnimationName: string) {
if (!isSpine(this._rendererObject)) {
return 0;
}
const animation = this._rendererObject.spineData.findAnimation(
sourceAnimationName
);
return animation ? animation.duration : 0;
}
getAnimationElapsedTime(): number {
if (!isSpine(this._rendererObject)) {
return 0;
}
const tracks = this._rendererObject.state.tracks;
if (tracks.length === 0) {
return 0;
}
// This should be fine because only 1 track is used.
const track = tracks[0];
// @ts-ignore TrackEntry.getAnimationTime is not exposed.
return track.getAnimationTime();
}
setAnimationElapsedTime(time: number): void {
if (!isSpine(this._rendererObject)) {
return;
}
const tracks = this._rendererObject.state.tracks;
if (tracks.length === 0) {
return;
}
const track = tracks[0];
track.trackTime = time;
}
isAnimationComplete(): boolean {
return this._isAnimationComplete;
}
private constructRendererObject(): pixi_spine.Spine | PIXI.Container {
const game = this.instanceContainer.getGame();
const spineManager = game.getSpineManager();
if (
!spineManager ||
!spineManager.isSpineLoaded(this._object.spineResourceName)
) {
return new PIXI.Container();
}
return new pixi_spine.Spine(
spineManager.getSpine(this._object.spineResourceName)!
);
}
}
export const SpineRuntimeObjectRenderer = SpineRuntimeObjectPixiRenderer;
}

View File

@@ -0,0 +1,406 @@
namespace gdjs {
type SpineAnimation = { name: string; source: string; loop: boolean };
export type SpineObjectDataType = {
content: {
opacity: float;
scale: float;
timeScale: float;
spineResourceName: string;
animations: SpineAnimation[];
};
};
export type SpineObjectData = ObjectData & SpineObjectDataType;
export class SpineRuntimeObject
extends gdjs.RuntimeObject
implements
gdjs.Resizable,
gdjs.Scalable,
gdjs.Animatable,
gdjs.OpacityHandler {
private _opacity: float = 255;
private _scaleX: number = 1;
private _scaleY: number = 1;
_originalScale: number;
private _flippedX: boolean = false;
private _flippedY: boolean = false;
private _animations: SpineAnimation[];
private _currentAnimationIndex = -1;
private _animationSpeedScale: float = 1;
private _animationPaused: boolean = false;
private _isPausedFrameDirty = false;
/** The duration in second for the smooth transition between 2 animations */
private _animationMixingDuration: number;
private _renderer: gdjs.SpineRuntimeObjectPixiRenderer;
readonly spineResourceName: string;
/**
* @param instanceContainer The container the object belongs to.
* @param objectData The object data used to initialize the object
*/
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
objectData: SpineObjectData
) {
super(instanceContainer, objectData);
this._animations = objectData.content.animations;
this._originalScale = objectData.content.scale;
this.spineResourceName = objectData.content.spineResourceName;
this._animationMixingDuration = 0.1;
this._renderer = new gdjs.SpineRuntimeObjectRenderer(
this,
instanceContainer
);
this.setAnimationIndex(0);
this._renderer.updateAnimation(0);
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
if (this._animationPaused) {
if (this._isPausedFrameDirty) {
this._renderer.updateAnimation(0);
this.invalidateHitboxes();
this._isPausedFrameDirty = false;
}
return;
}
const elapsedTime = this.getElapsedTime() / 1000;
this._renderer.updateAnimation(elapsedTime * this._animationSpeedScale);
this.invalidateHitboxes();
}
getRendererObject(): pixi_spine.Spine | PIXI.Container {
return this._renderer.getRendererObject();
}
updateFromObjectData(
oldObjectData: SpineObjectData,
newObjectData: SpineObjectData
): boolean {
super.updateFromObjectData(oldObjectData, newObjectData);
if (oldObjectData.content.scale !== newObjectData.content.scale) {
this._originalScale = newObjectData.content.scale;
this._renderer.updateScale();
this.invalidateHitboxes();
}
return true;
}
extraInitializationFromInitialInstance(
initialInstanceData: InstanceData
): void {
const animationData = initialInstanceData.numberProperties.find(
(data) => data.name === 'animation'
);
const animationIndex = animationData
? animationData.value
: this._currentAnimationIndex;
this.setAnimationIndexWithMixing(animationIndex, 0);
if (initialInstanceData.customSize) {
this.setSize(initialInstanceData.width, initialInstanceData.height);
this.invalidateHitboxes();
}
}
getDrawableX(): number {
const originOffset = this._renderer.getOriginOffset();
return this.getX() + originOffset.x;
}
getDrawableY(): number {
const originOffset = this._renderer.getOriginOffset();
return this.getY() + originOffset.y;
}
onDestroyed(): void {
super.onDestroyed();
this._renderer.onDestroy();
}
setX(x: float): void {
super.setX(x);
this._renderer.updatePosition();
}
setY(y: float): void {
super.setY(y);
this._renderer.updatePosition();
}
setAngle(angle: float): void {
super.setAngle(angle);
this._renderer.updateAngle();
}
setOpacity(opacity: float): void {
this._opacity = Math.max(0, Math.min(255, opacity));
this._renderer.updateOpacity();
}
getOpacity(): float {
return this._opacity;
}
getWidth(): float {
return this._renderer.getWidth();
}
getHeight(): float {
return this._renderer.getHeight();
}
setWidth(newWidth: float): void {
const unscaledWidth = this._renderer.getUnscaledWidth();
if (unscaledWidth !== 0) {
this.setScaleX(newWidth / unscaledWidth);
}
}
setHeight(newHeight: float): void {
const unscaledHeight = this._renderer.getUnscaledHeight();
if (unscaledHeight !== 0) {
this.setScaleY(newHeight / unscaledHeight);
}
}
setSize(newWidth: number, newHeight: number): void {
this.setWidth(newWidth);
this.setHeight(newHeight);
}
setScale(newScale: float): void {
if (newScale < 0) {
newScale = 0;
}
if (
newScale === Math.abs(this._scaleX) &&
newScale === Math.abs(this._scaleY)
) {
return;
}
this._scaleX = newScale * (this._flippedX ? -1 : 1);
this._scaleY = newScale * (this._flippedY ? -1 : 1);
this._renderer.updateScale();
this.invalidateHitboxes();
}
setScaleX(newScale: float): void {
if (newScale < 0) {
newScale = 0;
}
if (newScale === Math.abs(this._scaleX)) {
return;
}
this._scaleX = newScale * (this._flippedX ? -1 : 1);
this._renderer.updateScale();
this.invalidateHitboxes();
}
setScaleY(newScale: float): void {
if (newScale < 0) {
newScale = 0;
}
if (newScale === Math.abs(this._scaleY)) {
return;
}
this._scaleY = newScale * (this._flippedY ? -1 : 1);
this._renderer.updateScale();
this.invalidateHitboxes();
}
/**
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
*
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
*/
getScale(): float {
const scaleX = Math.abs(this._scaleX);
const scaleY = Math.abs(this._scaleY);
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
}
getScaleY(): float {
return Math.abs(this._scaleY);
}
getScaleX(): float {
return Math.abs(this._scaleX);
}
isFlippedX(): boolean {
return this._flippedX;
}
isFlippedY(): boolean {
return this._flippedY;
}
flipX(enable: boolean) {
if (enable !== this._flippedX) {
this._scaleX *= -1;
this._flippedX = enable;
this.invalidateHitboxes();
this._renderer.updateScale();
}
}
flipY(enable: boolean) {
if (enable !== this._flippedY) {
this._scaleY *= -1;
this._flippedY = enable;
this.invalidateHitboxes();
this._renderer.updateScale();
}
}
setAnimationIndex(animationIndex: number): void {
this.setAnimationIndexWithMixing(
animationIndex,
this._animationMixingDuration
);
}
setAnimationIndexWithMixing(
animationIndex: number,
mixingDuration: number
): void {
if (
this._animations.length === 0 ||
this._currentAnimationIndex === animationIndex ||
!this.isAnimationIndex(animationIndex)
) {
return;
}
const previousAnimation = this._animations[this._currentAnimationIndex];
const newAnimation = this._animations[animationIndex];
this._currentAnimationIndex = animationIndex;
if (previousAnimation) {
this._renderer.setMixing(
previousAnimation.source,
newAnimation.source,
mixingDuration
);
}
this._renderer.setAnimation(newAnimation.source, newAnimation.loop);
this._isPausedFrameDirty = true;
}
setAnimationName(animationName: string): void {
this.setAnimationNameWithMixing(
animationName,
this._animationMixingDuration
);
}
setAnimationNameWithMixing(
animationName: string,
mixingDuration: number
): void {
this.setAnimationIndexWithMixing(
this.getAnimationIndexFor(animationName),
mixingDuration
);
}
getAnimationIndexFor(animationName: string): number {
return this._animations.findIndex(
(animation) => animation.name === animationName
);
}
/**
* Return the duration in second for the smooth transition between 2 animations.
*/
getAnimationMixingDuration(): number {
return this._animationMixingDuration;
}
/**
* Change the duration in second for the smooth transition between 2 animations.
*/
setAnimationMixingDuration(animationMixingDuration: number): void {
this._animationMixingDuration = animationMixingDuration;
}
getAnimationIndex(): number {
return this._currentAnimationIndex;
}
getAnimationName(): string {
return this.isAnimationIndex(this._currentAnimationIndex)
? this._animations[this._currentAnimationIndex].name
: '';
}
isAnimationIndex(animationIndex: number): boolean {
return (
Number.isInteger(animationIndex) &&
animationIndex >= 0 &&
animationIndex < this._animations.length
);
}
hasAnimationEnded(): boolean {
return this._renderer.isAnimationComplete();
}
isAnimationPaused() {
return this._animationPaused;
}
pauseAnimation() {
this._animationPaused = true;
}
resumeAnimation() {
this._animationPaused = false;
}
getAnimationSpeedScale() {
return this._animationSpeedScale;
}
setAnimationSpeedScale(ratio: float): void {
this._animationSpeedScale = ratio;
}
getAnimationElapsedTime(): number {
if (this._animations.length === 0) {
return 0;
}
return this._renderer.getAnimationElapsedTime();
}
setAnimationElapsedTime(time: number): void {
if (this._animations.length === 0) {
return;
}
this._renderer.setAnimationElapsedTime(time);
this._isPausedFrameDirty = true;
}
getAnimationDuration(): number {
if (this._animations.length === 0) {
return 0;
}
return this._renderer.getAnimationDuration(
this._animations[this._currentAnimationIndex].source
);
}
}
gdjs.registerObject('SpineObject::SpineObject', gdjs.SpineRuntimeObject);
}

View File

@@ -383,7 +383,7 @@ module.exports = {
'res/actions/font24.png',
'res/actions/font.png'
)
.addParameter('object', _('Bitmap text'), 'TextInputObject', false)
.addParameter('object', _('Text input'), 'TextInputObject', false)
.addParameter('fontResource', _('Font resource name'), '', false)
.getCodeExtraInformation()
.setFunctionName('setFontResourceName');
@@ -400,9 +400,23 @@ module.exports = {
)
.addParameter('object', _('Text input'), 'TextInputObject', false)
.useStandardParameters(
'string',
gd.ParameterOptions.makeNewOptions().setDescription(_('Input type'))
) // TODO: stringWithSelector?
'stringWithSelector',
gd.ParameterOptions.makeNewOptions()
.setDescription(_('Input type'))
.setTypeExtraInfo(
JSON.stringify([
'text',
'text area',
'email',
'password',
'number',
'telephone number',
'url',
'search',
'email',
])
)
)
.setFunctionName('setInputType')
.setGetter('getInputType');

View File

@@ -204,6 +204,7 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
.AddParameter("color", _("Third Color"))
.AddParameter("color", _("Fourth Color"));
// Deprecated
obj.AddAction("SetOutline",
_("Outline"),
_("Change the outline of the text. A thickness of 0 disables "
@@ -213,20 +214,63 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
_("Effects"),
"res/actions/textOutline24.png",
"res/actions/textOutline.png")
.SetHidden()
.AddParameter("object", _("Object"), "Text")
.AddParameter("color", _("Color"))
.AddParameter("expression", _("Thickness"));
obj.AddScopedAction("SetOutlineEnabled",
_("Enable outline"),
_("Enable or disable the outline of the text."),
_("Enable the outline of _PARAM0_: _PARAM1_"),
_("Outline"),
"res/actions/textOutline24.png",
"res/actions/textOutline.png")
.AddParameter("object", _("Object"), "Text")
.AddParameter("yesorno", _("Enable outline"), "", true)
.SetDefaultValue("yes");
obj.AddScopedCondition("IsOutlineEnabled",
_("Outline enabled"),
_("Check if the text outline is enabled."),
_("The outline of _PARAM0_ is enabled"),
_("Outline"),
"res/actions/textOutline24.png",
"res/actions/textOutline.png")
.AddParameter("object", _("Object"), "Text");
obj.AddScopedAction("SetOutlineColor",
_("Outline color"),
_("Change the outline color of the text."),
_("Change the text outline color of _PARAM0_ to _PARAM1_"),
_("Outline"),
"res/actions/textOutline24.png",
"res/actions/textOutline.png")
.AddParameter("object", _("Object"), "Text")
.AddParameter("color", _("Color"));
obj.AddExpressionAndConditionAndAction("number", "OutlineThickness",
_("Outline thickness"),
_("the outline thickness of the text"),
_("the text outline thickness"),
_("Outline"),
"res/actions/textOutline24.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Thickness")));
// Deprecated
obj.AddAction("SetShadow",
_("Text shadow"),
_("Change the shadow of the text."),
_("Change the shadow of _PARAM0_ to color _PARAM1_ distance "
"_PARAM2_ blur _PARAM3_ angle _PARAM4_"),
_("Effects/Shadow"),
_("Shadow"),
"res/actions/textShadow24.png",
"res/actions/textShadow.png")
.SetHidden()
.AddParameter("object", _("Object"), "Text")
.AddParameter("color", _("Color"))
.AddParameter("expression", _("Distance"))
@@ -234,15 +278,82 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
.AddParameter("expression", _("Angle"));
obj.AddAction("ShowShadow",
_("Show Shadow"),
_("Show the shadow of the text."),
_("Show the shadow of _PARAM0_: _PARAM1_"),
_("Effects/Shadow"),
_("Enable shadow"),
_("Enable or disable the shadow of the text."),
_("Enable the shadow of _PARAM0_: _PARAM1_"),
_("Shadow"),
"res/actions/textShadow24.png",
"res/actions/textShadow.png")
.AddParameter("object", _("Object"), "Text")
.AddParameter("yesorno", _("Show the shadow"));
.AddParameter("yesorno", _("Show the shadow"), "", true)
.SetDefaultValue("yes");
obj.AddScopedCondition("IsShadowEnabled",
_("Shadow enabled"),
_("Check if the text shadow is enabled."),
_("The shadow of _PARAM0_ is enabled"),
_("Shadow"),
"res/actions/textShadow24.png",
"res/actions/textShadow.png")
.AddParameter("object", _("Object"), "Text");
obj.AddScopedAction("SetShadowColor",
_("Shadow color"),
_("Change the shadow color of the text."),
_("Change the shadow color of _PARAM0_ to _PARAM1_"),
_("Shadow"),
"res/actions/textShadow24.png",
"res/actions/textShadow.png")
.AddParameter("object", _("Object"), "Text")
.AddParameter("color", _("Color"));
obj.AddExpressionAndConditionAndAction("number", "ShadowOpacity",
_("Shadow opacity"),
_("the shadow opacity of the text"),
_("the shadow opacity "),
_("Shadow"),
"res/actions/textShadow24.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Opacity (0 - 255)")));
obj.AddExpressionAndConditionAndAction("number", "ShadowDistance",
_("Shadow distance"),
_("the shadow distance of the text"),
_("the shadow distance "),
_("Shadow"),
"res/actions/textShadow24.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Distance")));
obj.AddExpressionAndConditionAndAction("number", "ShadowAngle",
_("Shadow angle"),
_("the shadow angle of the text"),
_("the shadow angle "),
_("Shadow"),
"res/actions/textShadow24.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Angle (in degrees)")));
obj.AddExpressionAndConditionAndAction("number", "ShadowBlurRadius",
_("Shadow blur radius"),
_("the shadow blur radius of the text"),
_("the shadow blur radius "),
_("Shadow"),
"res/actions/textShadow24.png")
.AddParameter("object", _("Object"), "Text")
.UseStandardParameters(
"number",
gd::ParameterOptions::MakeNewOptions().SetDescription(
_("Blur radius")));
// Deprecated
obj.AddAction("Opacity",

View File

@@ -127,10 +127,60 @@ class TextObjectJsExtension : public gd::PlatformExtension {
GetAllActionsForObject("TextObject::Text")["TextObject::SetOutline"]
.SetFunctionName("setOutline");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetOutlineEnabled"]
.SetFunctionName("setOutlineEnabled");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::IsOutlineEnabled"]
.SetFunctionName("isOutlineEnabled");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetOutlineColor"]
.SetFunctionName("setOutlineColor");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::OutlineThickness"]
.SetFunctionName("getOutlineThickness");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::OutlineThickness"]
.SetFunctionName("getOutlineThickness");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetOutlineThickness"]
.SetFunctionName("setOutlineThickness")
.SetGetter("getOutlineThickness");
GetAllActionsForObject("TextObject::Text")["TextObject::SetShadow"]
.SetFunctionName("setShadow");
GetAllActionsForObject("TextObject::Text")["TextObject::ShowShadow"]
.SetFunctionName("showShadow");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::IsShadowEnabled"]
.SetFunctionName("isShadowEnabled");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetShadowColor"]
.SetFunctionName("setShadowColor");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::ShadowOpacity"]
.SetFunctionName("getShadowOpacity");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::ShadowOpacity"]
.SetFunctionName("getShadowOpacity");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetShadowOpacity"]
.SetFunctionName("setShadowOpacity")
.SetGetter("getShadowOpacity");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::ShadowDistance"]
.SetFunctionName("getShadowDistance");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::ShadowDistance"]
.SetFunctionName("getShadowDistance");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetShadowDistance"]
.SetFunctionName("setShadowDistance")
.SetGetter("getShadowDistance");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::ShadowAngle"]
.SetFunctionName("getShadowAngle");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::ShadowAngle"]
.SetFunctionName("getShadowAngle");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetShadowAngle"]
.SetFunctionName("setShadowAngle")
.SetGetter("getShadowAngle");
GetAllExpressionsForObject("TextObject::Text")["TextObject::Text::ShadowBlurRadius"]
.SetFunctionName("getShadowBlurRadius");
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::ShadowBlurRadius"]
.SetFunctionName("getShadowBlurRadius");
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetShadowBlurRadius"]
.SetFunctionName("setShadowBlurRadius")
.SetGetter("getShadowBlurRadius");
// Unimplemented actions and conditions:
GetAllActionsForObject("TextObject::Text")["TextObject::Font"]

View File

@@ -12,10 +12,8 @@ This project is released under the MIT License.
#include "GDCore/Serialization/SerializerElement.h"
#include "TextObject.h"
#if defined(GD_IDE_ONLY)
#include "GDCore/IDE/AbstractFileSystem.h"
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
#endif
using namespace std;
@@ -27,10 +25,17 @@ TextObject::TextObject()
bold(false),
italic(false),
underlined(false),
colorR(0),
colorG(0),
colorB(0),
textAlignment("left")
color("0;0;0"),
textAlignment("left"),
isOutlineEnabled(false),
outlineThickness(2),
outlineColor("255;255;255"),
isShadowEnabled(false),
shadowColor("0;0;0"),
shadowOpacity(127),
shadowAngle(90),
shadowDistance(4),
shadowBlurRadius(2)
{
}
@@ -38,41 +43,98 @@ TextObject::~TextObject(){};
void TextObject::DoUnserializeFrom(gd::Project& project,
const gd::SerializerElement& element) {
SetString(element.GetChild("string", 0, "String").GetValue().GetString());
SetFontName(element.GetChild("font", 0, "Font").GetValue().GetString());
SetTextAlignment(element.GetChild("textAlignment").GetValue().GetString());
SetCharacterSize(element.GetChild("characterSize", 0, "CharacterSize")
// Compatibility with GD <= 5.3.188
// end of compatibility code
bool isLegacy = !element.HasChild("content");
auto &content = isLegacy ? element : element.GetChild("content");
SetFontName(content.GetChild("font", 0, "Font").GetValue().GetString());
SetTextAlignment(content.GetChild("textAlignment").GetValue().GetString());
SetCharacterSize(content.GetChild("characterSize", 0, "CharacterSize")
.GetValue()
.GetInt());
SetColor(element.GetChild("color", 0, "Color").GetIntAttribute("r", 255),
element.GetChild("color", 0, "Color").GetIntAttribute("g", 255),
element.GetChild("color", 0, "Color").GetIntAttribute("b", 255));
smoothed = content.GetBoolAttribute("smoothed");
bold = content.GetBoolAttribute("bold");
italic = content.GetBoolAttribute("italic");
underlined = content.GetBoolAttribute("underlined");
smoothed = element.GetBoolAttribute("smoothed");
bold = element.GetBoolAttribute("bold");
italic = element.GetBoolAttribute("italic");
underlined = element.GetBoolAttribute("underlined");
// Compatibility with GD <= 5.3.188
if (isLegacy) {
SetText(content.GetChild("string", 0, "String").GetValue().GetString());
SetColor(
gd::String::From(
content.GetChild("color", 0, "Color").GetIntAttribute("r", 255)) +
";" +
gd::String::From(
content.GetChild("color", 0, "Color").GetIntAttribute("g", 255)) +
";" +
gd::String::From(
content.GetChild("color", 0, "Color").GetIntAttribute("b", 255)));
} else
// end of compatibility code
{
SetText(content.GetStringAttribute("text"));
SetColor(content.GetStringAttribute("color", "0;0;0"));
SetOutlineEnabled(content.GetBoolAttribute("isOutlineEnabled", false));
SetOutlineThickness(content.GetIntAttribute("outlineThickness", 2));
SetOutlineColor(content.GetStringAttribute("outlineColor", "255;255;255"));
SetShadowEnabled(content.GetBoolAttribute("isShadowEnabled", false));
SetShadowColor(content.GetStringAttribute("shadowColor", "0;0;0"));
SetShadowOpacity(content.GetIntAttribute("shadowOpacity", 127));
SetShadowAngle(content.GetIntAttribute("shadowAngle", 90));
SetShadowDistance(content.GetIntAttribute("shadowDistance", 4));
SetShadowBlurRadius(content.GetIntAttribute("shadowBlurRadius", 2));
}
}
#if defined(GD_IDE_ONLY)
void TextObject::DoSerializeTo(gd::SerializerElement& element) const {
element.AddChild("string").SetValue(GetString());
element.AddChild("font").SetValue(GetFontName());
element.AddChild("textAlignment").SetValue(GetTextAlignment());
element.AddChild("characterSize").SetValue(GetCharacterSize());
element.AddChild("color")
.SetAttribute("r", (int)GetColorR())
.SetAttribute("g", (int)GetColorG())
.SetAttribute("b", (int)GetColorB());
// Allow users to rollback to 5.3.188 or older releases without loosing their configuration.
// TODO Remove this in a few releases.
// Compatibility with GD <= 5.3.188
{
element.AddChild("string").SetValue(GetText());
element.AddChild("font").SetValue(GetFontName());
element.AddChild("textAlignment").SetValue(GetTextAlignment());
element.AddChild("characterSize").SetValue(GetCharacterSize());
auto colorComponents = GetColor().Split(';');
element.AddChild("color")
.SetAttribute("r", colorComponents.size() == 3 ? colorComponents[0].To<int>() : 0)
.SetAttribute("g", colorComponents.size() == 3 ? colorComponents[1].To<int>() : 0)
.SetAttribute("b", colorComponents.size() == 3 ? colorComponents[2].To<int>() : 0);
element.SetAttribute("smoothed", smoothed);
element.SetAttribute("bold", bold);
element.SetAttribute("italic", italic);
element.SetAttribute("underlined", underlined);
}
// end of compatibility code
auto& content = element.AddChild("content");
content.AddChild("text").SetValue(GetText());
content.AddChild("font").SetValue(GetFontName());
content.AddChild("textAlignment").SetValue(GetTextAlignment());
content.AddChild("characterSize").SetValue(GetCharacterSize());
content.AddChild("color").SetValue(GetColor());
element.SetAttribute("smoothed", smoothed);
element.SetAttribute("bold", bold);
element.SetAttribute("italic", italic);
element.SetAttribute("underlined", underlined);
content.SetAttribute("smoothed", smoothed);
content.SetAttribute("bold", bold);
content.SetAttribute("italic", italic);
content.SetAttribute("underlined", underlined);
content.SetAttribute("isOutlineEnabled", isOutlineEnabled);
content.SetAttribute("outlineThickness", outlineThickness);
content.SetAttribute("outlineColor", outlineColor);
content.SetAttribute("isShadowEnabled", isShadowEnabled);
content.SetAttribute("shadowColor", shadowColor);
content.SetAttribute("shadowOpacity", shadowOpacity);
content.SetAttribute("shadowAngle", shadowAngle);
content.SetAttribute("shadowDistance", shadowDistance);
content.SetAttribute("shadowBlurRadius", shadowBlurRadius);
}
void TextObject::ExposeResources(
gd::ArbitraryResourceWorker& worker) {
worker.ExposeFont(fontName);
}
#endif

View File

@@ -5,8 +5,8 @@ Copyright (c) 2008-2016 Florian Rival (Florian.Rival@gmail.com)
This project is released under the MIT License.
*/
#ifndef TEXTOBJECT_H
#define TEXTOBJECT_H
#pragma once
#include "GDCore/Project/ObjectConfiguration.h"
namespace gd {
class Project;
@@ -29,11 +29,11 @@ class GD_EXTENSION_API TextObject : public gd::ObjectConfiguration {
/** \brief Change the text.
*/
inline void SetString(const gd::String& str) { text = str; };
inline void SetText(const gd::String& str) { text = str; };
/** \brief Get the text.
*/
inline const gd::String& GetString() const { return text; };
inline const gd::String& GetText() const { return text; };
/** \brief Change the character size.
*/
@@ -66,31 +66,63 @@ class GD_EXTENSION_API TextObject : public gd::ObjectConfiguration {
void SetSmooth(bool smooth) { smoothed = smooth; };
bool IsSmoothed() const { return smoothed; };
void SetColor(unsigned int r, unsigned int g, unsigned int b) {
colorR = r;
colorG = g;
colorB = b;
void SetColor(const gd::String& color_) {
color = color_;
};
unsigned int GetColorR() const { return colorR; };
unsigned int GetColorG() const { return colorG; };
unsigned int GetColorB() const { return colorB; };
inline const gd::String& GetColor() const { return color; };
void SetOutlineEnabled(bool smooth) { isOutlineEnabled = smooth; };
bool IsOutlineEnabled() const { return isOutlineEnabled; };
void SetOutlineThickness(double value) { outlineThickness = value; };
double GetOutlineThickness() const { return outlineThickness; };
void SetOutlineColor(const gd::String& color) {
outlineColor = color;
};
const gd::String& GetOutlineColor() const { return outlineColor; };
void SetShadowEnabled(bool smooth) { isShadowEnabled = smooth; };
bool IsShadowEnabled() const { return isShadowEnabled; };
void SetShadowColor(const gd::String& color) {
shadowColor = color;
};
const gd::String& GetShadowColor() const { return shadowColor; };
void SetShadowOpacity(double value) { shadowOpacity = value; };
double GetShadowOpacity() const { return shadowOpacity; };
void SetShadowAngle(double value) { shadowAngle = value; };
double GetShadowAngle() const { return shadowAngle; };
void SetShadowDistance(double value) { shadowDistance = value; };
double GetShadowDistance() const { return shadowDistance; };
void SetShadowBlurRadius(double value) { shadowBlurRadius = value; };
double GetShadowBlurRadius() const { return shadowBlurRadius; };
private:
virtual void DoUnserializeFrom(gd::Project& project,
const gd::SerializerElement& element);
#if defined(GD_IDE_ONLY)
virtual void DoSerializeTo(gd::SerializerElement& element) const;
#endif
gd::String text;
double characterSize;
gd::String fontName;
bool smoothed;
bool bold, italic, underlined;
unsigned int colorR;
unsigned int colorG;
unsigned int colorB;
gd::String color;
gd::String textAlignment;
};
#endif // TEXTOBJECT_H
bool isOutlineEnabled;
double outlineThickness;
gd::String outlineColor;
bool isShadowEnabled;
gd::String shadowColor;
double shadowOpacity;
double shadowAngle;
double shadowDistance;
double shadowBlurRadius;
};

View File

@@ -69,17 +69,23 @@ namespace gdjs {
this._object._outlineColor[1],
this._object._outlineColor[2]
);
style.strokeThickness = this._object._outlineThickness;
style.strokeThickness = this._object._isOutlineEnabled
? this._object._outlineThickness
: 0;
style.dropShadow = this._object._shadow;
style.dropShadowColor = gdjs.rgbToHexNumber(
this._object._shadowColor[0],
this._object._shadowColor[1],
this._object._shadowColor[2]
);
style.dropShadowAlpha = this._object._shadowOpacity / 255;
style.dropShadowBlur = this._object._shadowBlur;
style.dropShadowAngle = this._object._shadowAngle;
style.dropShadowAngle = gdjs.toRad(this._object._shadowAngle);
style.dropShadowDistance = this._object._shadowDistance;
style.padding = this._object._padding;
const extraPaddingForShadow = style.dropShadow
? style.dropShadowDistance + style.dropShadowBlur
: 0;
style.padding = Math.ceil(this._object._padding + extraPaddingForShadow);
// Prevent spikey outlines by adding a miter limit
style.miterLimit = 3;

View File

@@ -5,28 +5,35 @@
namespace gdjs {
/** Base parameters for gdjs.TextRuntimeObject */
export type TextObjectDataType = {
/** The size of the characters */
characterSize: number;
/** The font name */
font: string;
/** Is Bold? */
bold: boolean;
/** Is Italic? */
italic: boolean;
/** Is Underlined? */
underlined: boolean;
/** The text color in an RGB representation */
color: {
/** The Red level from 0 to 255 */
r: number;
/** The Green level from 0 to 255 */
g: number;
/** The Blue level from 0 to 255 */
b: number;
content: {
/** The size of the characters */
characterSize: number;
/** The font name */
font: string;
/** Is Bold? */
bold: boolean;
/** Is Italic? */
italic: boolean;
/** Is Underlined? */
underlined: boolean;
/** The text color in an RGB representation */
color: string;
/** The text of the object */
text: string;
textAlignment: string;
isOutlineEnabled: boolean;
outlineThickness: float;
/** The outline color in an RGB representation */
outlineColor: string;
isShadowEnabled: boolean;
/** The shadow color in an RGB representation */
shadowColor: string;
shadowOpacity: float;
shadowDistance: float;
shadowAngle: float;
shadowBlurRadius: float;
};
/** The text of the object */
string: string;
textAlignment: string;
};
export type TextObjectData = ObjectData & TextObjectDataType;
@@ -50,13 +57,18 @@ namespace gdjs {
_textAlign: string = 'left';
_wrapping: boolean = false;
_wrappingWidth: float = 1;
_outlineThickness: number = 0;
_outlineColor: integer[] = [255, 255, 255];
_shadow: boolean = false;
_shadowColor: integer[] = [0, 0, 0];
_shadowDistance: number = 1;
_shadowBlur: integer = 1;
_shadowAngle: float = 0;
_isOutlineEnabled: boolean;
_outlineThickness: float;
_outlineColor: integer[];
_shadow: boolean;
_shadowColor: integer[];
_shadowOpacity: float;
_shadowDistance: float;
_shadowAngle: float;
_shadowBlur: float;
_padding: integer = 5;
_str: string;
_renderer: gdjs.TextRuntimeObjectRenderer;
@@ -74,18 +86,27 @@ namespace gdjs {
textObjectData: TextObjectData
) {
super(instanceContainer, textObjectData);
this._characterSize = Math.max(1, textObjectData.characterSize);
this._fontName = textObjectData.font;
this._bold = textObjectData.bold;
this._italic = textObjectData.italic;
this._underlined = textObjectData.underlined;
this._color = [
textObjectData.color.r,
textObjectData.color.g,
textObjectData.color.b,
];
this._str = textObjectData.string;
this._textAlign = textObjectData.textAlignment;
const content = textObjectData.content;
this._characterSize = Math.max(1, content.characterSize);
this._fontName = content.font;
this._bold = content.bold;
this._italic = content.italic;
this._underlined = content.underlined;
this._color = gdjs.rgbOrHexToRGBColor(content.color);
this._str = content.text;
this._textAlign = content.textAlignment;
this._isOutlineEnabled = content.isOutlineEnabled;
this._outlineThickness = content.outlineThickness;
this._outlineColor = gdjs.rgbOrHexToRGBColor(content.outlineColor);
this._shadow = content.isShadowEnabled;
this._shadowColor = gdjs.rgbOrHexToRGBColor(content.shadowColor);
this._shadowOpacity = content.shadowOpacity;
this._shadowDistance = content.shadowDistance;
this._shadowBlur = content.shadowBlurRadius;
this._shadowAngle = content.shadowAngle;
this._renderer = new gdjs.TextRuntimeObjectRenderer(
this,
instanceContainer
@@ -99,40 +120,58 @@ namespace gdjs {
oldObjectData: TextObjectData,
newObjectData: TextObjectData
): boolean {
if (oldObjectData.characterSize !== newObjectData.characterSize) {
this.setCharacterSize(newObjectData.characterSize);
const oldContent = oldObjectData.content;
const newContent = newObjectData.content;
if (oldContent.characterSize !== newContent.characterSize) {
this.setCharacterSize(newContent.characterSize);
}
if (oldObjectData.font !== newObjectData.font) {
this.setFontName(newObjectData.font);
if (oldContent.font !== newContent.font) {
this.setFontName(newContent.font);
}
if (oldObjectData.bold !== newObjectData.bold) {
this.setBold(newObjectData.bold);
if (oldContent.bold !== newContent.bold) {
this.setBold(newContent.bold);
}
if (oldObjectData.italic !== newObjectData.italic) {
this.setItalic(newObjectData.italic);
if (oldContent.italic !== newContent.italic) {
this.setItalic(newContent.italic);
}
if (
oldObjectData.color.r !== newObjectData.color.r ||
oldObjectData.color.g !== newObjectData.color.g ||
oldObjectData.color.b !== newObjectData.color.b
) {
this.setColor(
'' +
newObjectData.color.r +
';' +
newObjectData.color.g +
';' +
newObjectData.color.b
);
if (oldContent.color !== newContent.color) {
this.setColor(newContent.color);
}
if (oldObjectData.string !== newObjectData.string) {
this.setString(newObjectData.string);
if (oldContent.text !== newContent.text) {
this.setText(newContent.text);
}
if (oldObjectData.underlined !== newObjectData.underlined) {
if (oldContent.underlined !== newContent.underlined) {
return false;
}
if (oldObjectData.textAlignment !== newObjectData.textAlignment) {
this.setTextAlignment(newObjectData.textAlignment);
if (oldContent.textAlignment !== newContent.textAlignment) {
this.setTextAlignment(newContent.textAlignment);
}
if (oldContent.isOutlineEnabled !== newContent.isOutlineEnabled) {
this.setOutlineEnabled(newContent.isOutlineEnabled);
}
if (oldContent.outlineThickness !== newContent.outlineThickness) {
this.setOutlineThickness(newContent.outlineThickness);
}
if (oldContent.outlineColor !== newContent.outlineColor) {
this.setOutlineColor(newContent.outlineColor);
}
if (oldContent.isShadowEnabled !== newContent.isShadowEnabled) {
this.showShadow(newContent.isShadowEnabled);
}
if (oldContent.shadowColor !== newContent.shadowColor) {
this.setShadowColor(newContent.shadowColor);
}
if (oldContent.shadowOpacity !== newContent.shadowOpacity) {
this.setShadowOpacity(newContent.shadowOpacity);
}
if (oldContent.shadowDistance !== newContent.shadowDistance) {
this.setShadowDistance(newContent.shadowDistance);
}
if (oldContent.shadowAngle !== newContent.shadowAngle) {
this.setShadowAngle(newContent.shadowAngle);
}
if (oldContent.shadowBlurRadius !== newContent.shadowBlurRadius) {
this.setShadowBlurRadius(newContent.shadowBlurRadius);
}
return true;
}
@@ -173,7 +212,7 @@ namespace gdjs {
/**
* Set object position on X axis.
*/
setX(x): void {
setX(x: float): void {
super.setX(x);
this._updateTextPosition();
}
@@ -181,7 +220,7 @@ namespace gdjs {
/**
* Set object position on Y axis.
*/
setY(y): void {
setY(y: float): void {
super.setY(y);
this._updateTextPosition();
}
@@ -198,7 +237,7 @@ namespace gdjs {
/**
* Set object opacity.
*/
setOpacity(opacity): void {
setOpacity(opacity: float): void {
if (opacity < 0) {
opacity = 0;
}
@@ -292,7 +331,7 @@ namespace gdjs {
* Set bold for the object text.
* @param enable {boolean} true to have a bold text, false otherwise.
*/
setBold(enable): void {
setBold(enable: boolean): void {
this._bold = enable;
this._renderer.updateStyle();
}
@@ -308,7 +347,7 @@ namespace gdjs {
* Set italic for the object text.
* @param enable {boolean} true to have an italic text, false otherwise.
*/
setItalic(enable): void {
setItalic(enable: boolean): void {
this._italic = enable;
this._renderer.updateStyle();
}
@@ -485,6 +524,7 @@ namespace gdjs {
* Set the outline for the text object.
* @param str color as a "R;G;B" string, for example: "255;0;0"
* @param thickness thickness of the outline (0 = disabled)
* @deprecated Prefer independent setters.
*/
setOutline(str: string, thickness: number): void {
const color = str.split(';');
@@ -498,12 +538,48 @@ namespace gdjs {
this._renderer.updateStyle();
}
isOutlineEnabled(): boolean {
return this._isOutlineEnabled;
}
setOutlineEnabled(enable: boolean): void {
this._isOutlineEnabled = enable;
this._renderer.updateStyle();
}
/**
* Get the outline thickness of the text object.
* @return the outline thickness
*/
getOutlineThickness(): number {
return this._outlineThickness;
}
/**
* Set the outline thickness of the text object.
* @param value the outline thickness
*/
setOutlineThickness(value: float): void {
this._outlineThickness = value;
this._renderer.updateStyle();
}
/**
* Set the shadow color of the text object.
* @param color the shadow color as a "R;G;B" string, for example: "255;0;0"
*/
setOutlineColor(color: string): void {
this._outlineColor = gdjs.rgbOrHexToRGBColor(color);
this._renderer.updateStyle();
}
/**
* Set the shadow for the text object.
* @param str color as a "R;G;B" string, for example: "255;0;0"
* @param distance distance between the shadow and the text, in pixels.
* @param blur amount of shadow blur, in pixels.
* @param angle shadow offset direction, in degrees.
* @deprecated Prefer independent setters.
*/
setShadow(
str: string,
@@ -525,6 +601,96 @@ namespace gdjs {
this._renderer.updateStyle();
}
isShadowEnabled(): boolean {
return this._shadow;
}
/**
* Show the shadow of the text object.
* @param enable true to show the shadow, false to hide it
*/
showShadow(enable: boolean): void {
this._shadow = enable;
this._renderer.updateStyle();
}
/**
* Get the shadow opacity of the text object.
* @return the opacity (0 - 255)
*/
getShadowOpacity(): number {
return this._shadowOpacity;
}
/**
* Set the shadow opacity of the text object.
* @param value the opacity (0 - 255)
*/
setShadowOpacity(value: float): void {
this._shadowOpacity = value;
this._renderer.updateStyle();
}
/**
* Get the shadow offset distance of the text object.
* @return the shadow offset distance
*/
getShadowDistance(): number {
return this._shadowDistance;
}
/**
* Set the shadow offset distance of the text object.
* @param value the shadow offset distance
*/
setShadowDistance(value: float): void {
this._shadowDistance = value;
this._renderer.updateStyle();
}
/**
* Get the shadow offset angle of the text object.
* @return the shadow offset angle in degrees
*/
getShadowAngle(): number {
return this._shadowAngle;
}
/**
* Set the shadow offset angle of the text object.
* @param value the shadow offset angle in degrees
*/
setShadowAngle(value: float): void {
this._shadowAngle = value;
this._renderer.updateStyle();
}
/**
* Get the shadow blur radius of the text object.
* @return the shadow blur radius
*/
getShadowBlurRadius(): number {
return this._shadowBlur;
}
/**
* Set the shadow blur radius of the text object.
* @param value the shadow blur radius
*/
setShadowBlurRadius(value: float): void {
this._shadowBlur = value;
this._renderer.updateStyle();
}
/**
* Set the shadow color of the text object.
* @param color the shadow color as a "R;G;B" string, for example: "255;0;0"
*/
setShadowColor(color: string): void {
this._shadowColor = gdjs.rgbOrHexToRGBColor(color);
this._renderer.updateStyle();
}
/**
* Set the gradient for the text object.
* @param strFirstColor color as a "R;G;B" string, for example: "255;0;0"
@@ -578,15 +744,6 @@ namespace gdjs {
this._renderer.updateStyle();
}
/**
* Show the shadow of the text object.
* @param enable true to show the shadow, false to hide it
*/
showShadow(enable: boolean): void {
this._shadow = enable;
this._renderer.updateStyle();
}
/**
* Get padding of the text object.
* @return number of pixels around the text before it gets cropped

View File

@@ -96,7 +96,7 @@ TopDownMovementBehavior::GetProperties(
.SetType("Boolean");
gd::String viewpoint = behaviorContent.GetStringAttribute("viewpoint");
gd::String viewpointStr = _("Viewpoint");
gd::String viewpointStr = _("Top-Down");
if (viewpoint == "TopDown")
viewpointStr = _("Top-Down");
else if (viewpoint == "PixelIsometry")
@@ -108,6 +108,7 @@ TopDownMovementBehavior::GetProperties(
properties["Viewpoint"]
.SetLabel(_("Viewpoint"))
.SetGroup(_("Viewpoint"))
.SetAdvanced()
.SetValue(viewpointStr)
.SetType("Choice")
.AddExtraInfo(_("Top-Down"))
@@ -117,6 +118,7 @@ TopDownMovementBehavior::GetProperties(
properties["CustomIsometryAngle"]
.SetLabel(_("Custom isometry angle (between 1deg and 44deg)"))
.SetGroup(_("Viewpoint"))
.SetAdvanced()
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetValue(gd::String::From(
@@ -126,6 +128,7 @@ TopDownMovementBehavior::GetProperties(
properties["MovementAngleOffset"]
.SetLabel(_("Movement angle offset"))
.SetGroup(_("Viewpoint"))
.SetAdvanced()
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetValue(gd::String::From(

View File

@@ -567,7 +567,7 @@ module.exports = {
'Progress',
_('Tween progress'),
_('the progress of a tween (between 0.0 and 1.0)'),
_('the progress of a tween'),
_('the progress of the scene tween _PARAM1_'),
_('Scene Tweens'),
'JsPlatform/Extensions/tween_behavior32.png'
)
@@ -893,7 +893,7 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('addObjectPositionXTween2');
// deprecated use the 3D Tween extension
// deprecated
behavior
.addAction(
'AddObjectPositionZTween',
@@ -926,6 +926,38 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('addObjectPositionZTween');
behavior
.addAction(
'AddObjectPositionZTween2',
_('Tween object Z position'),
_(
'Tweens an object Z position (3D objects only) from its current Z position to a new one.'
),
_(
'Tween the Z position of _PARAM0_ to _PARAM4_ with easing _PARAM5_ over _PARAM6_ seconds as _PARAM3_'
),
_('Position'),
'JsPlatform/Extensions/tween_behavior24.png',
'JsPlatform/Extensions/tween_behavior32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter("behavior", _("3D capability"), "Scene3D::Base3DBehavior")
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To Z'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
'yesorno',
_('Destroy this object when tween finishes'),
'',
false
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setFunctionName('addObjectPositionZTween2');
// deprecated
behavior
.addAction(
@@ -1079,6 +1111,38 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('addObjectDepthTween');
behavior
.addAction(
'AddObjectDepthTween2',
_('Tween object depth'),
_(
'Tweens an object depth (suitable 3D objects only) from its current depth to a new one.'
),
_(
'Tween the depth of _PARAM0_ to _PARAM4_ with easing _PARAM5_ over _PARAM6_ seconds as _PARAM3_'
),
_('Size'),
'JsPlatform/Extensions/tween_behavior24.png',
'JsPlatform/Extensions/tween_behavior32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter("behavior", _("3D capability"), "Scene3D::Base3DBehavior")
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To depth'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
'yesorno',
_('Destroy this object when tween finishes'),
'',
false
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setFunctionName('addObjectDepthTween2');
// deprecated
behavior
.addAction(
@@ -1203,6 +1267,66 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('addObjectAngleTween2');
behavior
.addScopedAction(
'AddObjectRotationXTween',
_('Tween object rotation on X axis'),
_('Tweens an object rotation on X axis from its current angle to a new one.'),
_(
'Tween the rotation on X axis of _PARAM0_ to _PARAM4_° with easing _PARAM5_ over _PARAM6_ seconds as _PARAM3_'
),
_('Angle'),
'JsPlatform/Extensions/tween_behavior24.png',
'JsPlatform/Extensions/tween_behavior32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter("behavior", _("3D capability"), "Scene3D::Base3DBehavior")
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To angle (in degrees)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
'yesorno',
_('Destroy this object when tween finishes'),
'',
false
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setFunctionName('addObjectRotationXTween');
behavior
.addScopedAction(
'AddObjectRotationYTween',
_('Tween object rotation on Y axis'),
_('Tweens an object rotation on Y axis from its current angle to a new one.'),
_(
'Tween the rotation on Y axis of _PARAM0_ to _PARAM4_° with easing _PARAM5_ over _PARAM6_ seconds as _PARAM3_'
),
_('Angle'),
'JsPlatform/Extensions/tween_behavior24.png',
'JsPlatform/Extensions/tween_behavior32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter("behavior", _("3D capability"), "Scene3D::Base3DBehavior")
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To angle (in degrees)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
'yesorno',
_('Destroy this object when tween finishes'),
'',
false
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setFunctionName('addObjectRotationYTween');
// deprecated
behavior
.addAction(
@@ -1239,6 +1363,7 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('addObjectScaleTween');
// deprecated
behavior
.addScopedAction(
'AddObjectScaleTween2',
@@ -1253,6 +1378,7 @@ module.exports = {
'JsPlatform/Extensions/tween_behavior24.png',
'JsPlatform/Extensions/tween_behavior32.png'
)
.setHidden()
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
@@ -1273,6 +1399,39 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('addObjectScaleTween2');
behavior
.addScopedAction(
'AddObjectScaleTween3',
_('Tween object scale'),
_(
'Tweens an object scale from its current value to a new one (note: the scale can never be 0 or less).'
),
_(
'Tween the scale of _PARAM0_ to _PARAM3_ (from center: _PARAM7_) with easing _PARAM4_ over _PARAM5_ seconds as _PARAM2_'
),
_('Size'),
'JsPlatform/Extensions/tween_behavior24.png',
'JsPlatform/Extensions/tween_behavior32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To scale'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
'yesorno',
_('Destroy this object when tween finishes'),
'',
false
)
.setDefaultValue('no')
.addParameter('yesorno', _('Scale from center of object'), '', false)
.setDefaultValue('no')
.getCodeExtraInformation()
.setFunctionName('addObjectScaleTween3');
// deprecated
behavior
.addAction(
@@ -1900,7 +2059,7 @@ module.exports = {
'Progress',
_('Tween progress'),
_('the progress of a tween (between 0.0 and 1.0)'),
_('the progress of a tween'),
_('the progress of the tween _PARAM2_'),
'',
'JsPlatform/Extensions/tween_behavior32.png'
)

View File

@@ -10,210 +10,256 @@ describe('gdjs.TweenRuntimeBehavior', () => {
};
/** @type {gdjs.RuntimeScene} */
let layout;
let runtimeScene;
/** @type {gdjs.TweenRuntimeBehavior} */
beforeEach(() => {
layout = createScene();
layout.getLayer('').setTimeScale(1.5);
runtimeScene = createScene();
runtimeScene.getLayer('').setTimeScale(1.5);
});
const tween = gdjs.evtTools.tween;
const camera = gdjs.evtTools.camera;
it("can get default values for tweens that don't exist", () => {
expect(tween.sceneTweenExists(layout, 'MyTween')).to.be(false);
expect(tween.getValue(layout, 'MyTween')).to.be(0);
expect(tween.getProgress(layout, 'MyTween')).to.be(0);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenExists(runtimeScene, 'MyTween')).to.be(false);
expect(tween.getValue(runtimeScene, 'MyTween')).to.be(0);
expect(tween.getProgress(runtimeScene, 'MyTween')).to.be(0);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
});
it('can play a tween till the end', () => {
camera.setCameraRotation(layout, 200, '', 0);
tween.tweenCameraRotation2(layout, 'MyTween', 600, '', 'linear', 0.25);
camera.setCameraRotation(runtimeScene, 200, '', 0);
tween.tweenCameraRotation2(
runtimeScene,
'MyTween',
600,
'',
'linear',
0.25
);
// Tween actions don't change the value directly.
expect(camera.getCameraRotation(layout, '', 0)).to.be(200);
expect(tween.getValue(layout, 'MyTween')).to.be(200);
expect(tween.getProgress(layout, 'MyTween')).to.be(0);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(200);
expect(tween.getValue(runtimeScene, 'MyTween')).to.be(200);
expect(tween.getProgress(runtimeScene, 'MyTween')).to.be(0);
let oldAngle;
let oldValue;
let oldProgress;
for (let i = 0; i < 10; i++) {
oldAngle = camera.getCameraRotation(layout, '', 0);
oldValue = tween.getValue(layout, 'MyTween');
oldProgress = tween.getProgress(layout, 'MyTween');
oldAngle = camera.getCameraRotation(runtimeScene, '', 0);
oldValue = tween.getValue(runtimeScene, 'MyTween');
oldProgress = tween.getProgress(runtimeScene, 'MyTween');
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(layout, '', 0)).to.be.above(oldAngle);
expect(tween.getValue(layout, 'MyTween')).to.be.above(oldValue);
expect(tween.getProgress(layout, 'MyTween')).to.be.above(oldProgress);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be.above(
oldAngle
);
expect(tween.getValue(runtimeScene, 'MyTween')).to.be.above(oldValue);
expect(tween.getProgress(runtimeScene, 'MyTween')).to.be.above(
oldProgress
);
}
// The tween reaches the end
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(layout, '', 0)).to.be(600);
expect(tween.getValue(layout, 'MyTween')).to.be(600);
expect(tween.getProgress(layout, 'MyTween')).to.be(1);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(600);
expect(tween.getValue(runtimeScene, 'MyTween')).to.be(600);
expect(tween.getProgress(runtimeScene, 'MyTween')).to.be(1);
// The value is not changed after the tween is finished
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(layout, '', 0)).to.be(600);
expect(tween.getValue(layout, 'MyTween')).to.be(600);
expect(tween.getProgress(layout, 'MyTween')).to.be(1);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(600);
expect(tween.getValue(runtimeScene, 'MyTween')).to.be(600);
expect(tween.getProgress(runtimeScene, 'MyTween')).to.be(1);
// The value is not set to the targeted value over and over
// after the tween is finished.
camera.setCameraRotation(layout, 123, '', 0);
layout.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
camera.setCameraRotation(runtimeScene, 123, '', 0);
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
});
it('can pause and resume a tween', () => {
camera.setCameraRotation(layout, 200, '', 0);
tween.tweenCameraRotation2(layout, 'MyTween', 600, '', 'linear', 0.25);
camera.setCameraRotation(runtimeScene, 200, '', 0);
tween.tweenCameraRotation2(
runtimeScene,
'MyTween',
600,
'',
'linear',
0.25
);
// The tween starts
for (let i = 0; i < 5; i++) {
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
}
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
// Pause the tween
tween.pauseSceneTween(layout, 'MyTween');
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
tween.pauseSceneTween(runtimeScene, 'MyTween');
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
for (let i = 0; i < 5; i++) {
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
}
// The value is not overridden during the pause.
camera.setCameraRotation(layout, 123, '', 0);
layout.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
camera.setCameraRotation(runtimeScene, 123, '', 0);
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
// Resume the tween
tween.resumeSceneTween(layout, 'MyTween');
tween.resumeSceneTween(runtimeScene, 'MyTween');
// Tween actions don't change the value directly.
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
layout.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(layout, '', 0)).to.be(440);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(440);
});
it('can stop and restart a tween', () => {
camera.setCameraRotation(layout, 200, '', 0);
camera.setCameraRotation(runtimeScene, 200, '', 0);
// Start the tween
tween.tweenCameraRotation2(layout, 'MyTween', 600, '', 'linear', 0.25);
tween.tweenCameraRotation2(
runtimeScene,
'MyTween',
600,
'',
'linear',
0.25
);
for (let i = 0; i < 5; i++) {
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
}
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
// Stop the tween
tween.stopSceneTween(layout, 'MyTween', false);
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
tween.stopSceneTween(runtimeScene, 'MyTween', false);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
for (let i = 0; i < 5; i++) {
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
}
// The value is not overridden by a stopped tween.
camera.setCameraRotation(layout, 123, '', 0);
layout.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
camera.setCameraRotation(runtimeScene, 123, '', 0);
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
// A stopped tween can't be resumed.
tween.resumeSceneTween(layout, 'MyTween');
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
tween.resumeSceneTween(runtimeScene, 'MyTween');
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
layout.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(true);
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(true);
// Restart the tween
tween.tweenCameraRotation2(layout, 'MyTween', 623, '', 'linear', 0.25);
tween.tweenCameraRotation2(
runtimeScene,
'MyTween',
623,
'',
'linear',
0.25
);
for (let i = 0; i < 5; i++) {
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
}
expect(camera.getCameraRotation(layout, '', 0)).to.be(373);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(373);
});
it('can remove and recreate a tween', () => {
camera.setCameraRotation(layout, 200, '', 0);
camera.setCameraRotation(runtimeScene, 200, '', 0);
// Start the tween
expect(tween.sceneTweenExists(layout, 'MyTween')).to.be(false);
tween.tweenCameraRotation2(layout, 'MyTween', 600, '', 'linear', 0.25);
expect(tween.sceneTweenExists(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenExists(runtimeScene, 'MyTween')).to.be(false);
tween.tweenCameraRotation2(
runtimeScene,
'MyTween',
600,
'',
'linear',
0.25
);
expect(tween.sceneTweenExists(runtimeScene, 'MyTween')).to.be(true);
for (let i = 0; i < 5; i++) {
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
}
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
// Remove the tween
expect(tween.sceneTweenExists(layout, 'MyTween')).to.be(true);
tween.removeSceneTween(layout, 'MyTween');
expect(tween.sceneTweenExists(layout, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
expect(tween.sceneTweenExists(runtimeScene, 'MyTween')).to.be(true);
tween.removeSceneTween(runtimeScene, 'MyTween');
expect(tween.sceneTweenExists(runtimeScene, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
for (let i = 0; i < 5; i++) {
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(layout, '', 0)).to.be(400);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(400);
}
// The value is not overridden after the tween has been removed.
camera.setCameraRotation(layout, 123, '', 0);
layout.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
camera.setCameraRotation(runtimeScene, 123, '', 0);
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
// A removed tween can't be resumed.
tween.resumeSceneTween(layout, 'MyTween');
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
tween.resumeSceneTween(runtimeScene, 'MyTween');
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
layout.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(layout, '', 0)).to.be(123);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(123);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
// Recreate the tween
expect(tween.sceneTweenExists(layout, 'MyTween')).to.be(false);
tween.tweenCameraRotation2(layout, 'MyTween', 623, '', 'linear', 0.25);
expect(tween.sceneTweenExists(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenExists(runtimeScene, 'MyTween')).to.be(false);
tween.tweenCameraRotation2(
runtimeScene,
'MyTween',
623,
'',
'linear',
0.25
);
expect(tween.sceneTweenExists(runtimeScene, 'MyTween')).to.be(true);
for (let i = 0; i < 5; i++) {
layout.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(layout, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(layout, 'MyTween')).to.be(false);
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
}
expect(camera.getCameraRotation(layout, '', 0)).to.be(373);
expect(tween.sceneTweenExists(layout, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(373);
expect(tween.sceneTweenExists(runtimeScene, 'MyTween')).to.be(true);
});
const checkProgress = (steps, getValueFunctions) => {
@@ -222,7 +268,7 @@ describe('gdjs.TweenRuntimeBehavior', () => {
}
for (let i = 0; i < steps; i++) {
const oldValues = getValueFunctions.map((getValue) => getValue());
layout.renderAndStep(1000 / 60);
runtimeScene.renderAndStep(1000 / 60);
for (let index = 0; index < oldValues.length; index++) {
expect(getValueFunctions[index]()).not.to.be(oldValues[index]);
@@ -231,10 +277,10 @@ describe('gdjs.TweenRuntimeBehavior', () => {
};
it('can tween a scene variable', () => {
const variable = layout.getVariables().get('MyVariable');
const variable = runtimeScene.getVariables().get('MyVariable');
variable.setNumber(200);
tween.tweenVariableNumber3(
layout,
runtimeScene,
'MyTween',
variable,
600,
@@ -247,7 +293,7 @@ describe('gdjs.TweenRuntimeBehavior', () => {
it('can tween a layer value', () => {
tween.addLayerValueTween(
layout,
runtimeScene,
'MyTween',
200,
600,
@@ -256,13 +302,13 @@ describe('gdjs.TweenRuntimeBehavior', () => {
false,
''
);
checkProgress(6, () => tween.getValue(layout, 'MyTween'));
expect(tween.getValue(layout, 'MyTween')).to.be(440);
checkProgress(6, () => tween.getValue(runtimeScene, 'MyTween'));
expect(tween.getValue(runtimeScene, 'MyTween')).to.be(440);
});
it('can tween a layout value', () => {
tween.addLayoutValueTween(
layout,
runtimeScene,
'MyTween',
200,
600,
@@ -270,39 +316,69 @@ describe('gdjs.TweenRuntimeBehavior', () => {
0.25 / 1.5,
false
);
checkProgress(6, () => tween.getValue(layout, 'MyTween'));
expect(tween.getValue(layout, 'MyTween')).to.be(440);
checkProgress(6, () => tween.getValue(runtimeScene, 'MyTween'));
expect(tween.getValue(runtimeScene, 'MyTween')).to.be(440);
});
it('can tween a layer camera position', () => {
camera.setCameraX(layout, 200, '', 0);
camera.setCameraY(layout, 300, '', 0);
tween.tweenCamera2(layout, 'MyTween', 600, 900, '', 'linear', 0.25);
camera.setCameraX(runtimeScene, 200, '', 0);
camera.setCameraY(runtimeScene, 300, '', 0);
tween.tweenCamera2(runtimeScene, 'MyTween', 600, 900, '', 'linear', 0.25);
checkProgress(6, [
() => camera.getCameraX(layout, '', 0),
() => camera.getCameraY(layout, '', 0),
() => camera.getCameraX(runtimeScene, '', 0),
() => camera.getCameraY(runtimeScene, '', 0),
]);
expect(camera.getCameraX(layout, '', 0)).to.be(440);
expect(camera.getCameraY(layout, '', 0)).to.be(660);
expect(camera.getCameraX(runtimeScene, '', 0)).to.be(440);
expect(camera.getCameraY(runtimeScene, '', 0)).to.be(660);
});
it('can tween a layer camera zoom', () => {
camera.setCameraZoom(layout, 200, '', 0);
tween.tweenCameraZoom2(layout, 'MyTween', 600, '', 'linear', 0.25);
checkProgress(6, () => camera.getCameraZoom(layout, '', 0));
camera.setCameraZoom(runtimeScene, 200, '', 0);
tween.tweenCameraZoom2(runtimeScene, 'MyTween', 600, '', 'linear', 0.25);
checkProgress(6, () => camera.getCameraZoom(runtimeScene, '', 0));
// The interpolation is exponential.
expect(camera.getCameraZoom(layout, '', 0)).to.be(386.6364089863524);
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(386.6364089863524);
});
it('can tween a layer camera zoom to 0', () => {
camera.setCameraZoom(runtimeScene, 1, '', 0);
tween.tweenCameraZoom2(runtimeScene, 'MyTween', 0, '', 'linear', 0.25);
// A camera zoom of 0 doesn't make sense.
// Check that there is no NaN.
for (let i = 0; i < 11; i++) {
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(0);
}
});
it('can tween a layer camera zoom from 0', () => {
camera.setCameraZoom(runtimeScene, 0, '', 0);
tween.tweenCameraZoom2(runtimeScene, 'MyTween', 1, '', 'linear', 0.25);
// A camera zoom of 0 doesn't make sense.
// Check that there is no NaN.
for (let i = 0; i < 11; i++) {
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(0);
runtimeScene.renderAndStep(1000 / 60);
}
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(1);
});
it('can tween a layer camera rotation', () => {
camera.setCameraRotation(layout, 200, '', 0);
tween.tweenCameraRotation2(layout, 'MyTween', 600, '', 'linear', 0.25);
checkProgress(6, () => camera.getCameraRotation(layout, '', 0));
expect(camera.getCameraRotation(layout, '', 0)).to.be(440);
camera.setCameraRotation(runtimeScene, 200, '', 0);
tween.tweenCameraRotation2(
runtimeScene,
'MyTween',
600,
'',
'linear',
0.25
);
checkProgress(6, () => camera.getCameraRotation(runtimeScene, '', 0));
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(440);
});
it('can tween a number effect property', () => {
const layer = layout.getLayer('');
const layer = runtimeScene.getLayer('');
layer.addEffect({
effectType: 'Outline',
name: 'MyEffect',
@@ -311,7 +387,7 @@ describe('gdjs.TweenRuntimeBehavior', () => {
booleanParameters: {},
});
tween.tweenNumberEffectPropertyTween(
layout,
runtimeScene,
'MyTween',
600,
'',
@@ -329,7 +405,7 @@ describe('gdjs.TweenRuntimeBehavior', () => {
});
it('can tween a color effect property', () => {
const layer = layout.getLayer('');
const layer = runtimeScene.getLayer('');
layer.addEffect({
effectType: 'Outline',
name: 'MyEffect',
@@ -338,7 +414,7 @@ describe('gdjs.TweenRuntimeBehavior', () => {
booleanParameters: {},
});
tween.tweenColorEffectPropertyTween(
layout,
runtimeScene,
'MyTween',
'255;192;128',
'',

View File

@@ -85,6 +85,32 @@ describe('gdjs.TweenRuntimeBehavior', () => {
return object;
};
/**
* @param {gdjs.RuntimeScene} runtimeScene
*/
const addCube = (runtimeScene) => {
const object = new gdjs.Cube3DRuntimeObject(runtimeScene, {
name: 'Cube',
type: 'Scene3D::Cube3DObject',
effects: [],
variables: [],
behaviors: [
{
type: 'Tween::TweenBehavior',
name: behaviorName,
},
],
// @ts-ignore
content: {
width: 64,
height: 64,
depth: 64,
},
});
runtimeScene.addObject(object);
return object;
};
/**
* @param {gdjs.RuntimeScene} runtimeScene
*/
@@ -100,18 +126,25 @@ describe('gdjs.TweenRuntimeBehavior', () => {
name: behaviorName,
},
],
characterSize: 20,
font: '',
bold: false,
italic: false,
underlined: false,
color: {
r: 0,
g: 0,
b: 0,
content: {
characterSize: 20,
font: '',
bold: false,
italic: false,
underlined: false,
color: '0;0;0',
text: '',
textAlignment: 'left',
isOutlineEnabled: false,
outlineThickness: 2,
outlineColor: '255;255;255',
isShadowEnabled: false,
shadowColor: '0;0;0',
shadowOpacity: 128,
shadowDistance: 4,
shadowAngle: 90,
shadowBlurRadius: 2,
},
string: '',
textAlignment: 'left',
});
runtimeScene.addObject(object);
return object;
@@ -123,6 +156,8 @@ describe('gdjs.TweenRuntimeBehavior', () => {
let object;
/** @type {gdjs.SpriteRuntimeObject} */
let sprite;
/** @type {gdjs.Cube3DRuntimeObject} */
let cube;
/** @type {gdjs.TextRuntimeObject} */
let textObject;
/** @type {gdjs.TweenRuntimeBehavior} */
@@ -130,18 +165,23 @@ describe('gdjs.TweenRuntimeBehavior', () => {
/** @type {gdjs.TweenRuntimeBehavior} */
let spriteBehavior;
/** @type {gdjs.TweenRuntimeBehavior} */
let cubeBehavior;
/** @type {gdjs.TweenRuntimeBehavior} */
let textObjectBehavior;
beforeEach(() => {
runtimeScene = createScene();
runtimeScene.getLayer('').setTimeScale(1.5);
object = addObject(runtimeScene);
sprite = addSprite(runtimeScene);
cube = addCube(runtimeScene);
textObject = addTextObject(runtimeScene);
//@ts-ignore
behavior = object.getBehavior(behaviorName);
//@ts-ignore
spriteBehavior = sprite.getBehavior(behaviorName);
//@ts-ignore
cubeBehavior = cube.getBehavior(behaviorName);
//@ts-ignore
textObjectBehavior = textObject.getBehavior(behaviorName);
});
@@ -216,6 +256,19 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(object.getY()).to.be(440);
});
it('can tween the position on Z axis', () => {
cube.setZ(200);
cubeBehavior.addObjectPositionZTween(
'MyTween',
600,
'linear',
250 / 1.5,
false
);
checkProgress(6, () => cube.getZ());
expect(cube.getZ()).to.be(440);
});
it('can tween the angle', () => {
object.setAngle(200);
behavior.addObjectAngleTween('MyTween', 600, 'linear', 250 / 1.5, false);
@@ -237,6 +290,19 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(object.getHeight()).to.be(440);
});
it('can tween the depth', () => {
cube.setDepth(200);
cubeBehavior.addObjectDepthTween(
'MyTween',
600,
'linear',
250 / 1.5,
false
);
checkProgress(6, () => cube.getDepth());
expect(cube.getDepth()).to.be(440);
});
it('can tween the opacity', () => {
sprite.setOpacity(128);
spriteBehavior.addObjectOpacityTween(
@@ -424,4 +490,46 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(sprite.getX()).to.be(-7580);
expect(sprite.getY()).to.be(-11120);
});
it('can tween the scales in seconds', () => {
sprite.setPosition(100, 400);
sprite.setScaleX(200);
sprite.setScaleY(300);
spriteBehavior.addObjectScaleTween2(
'MyTween',
600,
900,
'linear',
0.25,
false,
false
);
checkProgress(6, [() => sprite.getScaleX(), () => sprite.getScaleY()]);
// The interpolation is exponential.
expect(sprite.getScaleX()).to.be(386.6364089863524);
expect(sprite.getScaleY()).to.be(579.9546134795287);
expect(sprite.getX()).to.be(100);
expect(sprite.getY()).to.be(400);
});
it('can tween the scales from center in seconds', () => {
sprite.setPosition(100, 400);
sprite.setScaleX(200);
sprite.setScaleY(300);
spriteBehavior.addObjectScaleTween2(
'MyTween',
600,
900,
'linear',
0.25,
false,
true
);
checkProgress(6, [() => sprite.getScaleX(), () => sprite.getScaleY()]);
// The interpolation is exponential.
expect(sprite.getScaleX()).to.be(386.6364089863524);
expect(sprite.getScaleY()).to.be(579.9546134795287);
expect(sprite.getX()).to.be(-5872.3650875632775);
expect(sprite.getY()).to.be(-8558.547631344918);
});
});

View File

@@ -85,6 +85,32 @@ describe('gdjs.TweenRuntimeBehavior', () => {
return object;
};
/**
* @param {gdjs.RuntimeScene} runtimeScene
*/
const addCube = (runtimeScene) => {
const object = new gdjs.Cube3DRuntimeObject(runtimeScene, {
name: 'Cube',
type: 'Scene3D::Cube3DObject',
effects: [],
variables: [],
behaviors: [
{
type: 'Tween::TweenBehavior',
name: behaviorName,
},
],
// @ts-ignore
content: {
width: 64,
height: 64,
depth: 64,
},
});
runtimeScene.addObject(object);
return object;
};
/**
* @param {gdjs.RuntimeScene} runtimeScene
*/
@@ -100,18 +126,25 @@ describe('gdjs.TweenRuntimeBehavior', () => {
name: behaviorName,
},
],
characterSize: 20,
font: '',
bold: false,
italic: false,
underlined: false,
color: {
r: 0,
g: 0,
b: 0,
content: {
characterSize: 20,
font: '',
bold: false,
italic: false,
underlined: false,
color: '0;0;0',
text: '',
textAlignment: 'left',
isOutlineEnabled: false,
outlineThickness: 2,
outlineColor: '255;255;255',
isShadowEnabled: false,
shadowColor: '0;0;0',
shadowOpacity: 128,
shadowDistance: 4,
shadowAngle: 90,
shadowBlurRadius: 2,
},
string: '',
textAlignment: 'left',
});
runtimeScene.addObject(object);
return object;
@@ -123,6 +156,8 @@ describe('gdjs.TweenRuntimeBehavior', () => {
let object;
/** @type {gdjs.SpriteRuntimeObject} */
let sprite;
/** @type {gdjs.Cube3DRuntimeObject} */
let cube;
/** @type {gdjs.TextRuntimeObject} */
let textObject;
/** @type {gdjs.TweenRuntimeBehavior} */
@@ -130,18 +165,23 @@ describe('gdjs.TweenRuntimeBehavior', () => {
/** @type {gdjs.TweenRuntimeBehavior} */
let spriteBehavior;
/** @type {gdjs.TweenRuntimeBehavior} */
let cubeBehavior;
/** @type {gdjs.TweenRuntimeBehavior} */
let textObjectBehavior;
beforeEach(() => {
runtimeScene = createScene();
runtimeScene.getLayer('').setTimeScale(1.5);
object = addObject(runtimeScene);
sprite = addSprite(runtimeScene);
cube = addCube(runtimeScene);
textObject = addTextObject(runtimeScene);
//@ts-ignore
behavior = object.getBehavior(behaviorName);
//@ts-ignore
spriteBehavior = sprite.getBehavior(behaviorName);
//@ts-ignore
cubeBehavior = cube.getBehavior(behaviorName);
//@ts-ignore
textObjectBehavior = textObject.getBehavior(behaviorName);
});
@@ -404,6 +444,20 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(object.getY()).to.be(440);
});
it('can tween the position on Z axis', () => {
cube.setZ(200);
cubeBehavior.addObjectPositionZTween2(
null,
'MyTween',
600,
'linear',
0.25,
false
);
checkProgress(6, () => cube.getZ());
expect(cube.getZ()).to.be(440);
});
it('can tween the angle', () => {
object.setAngle(200);
behavior.addObjectAngleTween2('MyTween', 600, 'linear', 0.25, false);
@@ -411,6 +465,34 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(object.getAngle()).to.be(440);
});
it('can tween the rotation X', () => {
cube.setRotationX(200);
cubeBehavior.addObjectRotationXTween(
null,
'MyTween',
600,
'linear',
0.25,
false
);
checkProgress(6, () => cube.getRotationX());
expect(cube.getRotationX()).to.be(440);
});
it('can tween the rotation Y', () => {
cube.setRotationY(200);
cubeBehavior.addObjectRotationYTween(
null,
'MyTween',
600,
'linear',
0.25,
false
);
checkProgress(6, () => cube.getRotationY());
expect(cube.getRotationY()).to.be(440);
});
it('can tween the width', () => {
object.setWidth(200);
behavior.addObjectWidthTween2('MyTween', 600, 'linear', 0.25, false);
@@ -425,6 +507,20 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(object.getHeight()).to.be(440);
});
it('can tween the depth', () => {
cube.setDepth(200);
cubeBehavior.addObjectDepthTween2(
null,
'MyTween',
600,
'linear',
0.25,
false
);
checkProgress(6, () => cube.getDepth());
expect(cube.getDepth()).to.be(440);
});
it('can tween a number effect property', () => {
sprite.addEffect({
effectType: 'Outline',
@@ -540,6 +636,53 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(sprite.getY()).to.be(400);
});
it('can tween the scale on X axis to 0', () => {
sprite.setPosition(100, 400);
sprite.setScaleX(1);
spriteBehavior.addObjectScaleTween3(
'MyTween',
0,
'linear',
0.25,
false,
false
);
// The interpolation is exponential.
// It would need an infinite speed to go away from 0.
// This is why the scale is set to 0 directly.
for (let i = 0; i < 11; i++) {
runtimeScene.renderAndStep(1000 / 60);
expect(sprite.getScaleX()).to.be(0);
}
expect(spriteBehavior.hasFinished('MyTween')).to.be(true);
expect(sprite.getX()).to.be(100);
expect(sprite.getY()).to.be(400);
});
it('can tween the scale on X axis from 0', () => {
sprite.setPosition(100, 400);
sprite.setScaleX(0);
spriteBehavior.addObjectScaleTween3(
'MyTween',
1,
'linear',
0.25,
false,
false
);
// The interpolation is exponential.
// It would need an infinite speed to go away from 0.
// This is why the scale is set to 1 directly at the end.
for (let i = 0; i < 11; i++) {
expect(sprite.getScale()).to.be(0);
runtimeScene.renderAndStep(1000 / 60);
}
expect(spriteBehavior.hasFinished('MyTween')).to.be(true);
expect(sprite.getScaleX()).to.be(1);
expect(sprite.getX()).to.be(100);
expect(sprite.getY()).to.be(400);
});
it('can tween the scale on X axis from center', () => {
sprite.setPosition(100, 400);
sprite.setScaleX(200);
@@ -576,6 +719,53 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(sprite.getY()).to.be(400);
});
it('can tween the scale on Y axis to 0', () => {
sprite.setPosition(100, 400);
sprite.setScaleY(1);
spriteBehavior.addObjectScaleTween3(
'MyTween',
0,
'linear',
0.25,
false,
false
);
// The interpolation is exponential.
// It would need an infinite speed to go away from 0.
// This is why the scale is set to 0 directly.
for (let i = 0; i < 11; i++) {
runtimeScene.renderAndStep(1000 / 60);
expect(sprite.getScaleY()).to.be(0);
}
expect(spriteBehavior.hasFinished('MyTween')).to.be(true);
expect(sprite.getX()).to.be(100);
expect(sprite.getY()).to.be(400);
});
it('can tween the scale on Y axis from 0', () => {
sprite.setPosition(100, 400);
sprite.setScaleY(0);
spriteBehavior.addObjectScaleTween3(
'MyTween',
1,
'linear',
0.25,
false,
false
);
// The interpolation is exponential.
// It would need an infinite speed to go away from 0.
// This is why the scale is set to 1 directly at the end.
for (let i = 0; i < 11; i++) {
expect(sprite.getScale()).to.be(0);
runtimeScene.renderAndStep(1000 / 60);
}
expect(spriteBehavior.hasFinished('MyTween')).to.be(true);
expect(sprite.getScaleY()).to.be(1);
expect(sprite.getX()).to.be(100);
expect(sprite.getY()).to.be(400);
});
it('can tween the scale on Y axis from center', () => {
sprite.setPosition(100, 400);
sprite.setScaleY(200);
@@ -623,45 +813,126 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(object.getY()).to.be(660);
});
it('can tween the scales', () => {
it('can tween the scale', () => {
sprite.setPosition(100, 400);
sprite.setScaleX(200);
sprite.setScaleY(300);
spriteBehavior.addObjectScaleTween2(
sprite.setScale(200);
spriteBehavior.addObjectScaleTween3(
'MyTween',
600,
900,
'linear',
0.25,
false,
false
);
checkProgress(6, [() => sprite.getScaleX(), () => sprite.getScaleY()]);
checkProgress(6, () => sprite.getScale());
// The interpolation is exponential.
expect(sprite.getScaleX()).to.be(386.6364089863524);
expect(sprite.getScaleY()).to.be(579.9546134795287);
expect(sprite.getScale()).to.be(386.6364089863524);
expect(sprite.getX()).to.be(100);
expect(sprite.getY()).to.be(400);
});
it('can tween the scale to 0', () => {
sprite.setPosition(100, 400);
sprite.setScale(1);
spriteBehavior.addObjectScaleTween3(
'MyTween',
0,
'linear',
0.25,
false,
false
);
// The interpolation is exponential.
// It would need an infinite speed to go away from 0.
// This is why the scale is set to 0 directly.
for (let i = 0; i < 11; i++) {
runtimeScene.renderAndStep(1000 / 60);
expect(sprite.getScale()).to.be(0);
}
expect(spriteBehavior.hasFinished('MyTween')).to.be(true);
expect(sprite.getX()).to.be(100);
expect(sprite.getY()).to.be(400);
});
it('can tween the scale from 0', () => {
sprite.setPosition(100, 400);
sprite.setScale(0);
spriteBehavior.addObjectScaleTween3(
'MyTween',
1,
'linear',
0.25,
false,
false
);
// The interpolation is exponential.
// It would need an infinite speed to go away from 0.
// This is why the scale is set to 1 directly at the end.
for (let i = 0; i < 11; i++) {
expect(sprite.getScale()).to.be(0);
runtimeScene.renderAndStep(1000 / 60);
}
expect(spriteBehavior.hasFinished('MyTween')).to.be(true);
expect(sprite.getScale()).to.be(1);
expect(sprite.getX()).to.be(100);
expect(sprite.getY()).to.be(400);
});
it('can tween the scales from center', () => {
sprite.setPosition(100, 400);
sprite.setScaleX(200);
sprite.setScaleY(300);
spriteBehavior.addObjectScaleTween2(
sprite.setScale(200);
spriteBehavior.addObjectScaleTween3(
'MyTween',
600,
900,
'linear',
0.25,
false,
true
);
checkProgress(6, [() => sprite.getScaleX(), () => sprite.getScaleY()]);
checkProgress(6, () => sprite.getScale());
// The interpolation is exponential.
expect(sprite.getScaleX()).to.be(386.6364089863524);
expect(sprite.getScaleY()).to.be(579.9546134795287);
expect(sprite.getScale()).to.be(386.6364089863524);
expect(sprite.getX()).to.be(-5872.3650875632775);
expect(sprite.getY()).to.be(-8558.547631344918);
expect(sprite.getY()).to.be(-5572.3650875632775);
});
it('can tween the scale of a cube', () => {
cube.setPosition(100, 400);
cube.setZ(800);
cube.setScale(200);
cubeBehavior.addObjectScaleTween3(
'MyTween',
600,
'linear',
0.25,
false,
false
);
checkProgress(6, () => cube.getScale());
// The interpolation is exponential.
expect(cube.getScale()).to.be(386.6364089863524);
expect(cube.getX()).to.be(100);
expect(cube.getY()).to.be(400);
expect(cube.getZ()).to.be(800);
});
it('can tween the scales of a cube from center', () => {
cube.setPosition(100, 400);
cube.setZ(800);
cube.setScale(200);
cubeBehavior.addObjectScaleTween3(
'MyTween',
600,
'linear',
0.25,
false,
true
);
checkProgress(6, () => cube.getScale());
// The interpolation is exponential.
expect(cube.getScale()).to.be(386.6364089863524);
expect(cube.getX()).to.be(-5872.3650875632775);
expect(cube.getY()).to.be(-5572.3650875632775);
expect(cube.getZ()).to.be(-5172.3650875632775);
});
});

View File

@@ -15,27 +15,44 @@ namespace gdjs {
}
function isScalable(
o: gdjs.RuntimeObject
): o is gdjs.RuntimeObject & gdjs.Scalable {
//@ts-ignore We are checking if the methods are present.
return o.setScaleX && o.setScaleY && o.getScaleX && o.getScaleY;
object: gdjs.RuntimeObject
): object is gdjs.RuntimeObject & gdjs.Scalable {
return (
//@ts-ignore We are checking if the methods are present.
object.setScaleX &&
//@ts-ignore
object.setScaleY &&
//@ts-ignore
object.getScaleX &&
//@ts-ignore
object.getScaleY
);
}
function isOpaque(
o: gdjs.RuntimeObject
): o is gdjs.RuntimeObject & gdjs.OpacityHandler {
object: gdjs.RuntimeObject
): object is gdjs.RuntimeObject & gdjs.OpacityHandler {
//@ts-ignore We are checking if the methods are present.
return o.setOpacity && o.getOpacity;
return object.setOpacity && object.getOpacity;
}
function isColorable(o: gdjs.RuntimeObject): o is IColorable {
function is3D(
object: gdjs.RuntimeObject
): object is gdjs.RuntimeObject & gdjs.Base3DHandler {
//@ts-ignore We are checking if the methods are present.
return o.setColor && o.getColor;
return object.getZ && object.setZ;
}
function isCharacterScalable(o: gdjs.RuntimeObject): o is ICharacterScalable {
function isColorable(object: gdjs.RuntimeObject): object is IColorable {
//@ts-ignore We are checking if the methods are present.
return o.setCharacterSize && o.getCharacterSize;
return object.setColor && object.getColor;
}
function isCharacterScalable(
object: gdjs.RuntimeObject
): object is ICharacterScalable {
//@ts-ignore We are checking if the methods are present.
return object.setCharacterSize && object.getCharacterSize;
}
const linearInterpolation = gdjs.evtTools.common.lerp;
@@ -456,7 +473,7 @@ namespace gdjs {
/**
* Tween an object Z position.
* @deprecated Use the 3D Tween extension instead.
* @deprecated Use addObjectPositionZTween2 instead.
* @param identifier Unique id to identify the tween
* @param toZ The target Z position
* @param easing Easing function identifier
@@ -469,14 +486,59 @@ namespace gdjs {
easing: string,
duration: float,
destroyObjectWhenFinished: boolean
) {
this._addObjectPositionZTween(
identifier,
toZ,
easing,
duration / 1000,
destroyObjectWhenFinished,
this.owner.getRuntimeScene()
);
}
/**
* Tween an object Z position.
* @param object3DBehavior Only used by events can be set to null
* @param identifier Unique id to identify the tween
* @param toZ The target Z position
* @param easing Easing function identifier
* @param duration Duration in seconds
* @param destroyObjectWhenFinished Destroy this object when the tween ends
*/
addObjectPositionZTween2(
object3DBehavior: any,
identifier: string,
toZ: number,
easing: string,
duration: float,
destroyObjectWhenFinished: boolean
) {
this._addObjectPositionZTween(
identifier,
toZ,
easing,
duration,
destroyObjectWhenFinished,
this.owner
);
}
private _addObjectPositionZTween(
identifier: string,
toZ: number,
easing: string,
duration: float,
destroyObjectWhenFinished: boolean,
timeSource: gdjs.evtTools.tween.TimeSource
) {
const { owner } = this;
if (!(owner instanceof gdjs.RuntimeObject3D)) return;
if (!is3D(owner)) return;
this._tweens.addSimpleTween(
identifier,
this.owner.getRuntimeScene(),
duration / 1000,
timeSource,
duration,
easing,
linearInterpolation,
owner.getZ(),
@@ -558,6 +620,72 @@ namespace gdjs {
);
}
/**
* Tween a 3D object rotation X.
* @param object3DBehavior Only used by events can be set to null
* @param identifier Unique id to identify the tween
* @param toAngle The target angle
* @param easing Easing function identifier
* @param duration Duration in seconds
* @param destroyObjectWhenFinished Destroy this object when the tween ends
*/
addObjectRotationXTween(
object3DBehavior: any,
identifier: string,
toAngle: float,
easing: string,
duration: float,
destroyObjectWhenFinished: boolean
) {
const { owner } = this;
if (!is3D(owner)) return;
this._tweens.addSimpleTween(
identifier,
this.owner,
duration,
easing,
linearInterpolation,
owner.getRotationX(),
toAngle,
(value: float) => owner.setRotationX(value),
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
/**
* Tween a 3D object rotation Y.
* @param object3DBehavior Only used by events can be set to null
* @param identifier Unique id to identify the tween
* @param toAngle The target angle
* @param easing Easing function identifier
* @param duration Duration in seconds
* @param destroyObjectWhenFinished Destroy this object when the tween ends
*/
addObjectRotationYTween(
object3DBehavior: any,
identifier: string,
toAngle: float,
easing: string,
duration: float,
destroyObjectWhenFinished: boolean
) {
const { owner } = this;
if (!is3D(owner)) return;
this._tweens.addSimpleTween(
identifier,
this.owner,
duration,
easing,
linearInterpolation,
owner.getRotationY(),
toAngle,
(value: float) => owner.setRotationY(value),
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
/**
* Tween an object scale.
* @deprecated Use addObjectScaleTween2 instead.
@@ -593,6 +721,7 @@ namespace gdjs {
/**
* Tween an object scale.
* @deprecated Use addObjectScaleXTween2 and addObjectScaleYTween2 instead.
* @param identifier Unique id to identify the tween
* @param toScaleX The target X-scale
* @param toScaleY The target Y-scale
@@ -666,6 +795,68 @@ namespace gdjs {
);
}
/**
* Tween an object scale.
* @param identifier Unique id to identify the tween
* @param toScale The target scale
* @param easing Easing function identifier
* @param duration Duration in seconds
* @param destroyObjectWhenFinished Destroy this object when the tween ends
* @param scaleFromCenterOfObject Scale the transform from the center of the object (or point that is called center), not the top-left origin
*/
addObjectScaleTween3(
identifier: string,
toScale: number,
easing: string,
duration: float,
destroyObjectWhenFinished: boolean,
scaleFromCenterOfObject: boolean
) {
this._addObjectScaleXTween(
identifier,
toScale,
easing,
duration,
destroyObjectWhenFinished,
scaleFromCenterOfObject,
this.owner,
exponentialInterpolation
);
const owner = this.owner;
if (!isScalable(owner)) return;
// This action doesn't require 3D capabilities.
// So, gdjs.RuntimeObject3D may not exist
// when the 3D extension is not used.
const owner3d = is3D(owner) ? owner : null;
const setValue = scaleFromCenterOfObject
? (scale: float) => {
const oldX = owner.getCenterXInScene();
const oldY = owner.getCenterYInScene();
const oldZ = owner3d ? owner3d.getCenterZInScene() : 0;
owner.setScale(scale);
owner.setCenterXInScene(oldX);
owner.setCenterYInScene(oldY);
if (owner3d) {
owner3d.setCenterZInScene(oldZ);
}
}
: (scale: float) => owner.setScale(scale);
this._tweens.addSimpleTween(
identifier,
this.owner,
duration,
easing,
exponentialInterpolation,
owner.getScale(),
toScale,
setValue,
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
/**
* Tween an object X-scale.
* @deprecated Use addObjectScaleXTween2 instead.
@@ -1519,7 +1710,7 @@ namespace gdjs {
/**
* Tween an object depth.
* @deprecated Use the 3D Tween extension instead.
* @deprecated Use addObjectDepthTween2 instead.
* @param identifier Unique id to identify the tween
* @param toDepth The target depth
* @param easing Easing function identifier
@@ -1532,14 +1723,59 @@ namespace gdjs {
easing: string,
duration: float,
destroyObjectWhenFinished: boolean
) {
this._addObjectDepthTween(
identifier,
toDepth,
easing,
duration / 1000,
destroyObjectWhenFinished,
this.owner.getRuntimeScene()
);
}
/**
* Tween an object depth.
* @param object3DBehavior Only used by events can be set to null
* @param identifier Unique id to identify the tween
* @param toDepth The target depth
* @param easing Easing function identifier
* @param duration Duration in seconds
* @param destroyObjectWhenFinished Destroy this object when the tween ends
*/
addObjectDepthTween2(
object3DBehavior: any,
identifier: string,
toDepth: float,
easing: string,
duration: float,
destroyObjectWhenFinished: boolean
) {
this._addObjectDepthTween(
identifier,
toDepth,
easing,
duration,
destroyObjectWhenFinished,
this.owner
);
}
private _addObjectDepthTween(
identifier: string,
toDepth: float,
easing: string,
duration: float,
destroyObjectWhenFinished: boolean,
timeSource: gdjs.evtTools.tween.TimeSource
) {
const { owner } = this;
if (!(owner instanceof gdjs.RuntimeObject3D)) return;
if (!is3D(owner)) return;
this._tweens.addSimpleTween(
identifier,
this.owner.getRuntimeScene(),
duration / 1000,
timeSource,
duration,
easing,
linearInterpolation,
owner.getDepth(),

2
GDJS/.gitignore vendored
View File

@@ -1 +1 @@
/node_modules
/node_modules

View File

@@ -125,14 +125,18 @@ gd::ObjectMetadata &MetadataDeclarationHelper::DeclareObjectMetadata(
// Note: EventsFunctionsExtension should be used instead of
// PlatformExtension but this line will be removed soon.
.SetCategoryFullName(extension.GetCategory())
// Update Project::CreateObject when default behavior are added.
// Update Project::CreateObject when default behaviors are added.
.AddDefaultBehavior("EffectCapability::EffectBehavior")
.AddDefaultBehavior("ResizableCapability::ResizableBehavior")
.AddDefaultBehavior("ScalableCapability::ScalableBehavior")
.AddDefaultBehavior("FlippableCapability::FlippableBehavior")
.AddDefaultBehavior("OpacityCapability::OpacityBehavior");
.AddDefaultBehavior("FlippableCapability::FlippableBehavior");
if (eventsBasedObject.IsRenderedIn3D()) {
objectMetadata.MarkAsRenderedIn3D();
objectMetadata
.MarkAsRenderedIn3D()
.AddDefaultBehavior("Scene3D::Base3DBehavior");
}
else {
objectMetadata.AddDefaultBehavior("OpacityCapability::OpacityBehavior");
}
// TODO EBO Use full type to identify object to avoid collision.
@@ -933,18 +937,15 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
gd::String MetadataDeclarationHelper::GetStringifiedExtraInfo(
const gd::PropertyDescriptor &property) {
gd::String stringifiedExtraInfo = "";
if (property.GetType() == "Choice") {
stringifiedExtraInfo += "[";
for (size_t i = 0; i < property.GetExtraInfo().size(); i++) {
stringifiedExtraInfo += property.GetExtraInfo().at(i);
if (i < property.GetExtraInfo().size() - 1) {
stringifiedExtraInfo += ",";
}
}
stringifiedExtraInfo += "]";
if (property.GetType() != "Choice") {
return "";
}
return stringifiedExtraInfo;
SerializerElement element;
element.ConsiderAsArray();
for (auto&& value : property.GetExtraInfo()) {
element.AddChild("").SetStringValue(value);
}
return Serializer::ToJSON(element);
}
gd::String
@@ -968,6 +969,9 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
addObjectAndBehaviorParameters) {
auto &propertyType = property.GetType();
auto group = (eventsBasedEntity.GetFullName() || eventsBasedEntity.GetName())
+ " " + property.GetGroup() + " properties";
auto uncapitalizedLabel =
UncapitalizeFirstLetter(property.GetLabel()) || property.GetName();
if (propertyType == "Boolean") {
@@ -977,7 +981,7 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
.FindAndReplace("<property_name>", uncapitalizedLabel),
_("Property <property_name> of _PARAM0_ is true")
.FindAndReplace("<property_name>", uncapitalizedLabel),
eventsBasedEntity.GetFullName() || eventsBasedEntity.GetName(),
group,
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
addObjectAndBehaviorParameters(conditionMetadata);
conditionMetadata.SetFunctionName(getterName);
@@ -992,7 +996,7 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
.FindAndReplace("<property_value>",
"_PARAM" + gd::String::From(valueParameterIndex) +
"_"),
eventsBasedEntity.GetFullName() || eventsBasedEntity.GetName(),
group,
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
addObjectAndBehaviorParameters(setterActionMetadata);
setterActionMetadata
@@ -1007,7 +1011,7 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
.FindAndReplace("<property_name>", uncapitalizedLabel),
_("Toggle property <property_name> of _PARAM0_")
.FindAndReplace("<property_name>", uncapitalizedLabel),
eventsBasedEntity.GetFullName() || eventsBasedEntity.GetName(),
group,
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
addObjectAndBehaviorParameters(toggleActionMetadata);
toggleActionMetadata.SetFunctionName(toggleFunctionName);
@@ -1018,13 +1022,14 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
parameterOptions.SetTypeExtraInfo(typeExtraInfo);
auto propertyInstructionMetadata =
entityMetadata.AddExpressionAndConditionAndAction(
gd::ValueTypeMetadata::ConvertPropertyTypeToValueType(propertyType),
gd::ValueTypeMetadata::GetPrimitiveValueType(
gd::ValueTypeMetadata::ConvertPropertyTypeToValueType(propertyType)),
expressionName, propertyLabel,
_("the property value for the <property_name>")
.FindAndReplace("<property_name>", uncapitalizedLabel),
_("the property value for the <property_name>")
.FindAndReplace("<property_name>", uncapitalizedLabel),
eventsBasedEntity.GetFullName() || eventsBasedEntity.GetName(),
group,
GetExtensionIconUrl(extension));
addObjectAndBehaviorParameters(propertyInstructionMetadata);
propertyInstructionMetadata
@@ -1213,20 +1218,39 @@ void MetadataDeclarationHelper::DeclareObjectInternalInstructions(
// Objects are identified by their name alone.
auto &objectType = eventsBasedObject.GetName();
objectMetadata
.AddScopedAction(
"SetRotationCenter", _("Center of rotation"),
_("Change the center of rotation of an object relatively to the "
"object origin."),
_("Change the center of rotation of _PARAM0_ to _PARAM1_, _PARAM2_"),
_("Angle"), "res/actions/position24_black.png",
"res/actions/position_black.png")
.AddParameter("object", _("Object"), objectType)
.AddParameter("number", _("X position"))
.AddParameter("number", _("Y position"))
.MarkAsAdvanced()
.SetPrivate()
.SetFunctionName("setRotationCenter");
if (eventsBasedObject.IsRenderedIn3D()) {
objectMetadata
.AddScopedAction(
"SetRotationCenter", _("Center of rotation"),
_("Change the center of rotation of an object relatively to the "
"object origin."),
_("Change the center of rotation of _PARAM0_ to _PARAM1_ ; _PARAM2_ ; _PARAM3_"),
_("Angle"), "res/actions/position24_black.png",
"res/actions/position_black.png")
.AddParameter("object", _("Object"), objectType)
.AddParameter("number", _("X position"))
.AddParameter("number", _("Y position"))
.AddParameter("number", _("Z position"))
.MarkAsAdvanced()
.SetPrivate()
.SetFunctionName("setRotationCenter3D");
}
else {
objectMetadata
.AddScopedAction(
"SetRotationCenter", _("Center of rotation"),
_("Change the center of rotation of an object relatively to the "
"object origin."),
_("Change the center of rotation of _PARAM0_ to _PARAM1_ ; _PARAM2_"),
_("Angle"), "res/actions/position24_black.png",
"res/actions/position_black.png")
.AddParameter("object", _("Object"), objectType)
.AddParameter("number", _("X position"))
.AddParameter("number", _("Y position"))
.MarkAsAdvanced()
.SetPrivate()
.SetFunctionName("setRotationCenter");
}
}
void MetadataDeclarationHelper::AddParameter(

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