Compare commits

...

80 Commits

Author SHA1 Message Date
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
440 changed files with 31187 additions and 35755 deletions

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

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

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

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

@@ -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,222 @@
/*
* 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 "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::map<gd::String, gd::String> &resourcesFileNameMap) {
auto cleanObject = object.Clone();
cleanObject->GetVariables().Clear();
cleanObject->GetEffects().Clear();
for (auto &&behaviorName : cleanObject->GetAllBehaviorNames()) {
cleanObject->RemoveBehavior(behaviorName);
}
gd::String extensionName = GetObjectExtensionName(*cleanObject);
std::map<gd::String, gd::String> resourcesNameReverseMap;
gd::ObjectAssetSerializer::RenameObjectResourceFiles(
project, *cleanObject, "", objectFullName, resourcesFileNameMap,
resourcesNameReverseMap);
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 &&newResourceName : resourcesInUse.GetAllResources()) {
if (newResourceName.length() == 0) {
continue;
}
auto &resource = resourcesManager.GetResource(
resourcesNameReverseMap.find(newResourceName) !=
resourcesNameReverseMap.end()
? resourcesNameReverseMap[newResourceName]
: newResourceName);
SerializerElement &resourceElement = resourcesElement.AddChild("resource");
resource.SerializeTo(resourceElement);
// Override name and file because the project and the asset don't use the
// same one.
resourceElement.SetAttribute("kind", resource.GetKind());
resourceElement.SetAttribute("name", newResourceName);
auto &oldFilePath = resource.GetFile();
resourceElement.SetAttribute("file",
resourcesFileNameMap.find(oldFilePath) !=
resourcesFileNameMap.end()
? resourcesFileNameMap[oldFilePath]
: oldFilePath);
}
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");
}
void ObjectAssetSerializer::RenameObjectResourceFiles(
gd::Project &project, gd::Object &object,
const gd::String &destinationDirectory, const gd::String &objectFullName,
std::map<gd::String, gd::String> &resourcesFileNameMap,
std::map<gd::String, gd::String> &resourcesNameReverseMap) {
gd::AssetResourcePathCleaner assetResourcePathCleaner(
project.GetResourcesManager(), resourcesFileNameMap,
resourcesNameReverseMap);
object.GetConfiguration().ExposeResources(assetResourcePathCleaner);
// Use asset store script naming conventions for sprite resource files.
if (object.GetConfiguration().GetType() == "Sprite") {
gd::SpriteObject &spriteConfiguration =
dynamic_cast<gd::SpriteObject &>(object.GetConfiguration());
std::map<gd::String, gd::String> normalizedFileNames;
for (std::size_t animationIndex = 0;
animationIndex < spriteConfiguration.GetAnimationsCount();
animationIndex++) {
auto &animation = spriteConfiguration.GetAnimation(animationIndex);
auto &direction = animation.GetDirection(0);
const gd::String &animationName =
animation.GetName().empty()
? gd::String::From(animationIndex)
: animation.GetName().FindAndReplace("_", " ", true);
// Search frames that share the same resource.
std::map<gd::String, std::vector<int>> frameIndexes;
for (std::size_t frameIndex = 0; frameIndex < direction.GetSpritesCount();
frameIndex++) {
auto &frame = direction.GetSprite(frameIndex);
if (frameIndexes.find(frame.GetImageName()) == frameIndexes.end()) {
std::vector<int> emptyVector;
frameIndexes[frame.GetImageName()] = emptyVector;
}
auto &indexes = frameIndexes[frame.GetImageName()];
indexes.push_back(frameIndex);
}
for (std::size_t frameIndex = 0; frameIndex < direction.GetSpritesCount();
frameIndex++) {
auto &frame = direction.GetSprite(frameIndex);
auto oldName = frame.GetImageName();
if (normalizedFileNames.find(oldName) != normalizedFileNames.end()) {
gd::LogWarning("The resource \"" + oldName +
"\" is shared by several animations.");
continue;
}
gd::String newName = objectFullName;
if (spriteConfiguration.GetAnimationsCount() > 1) {
newName += "_" + animationName;
}
if (direction.GetSpritesCount() > 1) {
newName += "_";
auto &indexes = frameIndexes[frame.GetImageName()];
for (size_t i = 0; i < indexes.size(); i++) {
newName += gd::String::From(indexes.at(i) + 1);
if (i < indexes.size() - 1) {
newName += ";";
}
}
}
gd::String extension = oldName.substr(oldName.find_last_of("."));
newName += extension;
frame.SetImageName(newName);
normalizedFileNames[oldName] = newName;
}
}
for (std::map<gd::String, gd::String>::const_iterator it =
resourcesFileNameMap.begin();
it != resourcesFileNameMap.end(); ++it) {
if (!it->first.empty()) {
gd::String originFile = it->first;
gd::String destinationFile = it->second;
resourcesFileNameMap[originFile] = normalizedFileNames[destinationFile];
}
}
auto clonedResourcesNameReverseMap = resourcesNameReverseMap;
resourcesNameReverseMap.clear();
for (std::map<gd::String, gd::String>::const_iterator it =
clonedResourcesNameReverseMap.begin();
it != clonedResourcesNameReverseMap.end(); ++it) {
if (!it->first.empty()) {
gd::String newResourceName = it->first;
gd::String oldResourceName = it->second;
resourcesNameReverseMap[normalizedFileNames[newResourceName]] =
oldResourceName;
}
}
}
}
} // namespace gd

View File

@@ -0,0 +1,64 @@
/*
* 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 resourcesFileNameMap The map from project resource file paths to
* asset resource file paths.
*/
static void
SerializeTo(gd::Project &project, const gd::Object &object,
const gd::String &objectFullName, SerializerElement &element,
std::map<gd::String, gd::String> &resourcesFileNameMap);
~ObjectAssetSerializer(){};
private:
ObjectAssetSerializer(){};
static void RenameObjectResourceFiles(
gd::Project &project, gd::Object &object,
const gd::String &destinationDirectory, const gd::String &objectFullName,
std::map<gd::String, gd::String> &resourcesFileNameMap,
std::map<gd::String, gd::String> &resourcesNameReverseMap);
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

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

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

@@ -30,6 +30,9 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
extraInformationElement.AddChild("").SetStringValue(information);
}
element.AddChild("hidden").SetBoolValue(hidden);
if (deprecated) {
element.AddChild("deprecated").SetBoolValue(deprecated);
}
}
void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
@@ -58,6 +61,9 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
hidden = element.HasChild("hidden")
? element.GetChild("hidden").GetBoolValue()
: false;
deprecated = element.HasChild("deprecated")
? element.GetChild("deprecated").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),
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),
measurementUnit(gd::MeasurementUnit::GetUndefined()){};
/**
* \brief Destructor
@@ -142,6 +146,19 @@ 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; }
/** \name Serialization
*/
///@{
@@ -179,6 +196,7 @@ class GD_CORE_API PropertyDescriptor {
///< choices, if a property is a displayed as a combo
///< box.
bool hidden;
bool deprecated;
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,148 @@
/*
* 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::map<gd::String, gd::String> resourcesFileNameMap;
ObjectAssetSerializer::SerializeTo(project, object, "My Object",
assetElement, resourcesFileNameMap);
// This mapping is used to copy resource files.
REQUIRE(resourcesFileNameMap.find("assets/Idle.png") !=
resourcesFileNameMap.end());
REQUIRE(resourcesFileNameMap["assets/Idle.png"] == "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") == "Idle.png");
REQUIRE(resourceElement.GetStringAttribute("file") == "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") == "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;

1
Extensions/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
Spine/pixi-spine

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'))
@@ -2695,7 +2695,7 @@ module.exports = {
}
static getThumbnail(project, resourcesLoader, objectConfiguration) {
return 'JsPlatform/Extensions/3d_box.svg';
return 'JsPlatform/Extensions/3d_model.svg';
}
getOriginX() {

View File

@@ -11,7 +11,12 @@ namespace gdjs {
const layer = runtimeScene.getLayer(layerName);
const layerRenderer = layer.getRenderer();
const threeCamera = layerRenderer.getThreeCamera();
const fov = threeCamera ? threeCamera.fov : assumedFovIn2D;
const fov =
threeCamera instanceof THREE.OrthographicCamera
? null
: threeCamera
? threeCamera.fov
: assumedFovIn2D;
return layer.getCameraZ(fov, cameraIndex);
};
@@ -24,7 +29,12 @@ namespace gdjs {
const layer = runtimeScene.getLayer(layerName);
const layerRenderer = layer.getRenderer();
const threeCamera = layerRenderer.getThreeCamera();
const fov = threeCamera ? threeCamera.fov : assumedFovIn2D;
const fov =
threeCamera instanceof THREE.OrthographicCamera
? null
: threeCamera
? threeCamera.fov
: assumedFovIn2D;
layer.setCameraZ(z, fov, cameraIndex);
};
@@ -213,8 +223,10 @@ namespace gdjs {
const layerRenderer = layer.getRenderer();
const threeCamera = layerRenderer.getThreeCamera();
if (!threeCamera) return 45;
return threeCamera.fov;
if (!threeCamera) return assumedFovIn2D;
return threeCamera instanceof THREE.OrthographicCamera
? 0
: threeCamera.fov;
};
export const setFov = (
@@ -227,7 +239,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

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

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.

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,199 @@
/*
* 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);
};
PIXI.Assets.setPreferences({
preferWorkers: false,
crossOrigin: this._resourceLoader.checkIfCredentialsRequired(
resource.file
)
? 'use-credentials'
: 'anonymous',
});
PIXI.Assets.add(resource.name, resource.file, { 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,120 @@
/*
* 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
);
PIXI.Assets.setPreferences({
preferWorkers: false,
crossOrigin: this._resourceLoader.checkIfCredentialsRequired(
resource.file
)
? 'use-credentials'
: 'anonymous',
});
PIXI.Assets.add(resource.name, resource.file, { 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;
}
}
}

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

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

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
*/
@@ -123,6 +149,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 +158,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 +249,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 +283,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 +483,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
*/
@@ -123,6 +149,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 +158,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 +437,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 +458,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 +500,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 +629,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 +712,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 +806,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

@@ -456,7 +456,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 +469,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;
this._tweens.addSimpleTween(
identifier,
this.owner.getRuntimeScene(),
duration / 1000,
timeSource,
duration,
easing,
linearInterpolation,
owner.getZ(),
@@ -558,6 +603,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 (!(owner instanceof gdjs.RuntimeObject3D)) 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 (!(owner instanceof gdjs.RuntimeObject3D)) 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 +704,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 +778,65 @@ 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;
const owner3d = owner instanceof gdjs.RuntimeObject3D ? 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 +1690,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 +1703,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;
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

@@ -124,6 +124,8 @@ namespace gdjs {
private _jsonManager: JsonManager;
private _model3DManager: Model3DManager;
private _bitmapFontManager: BitmapFontManager;
private _spineAtlasManager: SpineAtlasManager | null = null;
private _spineManager: SpineManager | null = null;
/**
* Only used by events.
@@ -172,6 +174,18 @@ namespace gdjs {
);
this._model3DManager = new gdjs.Model3DManager(this);
// add spine related managers only if spine extension is used
if (gdjs.SpineAtlasManager && gdjs.SpineManager) {
this._spineAtlasManager = new gdjs.SpineAtlasManager(
this,
this._imageManager
);
this._spineManager = new gdjs.SpineManager(
this,
this._spineAtlasManager
);
}
const resourceManagers: Array<ResourceManager> = [
this._imageManager,
this._soundManager,
@@ -180,6 +194,11 @@ namespace gdjs {
this._bitmapFontManager,
this._model3DManager,
];
if (this._spineAtlasManager)
resourceManagers.push(this._spineAtlasManager);
if (this._spineManager) resourceManagers.push(this._spineManager);
this._resourceManagersMap = new Map<ResourceKind, ResourceManager>();
for (const resourceManager of resourceManagers) {
for (const resourceKind of resourceManager.getResourceKinds()) {
@@ -188,6 +207,13 @@ namespace gdjs {
}
}
/**
* @returns the runtime game instance.
*/
getRuntimeGame(): RuntimeGame {
return this._runtimeGame;
}
/**
* Update the resources data of the game. Useful for hot-reloading, should
* not be used otherwise.
@@ -576,6 +602,24 @@ namespace gdjs {
getModel3DManager(): gdjs.Model3DManager {
return this._model3DManager;
}
/**
* Get the Spine manager of the game, used to load and construct spine skeletons from game
* resources.
* @return The Spine manager for the game
*/
getSpineManager(): gdjs.SpineManager | null {
return this._spineManager;
}
/**
* Get the Spine Atlas manager of the game, used to load atlases from game
* resources.
* @return The Spine Atlas manager for the game
*/
getSpineAtlasManager(): gdjs.SpineAtlasManager | null {
return this._spineAtlasManager;
}
}
type PromiseError<T> = { item: T; error: Error };

View File

@@ -56,7 +56,7 @@ namespace gdjs {
setCameraZ(z: float, fov: float, cameraId?: integer): void {}
getCameraZ(fov: float = 45, cameraId?: integer): float {
getCameraZ(fov: float | null, cameraId?: integer): float {
return 0;
}

View File

@@ -19,6 +19,15 @@ namespace gdjs {
? RuntimeLayerRenderingType.TWO_D_PLUS_THREE_D
: RuntimeLayerRenderingType.TWO_D;
export enum RuntimeLayerCameraType {
PERSPECTIVE,
ORTHOGRAPHIC,
}
const getCameraTypeFromString = (renderingTypeAsString: string | undefined) =>
renderingTypeAsString === 'orthographic'
? RuntimeLayerCameraType.ORTHOGRAPHIC
: RuntimeLayerCameraType.PERSPECTIVE;
/**
* Represents a layer of a "container", used to display objects.
* The container can be a scene (see gdjs.Layer)
@@ -27,6 +36,7 @@ namespace gdjs {
export abstract class RuntimeLayer implements EffectsTarget {
_name: string;
_renderingType: RuntimeLayerRenderingType;
_cameraType: RuntimeLayerCameraType;
_timeScale: float = 1;
_defaultZOrder: integer = 0;
_hidden: boolean;
@@ -59,12 +69,13 @@ namespace gdjs {
) {
this._name = layerData.name;
this._renderingType = getRenderingTypeFromString(layerData.renderingType);
this._cameraType = getCameraTypeFromString(layerData.cameraType);
this._hidden = !layerData.visibility;
this._initialCamera3DFieldOfView = layerData.camera3DFieldOfView || 45;
this._initialCamera3DFarPlaneDistance =
layerData.camera3DFarPlaneDistance || 0.1;
this._initialCamera3DNearPlaneDistance =
layerData.camera3DNearPlaneDistance || 2000;
layerData.camera3DNearPlaneDistance || 0.1;
this._initialCamera3DFarPlaneDistance =
layerData.camera3DFarPlaneDistance || 2000;
this._initialEffectsData = layerData.effects || [];
this._runtimeScene = instanceContainer;
this._effectsManager = instanceContainer.getGame().getEffectsManager();
@@ -103,6 +114,10 @@ namespace gdjs {
return this._renderingType;
}
getCameraType(): RuntimeLayerCameraType {
return this._cameraType;
}
/**
* Get the default Z order to be attributed to objects created on this layer
* (usually from events generated code).
@@ -246,7 +261,7 @@ namespace gdjs {
* @param fov The field of view.
* @param cameraId The camera number. Currently ignored.
*/
abstract setCameraZ(z: float, fov: float, cameraId?: integer): void;
abstract setCameraZ(z: float, fov: float | null, cameraId?: integer): void;
/**
* Get the camera center Z position.
@@ -255,7 +270,7 @@ namespace gdjs {
* @param cameraId The camera number. Currently ignored.
* @return The z position of the camera
*/
abstract getCameraZ(fov: float, cameraId?: integer): float;
abstract getCameraZ(fov: float | null, cameraId?: integer): float;
/**
* Get the rotation of the camera, expressed in degrees.

View File

@@ -290,6 +290,16 @@ namespace gdjs {
end: float,
progress: float
) => {
if (progress === 0) {
return start;
}
if (progress === 1) {
return end;
}
if (start <= 0 || end <= 0) {
// The exponential function is flattened to something like a 90° corner.
return 0;
}
const startLog = Math.log(start);
const endLog = Math.log(end);
return Math.exp(startLog + (endLog - startLog) * progress);

View File

@@ -43,17 +43,22 @@ namespace gdjs {
oldGameResolutionOriginX: float,
oldGameResolutionOriginY: float
): void {
// Adapt position of the camera center as:
// * Most cameras following a player/object on the scene will be updating this
// in events anyway.
// Adapt position of the camera center only if the camera has never moved as:
// * When the camera follows a player/object, it will rarely be at the default position.
// * Cameras not following a player/object are usually UIs which are intuitively
// expected not to "move". Not adapting the center position would make the camera
// move from its initial position (which is centered in the screen) - and anchor
// behavior would behave counterintuitively.
this._cameraX +=
this._runtimeScene.getViewportOriginX() - oldGameResolutionOriginX;
this._cameraY +=
this._runtimeScene.getViewportOriginY() - oldGameResolutionOriginY;
if (
this._cameraX === oldGameResolutionOriginX &&
this._cameraY === oldGameResolutionOriginY &&
this._zoomFactor === 1
) {
this._cameraX +=
this._runtimeScene.getViewportOriginX() - oldGameResolutionOriginX;
this._cameraY +=
this._runtimeScene.getViewportOriginY() - oldGameResolutionOriginY;
}
this._renderer.updatePosition();
}
@@ -154,18 +159,20 @@ namespace gdjs {
* @param fov The field of view.
* @param cameraId The camera number. Currently ignored.
*/
setCameraZ(z: float, fov: float = 45, cameraId?: integer): void {
const cameraFovInRadians = gdjs.toRad(fov);
setCameraZ(z: float, fov: float | null, cameraId?: integer): void {
if (fov) {
const cameraFovInRadians = gdjs.toRad(fov);
// The zoom factor is capped to a not too big value to avoid infinity.
// MAX_SAFE_INTEGER is an arbitrary choice. It's big but not too big.
const zoomFactor = Math.min(
Number.MAX_SAFE_INTEGER,
(0.5 * this.getHeight()) / (z * Math.tan(0.5 * cameraFovInRadians))
);
// The zoom factor is capped to a not too big value to avoid infinity.
// MAX_SAFE_INTEGER is an arbitrary choice. It's big but not too big.
const zoomFactor = Math.min(
Number.MAX_SAFE_INTEGER,
(0.5 * this.getHeight()) / (z * Math.tan(0.5 * cameraFovInRadians))
);
if (zoomFactor > 0) {
this._zoomFactor = zoomFactor;
if (zoomFactor > 0) {
this._zoomFactor = zoomFactor;
}
}
this._cameraZ = z;
@@ -180,8 +187,8 @@ namespace gdjs {
* @param cameraId The camera number. Currently ignored.
* @return The z position of the camera
*/
getCameraZ(fov: float = 45, cameraId?: integer): float {
if (!this._isCameraZDirty) {
getCameraZ(fov: float | null, cameraId?: integer): float {
if (!this._isCameraZDirty || !fov) {
return this._cameraZ;
}

View File

@@ -29,8 +29,6 @@ namespace gdjs {
*/
setAnimationName(newAnimationName: string): void;
isCurrentAnimationName(name: string): boolean;
/**
* Return true if animation has ended.
* The animation had ended if:
@@ -117,10 +115,6 @@ namespace gdjs {
this.object.setAnimationName(newAnimationName);
}
isCurrentAnimationName(name: string): boolean {
return this.object.isCurrentAnimationName(name);
}
hasAnimationEnded(): boolean {
return this.object.hasAnimationEnded();
}

View File

@@ -34,7 +34,10 @@ namespace gdjs {
// For a 3D (or 2D+3D) layer:
private _threeGroup: THREE.Group | null = null;
private _threeScene: THREE.Scene | null = null;
private _threeCamera: THREE.PerspectiveCamera | null = null;
private _threeCamera:
| THREE.PerspectiveCamera
| THREE.OrthographicCamera
| null = null;
private _threeCameraDirty: boolean = false;
// For a 2D+3D layer, the 2D rendering is done on the render texture
@@ -101,13 +104,23 @@ namespace gdjs {
}
private _update3DCameraAspectAndPosition() {
if (this._threeCamera) {
if (!this._threeCamera) {
return;
}
if (this._threeCamera instanceof THREE.OrthographicCamera) {
const width = this._layer.getWidth();
const height = this._layer.getHeight();
this._threeCamera.left = -width / 2;
this._threeCamera.right = width / 2;
this._threeCamera.top = height / 2;
this._threeCamera.bottom = -height / 2;
} else {
this._threeCamera.aspect =
this._layer.getWidth() / this._layer.getHeight();
this._threeCamera.updateProjectionMatrix();
this.updatePosition();
}
this._threeCamera.updateProjectionMatrix();
this.updatePosition();
}
getRendererObject(): PIXI.Container {
@@ -118,7 +131,10 @@ namespace gdjs {
return this._threeScene;
}
getThreeCamera(): THREE.PerspectiveCamera | null {
getThreeCamera():
| THREE.PerspectiveCamera
| THREE.OrthographicCamera
| null {
return this._threeCamera;
}
@@ -164,12 +180,28 @@ namespace gdjs {
this._threeGroup = new THREE.Group();
this._threeScene.add(this._threeGroup);
this._threeCamera = new THREE.PerspectiveCamera(
this._layer.getInitialCamera3DFieldOfView(),
1,
this._layer.getInitialCamera3DNearPlaneDistance(),
this._layer.getInitialCamera3DFarPlaneDistance()
);
if (
this._layer.getCameraType() ===
gdjs.RuntimeLayerCameraType.ORTHOGRAPHIC
) {
const width = this._layer.getWidth();
const height = this._layer.getHeight();
this._threeCamera = new THREE.OrthographicCamera(
-width / 2,
width / 2,
height / 2,
-height / 2,
this._layer.getInitialCamera3DNearPlaneDistance(),
this._layer.getInitialCamera3DFarPlaneDistance()
);
} else {
this._threeCamera = new THREE.PerspectiveCamera(
this._layer.getInitialCamera3DFieldOfView(),
1,
this._layer.getInitialCamera3DNearPlaneDistance(),
this._layer.getInitialCamera3DFarPlaneDistance()
);
}
this._threeCamera.rotation.order = 'ZYX';
if (
@@ -375,9 +407,14 @@ namespace gdjs {
this._threeCamera.position.y = -this._layer.getCameraY(); // Inverted because the scene is mirrored on Y axis.
this._threeCamera.rotation.z = angle;
this._threeCamera.position.z = this._layer.getCameraZ(
this._threeCamera.fov
);
if (this._threeCamera instanceof THREE.OrthographicCamera) {
this._threeCamera.zoom = this._layer.getCameraZoom();
this._threeCamera.position.z = this._layer.getCameraZ(null);
} else {
this._threeCamera.position.z = this._layer.getCameraZ(
this._threeCamera.fov
);
}
if (this._threePlaneMesh) {
// Adapt the plane size so that it covers the whole screen.
@@ -415,6 +452,8 @@ namespace gdjs {
}
const width = this._layer.getWidth();
const height = this._layer.getHeight();
const normalizedX = (screenX / width) * 2 - 1;
const normalizedY = -(screenY / height) * 2 + 1;
let vector = LayerPixiRenderer.vectorForProjections;
if (!vector) {
@@ -422,12 +461,31 @@ namespace gdjs {
LayerPixiRenderer.vectorForProjections = vector;
}
// https://stackoverflow.com/questions/13055214/mouse-canvas-x-y-to-three-js-world-x-y-z
vector.set((screenX / width) * 2 - 1, -(screenY / height) * 2 + 1, 0.5);
vector.unproject(camera);
vector.sub(camera.position).normalize();
const distance = (worldZ - camera.position.z) / vector.z;
vector.multiplyScalar(distance);
camera.updateMatrixWorld();
if (camera instanceof THREE.OrthographicCamera) {
// https://discourse.threejs.org/t/how-to-unproject-mouse2d-with-orthographic-camera/4777
vector.set(normalizedX, normalizedY, 0);
vector.unproject(camera);
// The unprojected point is on the camera.
// Find x and y for a given z along the camera direction line.
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
const distance = (worldZ - vector.z) / direction.z;
vector.x += distance * direction.x;
vector.y += distance * direction.y;
} else {
// https://stackoverflow.com/questions/13055214/mouse-canvas-x-y-to-three-js-world-x-y-z
vector.set(normalizedX, normalizedY, 0.5);
vector.unproject(camera);
// The unprojected point is on the frustum plane.
// Find x and y for a given z along the line between the camera and
// the one on the frustum.
vector.sub(camera.position).normalize();
const distance = (worldZ - camera.position.z) / vector.z;
vector.x = distance * vector.x + camera.position.x;
vector.y = distance * vector.y + camera.position.y;
}
// The plane z == worldZ may not be visible on the camera.
if (!Number.isFinite(vector.x) || !Number.isFinite(vector.y)) {
@@ -436,8 +494,8 @@ namespace gdjs {
return result;
}
result[0] = camera.position.x + vector.x;
result[1] = -(camera.position.y + vector.y);
result[0] = vector.x;
result[1] = -vector.y;
return result;
}

View File

@@ -340,12 +340,15 @@ namespace gdjs {
});
const baseTexture = texture.baseTexture;
baseTexture.on('loaded', () => {
this._loadedTextures.set(resource, texture);
applyTextureSettings(texture, resource);
resolve();
});
baseTexture
.on('loaded', () => {
this._loadedTextures.set(resource, texture);
applyTextureSettings(texture, resource);
resolve();
})
.on('error', (error) => {
reject(error);
});
});
} else {
// If the file has no extension, PIXI.assets.load cannot find

View File

@@ -152,6 +152,7 @@ namespace gdjs {
getGlobalResourceNames(data),
data.layouts
);
this._effectsManager = new gdjs.EffectsManager();
this._maxFPS = this._data.properties.maxFPS;
this._minFPS = this._data.properties.minFPS;
@@ -306,6 +307,24 @@ namespace gdjs {
return this._resourcesLoader.getModel3DManager();
}
/**
* Get the Spine manager of the game, used to load and construct spine skeletons from game
* resources.
* @return The Spine manager for the game
*/
getSpineManager(): gdjs.SpineManager | null {
return this._resourcesLoader.getSpineManager();
}
/**
* Get the Spine Atlas manager of the game, used to load atlases from game
* resources.
* @return The Spine Atlas manager for the game
*/
getSpineAtlasManager(): gdjs.SpineAtlasManager | null {
return this._resourcesLoader.getSpineAtlasManager();
}
/**
* Get the input manager of the game, storing mouse, keyboard
* and touches states.
@@ -1152,5 +1171,16 @@ namespace gdjs {
? mapping[embeddedResourceName]
: embeddedResourceName;
}
/**
* Returns the array of resources that are embedded to passed one.
* @param resourceName The name of resource to find embedded resources of.
* @returns The array of related resources names.
*/
getEmbeddedResourcesNames(resourceName: string): string[] {
return this._embeddedResourcesMappings.has(resourceName)
? Object.keys(this._embeddedResourcesMappings.get(resourceName)!)
: [];
}
}
}

View File

@@ -0,0 +1,4 @@
import * as pixi_spine from 'pixi-spine';
export = pixi_spine;
export as namespace pixi_spine;

View File

@@ -146,6 +146,7 @@ declare interface InstanceStringProperty {
declare interface LayerData {
name: string;
renderingType?: '' | '2d' | '3d' | '2d+3d';
cameraType?: 'perspective' | 'orthographic';
visibility: boolean;
cameras: CameraData[];
effects: EffectData[];
@@ -279,4 +280,6 @@ declare type ResourceKind =
| 'tilemap'
| 'tileset'
| 'bitmapFont'
| 'model3D';
| 'model3D'
| 'atlas'
| 'spine';

174
GDJS/package-lock.json generated
View File

@@ -19,6 +19,7 @@
"lebab": "^3.1.0",
"minimist": "^1.2.5",
"patch-package": "^6.4.7",
"pixi-spine": "4.0.4",
"pixi.js": "7.3.0",
"prettier": "2.1.2",
"recursive-readdir": "^2.2.2",
@@ -28,6 +29,91 @@
"typescript": "4.3.2"
}
},
"node_modules/@pixi-spine/base": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/base/-/base-4.0.3.tgz",
"integrity": "sha512-0bunaWebaDswLFtYZ6whV+ZvgLQ7oANcvbPmIOoVpS/1pOY3Y/GAnWOFbgp3qt9Q/ntLYqNjGve6xq0IqpsTAA==",
"dev": true,
"peerDependencies": {
"@pixi/core": "^7.0.0",
"@pixi/display": "^7.0.0",
"@pixi/graphics": "^7.0.0",
"@pixi/mesh": "^7.0.0",
"@pixi/mesh-extras": "^7.0.0",
"@pixi/sprite": "^7.0.0"
}
},
"node_modules/@pixi-spine/loader-base": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-base/-/loader-base-4.0.4.tgz",
"integrity": "sha512-Grgu+PxiUpgYWpuMRr3h5jrN3ZTnwyXfu3HuYdFb6mbJTTMub4xBPALeui+O+tw0k9RNRAr99pUzu9Rc9XTbAw==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/assets": " ^7.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi-spine/loader-uni": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-uni/-/loader-uni-4.0.3.tgz",
"integrity": "sha512-tfhTJrnuog8ObKbbiSG1wV/nIUc3O98WfwS6lCmewaupoMIKF0ujg21MCqXUXJvljQJzU9tbURI+DWu4w9dnnA==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi-spine/loader-base": "^4.0.0",
"@pixi-spine/runtime-3.7": "^4.0.0",
"@pixi-spine/runtime-3.8": "^4.0.0",
"@pixi-spine/runtime-4.1": "^4.0.0",
"@pixi/assets": " ^7.0.0",
"@pixi/core": "^7.0.0",
"@pixi/display": "^7.0.0",
"@pixi/graphics": "^7.0.0",
"@pixi/mesh": "^7.0.0",
"@pixi/mesh-extras": "^7.0.0",
"@pixi/sprite": "^7.0.0"
}
},
"node_modules/@pixi-spine/runtime-3.7": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.7/-/runtime-3.7-4.0.3.tgz",
"integrity": "sha512-zuopKtSqjRc37wjW5xJ64j9DbiBB7rkPMFeldeWBPCbfZiCcFcwSZwZnrcgC+f4HIGo0NeviAvJGM8Hcf3AyeA==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi-spine/runtime-3.8": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.8/-/runtime-3.8-4.0.3.tgz",
"integrity": "sha512-lIhb4jOTon+FVYLO9AIgcB6jf9hC+RLEn8PesaDRibDocQ1htVCkEIhCIU3Mc00fuqIby7lMBsINeS/Th0q3bw==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi-spine/runtime-4.0": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.0/-/runtime-4.0-4.0.3.tgz",
"integrity": "sha512-2Y8qhxRkg/yH/9VylGsRVAd5W+dXVPhHTjFk0RR9wEUzTCkdZ17pE+56s2nESi2X3sYNKkz8FowfaqIvXnVGxw==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi-spine/runtime-4.1": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.1/-/runtime-4.1-4.0.3.tgz",
"integrity": "sha512-jK433snCQMC4FUPiDgyIcxhiatvRNSxqgs0CgHjjQ0l8GlY6gPpkkdThQ6GsFNme1SUZ4uvnWwawXFIGjW1IpQ==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi/accessibility": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.3.0.tgz",
@@ -1599,6 +1685,30 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pixi-spine": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/pixi-spine/-/pixi-spine-4.0.4.tgz",
"integrity": "sha512-XRq1yARVoi4av7RXnd9+P37SWI9+e4/f5yTScZPJGB+sY5VcRYN6BYkBQ+y8nUKI1aJIjlms9z+pGxqikm+eFQ==",
"dev": true,
"dependencies": {
"@pixi-spine/base": "^4.0.3",
"@pixi-spine/loader-base": "^4.0.4",
"@pixi-spine/loader-uni": "^4.0.3",
"@pixi-spine/runtime-3.7": "^4.0.3",
"@pixi-spine/runtime-3.8": "^4.0.3",
"@pixi-spine/runtime-4.0": "^4.0.3",
"@pixi-spine/runtime-4.1": "^4.0.3"
},
"peerDependencies": {
"@pixi/assets": "^7.0.0",
"@pixi/core": "^7.0.0",
"@pixi/display": "^7.0.0",
"@pixi/graphics": "^7.0.0",
"@pixi/mesh": "^7.0.0",
"@pixi/mesh-extras": "^7.0.0",
"@pixi/sprite": "^7.0.0"
}
},
"node_modules/pixi.js": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.3.0.tgz",
@@ -1981,6 +2091,55 @@
}
},
"dependencies": {
"@pixi-spine/base": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/base/-/base-4.0.3.tgz",
"integrity": "sha512-0bunaWebaDswLFtYZ6whV+ZvgLQ7oANcvbPmIOoVpS/1pOY3Y/GAnWOFbgp3qt9Q/ntLYqNjGve6xq0IqpsTAA==",
"dev": true,
"requires": {}
},
"@pixi-spine/loader-base": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-base/-/loader-base-4.0.4.tgz",
"integrity": "sha512-Grgu+PxiUpgYWpuMRr3h5jrN3ZTnwyXfu3HuYdFb6mbJTTMub4xBPALeui+O+tw0k9RNRAr99pUzu9Rc9XTbAw==",
"dev": true,
"requires": {}
},
"@pixi-spine/loader-uni": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-uni/-/loader-uni-4.0.3.tgz",
"integrity": "sha512-tfhTJrnuog8ObKbbiSG1wV/nIUc3O98WfwS6lCmewaupoMIKF0ujg21MCqXUXJvljQJzU9tbURI+DWu4w9dnnA==",
"dev": true,
"requires": {}
},
"@pixi-spine/runtime-3.7": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.7/-/runtime-3.7-4.0.3.tgz",
"integrity": "sha512-zuopKtSqjRc37wjW5xJ64j9DbiBB7rkPMFeldeWBPCbfZiCcFcwSZwZnrcgC+f4HIGo0NeviAvJGM8Hcf3AyeA==",
"dev": true,
"requires": {}
},
"@pixi-spine/runtime-3.8": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.8/-/runtime-3.8-4.0.3.tgz",
"integrity": "sha512-lIhb4jOTon+FVYLO9AIgcB6jf9hC+RLEn8PesaDRibDocQ1htVCkEIhCIU3Mc00fuqIby7lMBsINeS/Th0q3bw==",
"dev": true,
"requires": {}
},
"@pixi-spine/runtime-4.0": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.0/-/runtime-4.0-4.0.3.tgz",
"integrity": "sha512-2Y8qhxRkg/yH/9VylGsRVAd5W+dXVPhHTjFk0RR9wEUzTCkdZ17pE+56s2nESi2X3sYNKkz8FowfaqIvXnVGxw==",
"dev": true,
"requires": {}
},
"@pixi-spine/runtime-4.1": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.1/-/runtime-4.1-4.0.3.tgz",
"integrity": "sha512-jK433snCQMC4FUPiDgyIcxhiatvRNSxqgs0CgHjjQ0l8GlY6gPpkkdThQ6GsFNme1SUZ4uvnWwawXFIGjW1IpQ==",
"dev": true,
"requires": {}
},
"@pixi/accessibility": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.3.0.tgz",
@@ -3203,6 +3362,21 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"pixi-spine": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/pixi-spine/-/pixi-spine-4.0.4.tgz",
"integrity": "sha512-XRq1yARVoi4av7RXnd9+P37SWI9+e4/f5yTScZPJGB+sY5VcRYN6BYkBQ+y8nUKI1aJIjlms9z+pGxqikm+eFQ==",
"dev": true,
"requires": {
"@pixi-spine/base": "^4.0.3",
"@pixi-spine/loader-base": "^4.0.4",
"@pixi-spine/loader-uni": "^4.0.3",
"@pixi-spine/runtime-3.7": "^4.0.3",
"@pixi-spine/runtime-3.8": "^4.0.3",
"@pixi-spine/runtime-4.0": "^4.0.3",
"@pixi-spine/runtime-4.1": "^4.0.3"
}
},
"pixi.js": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.3.0.tgz",

View File

@@ -16,6 +16,7 @@
"minimist": "^1.2.5",
"patch-package": "^6.4.7",
"pixi.js": "7.3.0",
"pixi-spine": "4.0.4",
"prettier": "2.1.2",
"recursive-readdir": "^2.2.2",
"shelljs": "^0.8.4",
@@ -23,8 +24,19 @@
"typedoc-plugin-reference-excluder": "^1.0.0",
"typescript": "4.3.2"
},
"overrides": {
"pixi-spine": {
"@pixi/assets": "7.3.0",
"@pixi/core": "7.3.0",
"@pixi/display": "7.3.0",
"@pixi/graphics": "7.3.0",
"@pixi/mesh": "7.3.0",
"@pixi/mesh-extras": "7.3.0",
"@pixi/sprite": "7.3.0"
}
},
"scripts": {
"postinstall": "patch-package",
"postinstall": "patch-package && node scripts/install-spine.js",
"check-types": "tsc",
"build": "node scripts/build.js",
"test": "cd tests && npm run test-benchmark",

View File

@@ -0,0 +1,36 @@
const path = require('path');
const shell = require('shelljs');
const readContent = (path, testErrorMessage) => {
if (!shell.test('-f', path)) throw new Error(`${testErrorMessage} Should exist by ${path}.`);
const readingResult = shell.cat(path);
if (readingResult.stderr) throw new Error(readingResult.stderr);
return readingResult.toString();
};
try {
shell.echo(`Start pixi-spine.js copying...`);
const originalSpineDir = path.resolve('node_modules/pixi-spine');
const originalSpinePackage = JSON.parse(readContent(path.join(originalSpineDir, 'package.json'), 'Cannot find pixi-spine package.json file.'));
const originalSpineContent = readContent(path.join(originalSpineDir, originalSpinePackage.extensionConfig.bundle), 'Cannot find pixi-spine.js.');
const varSpineExport = '\nvar pixi_spine = this.PIXI.spine;\n';
const runtimeSpineDir = '../Extensions/Spine/pixi-spine';
if (!shell.test('-d', runtimeSpineDir)) {
shell.echo(`Creating directory for pixi-spine.js ${runtimeSpineDir}.`);
shell.mkdir(runtimeSpineDir);
}
const runtimeSpinePath = path.join(runtimeSpineDir, 'pixi-spine.js');
new shell.ShellString(originalSpineContent + varSpineExport).to(runtimeSpinePath);
shell.echo(`✅ Properly copied pixi-spine.js from node_modules to ${runtimeSpinePath}.`);
} catch(error) {
shell.echo(`❌ Unable to copy pixi-spine.js from node_modules. Error is: ${error}`)
shell.exit(1);
}

View File

@@ -23,6 +23,7 @@ const transformExcludedExtensions = ['.min.js', '.d.ts'];
const untransformedPaths = [
// GDJS prebuilt files:
'GDJS/Runtime/pixi-renderers/pixi.js',
'GDJS/Runtime/pixi-renderers/pixi-spine.js',
'GDJS/Runtime/pixi-renderers/three.js',
'GDJS/Runtime/pixi-renderers/ThreeAddons.js',
'GDJS/Runtime/pixi-renderers/draco/gltf/draco_wasm_wrapper.js',

View File

@@ -0,0 +1,101 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 813, 160, 45, 45
eye-indifferent
bounds: 569, 2, 47, 45
eye-surprised
bounds: 643, 7, 47, 45
rotate: 90
front-bracer
bounds: 811, 51, 29, 40
front-fist-closed
bounds: 807, 93, 38, 41
front-fist-open
bounds: 815, 210, 43, 44
front-foot
bounds: 706, 64, 63, 35
rotate: 90
front-shin
bounds: 80, 11, 41, 92
front-thigh
bounds: 754, 12, 23, 56
front-upper-arm
bounds: 618, 5, 23, 49
goggles
bounds: 214, 20, 131, 83
gun
bounds: 347, 14, 105, 102
rotate: 90
head
bounds: 80, 105, 136, 149
hoverboard-board
bounds: 2, 8, 246, 76
rotate: 90
hoverboard-thruster
bounds: 478, 2, 30, 32
hoverglow-small
bounds: 218, 117, 137, 38
rotate: 90
mouth-grind
bounds: 775, 80, 47, 30
rotate: 90
mouth-oooo
bounds: 779, 31, 47, 30
rotate: 90
mouth-smile
bounds: 783, 207, 47, 30
rotate: 90
muzzle-glow
bounds: 779, 4, 25, 25
muzzle-ring
bounds: 451, 14, 25, 105
muzzle01
bounds: 664, 60, 67, 40
rotate: 90
muzzle02
bounds: 580, 56, 68, 42
rotate: 90
muzzle03
bounds: 478, 36, 83, 53
rotate: 90
muzzle04
bounds: 533, 49, 75, 45
rotate: 90
muzzle05
bounds: 624, 56, 68, 38
rotate: 90
neck
bounds: 806, 8, 18, 21
portal-bg
bounds: 258, 121, 133, 133
portal-flare1
bounds: 690, 2, 56, 30
rotate: 90
portal-flare2
bounds: 510, 3, 57, 31
portal-flare3
bounds: 722, 4, 58, 30
rotate: 90
portal-shade
bounds: 393, 121, 133, 133
portal-streaks1
bounds: 528, 126, 126, 128
portal-streaks2
bounds: 656, 129, 125, 125
rear-bracer
bounds: 826, 13, 28, 36
rear-foot
bounds: 743, 70, 57, 30
rotate: 90
rear-shin
bounds: 174, 14, 38, 89
rear-thigh
bounds: 783, 158, 28, 47
rear-upper-arm
bounds: 783, 136, 20, 44
rotate: 90
torso
bounds: 123, 13, 49, 90

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -41,14 +41,15 @@ module.exports = function (config) {
'./newIDE/app/resources/GDJS/Runtime/AsyncTasksManager.js',
'./newIDE/app/resources/GDJS/Runtime/libs/rbush.js',
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/pixi.js',
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/pixi-spine.js',
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/three.js',
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/*.js',
'./newIDE/app/resources/GDJS/Runtime/howler-sound-manager/howler.min.js',
'./newIDE/app/resources/GDJS/Runtime/howler-sound-manager/howler-sound-manager.js',
'./newIDE/app/resources/GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver.js',
'./newIDE/app/resources/GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver-font-manager.js',
'./newIDE/app/resources/GDJS/Runtime/jsonmanager.js',
'./newIDE/app/resources/GDJS/Runtime/Model3DManager.js',
'./newIDE/app/resources/GDJS/Runtime/jsonmanager.js',
'./newIDE/app/resources/GDJS/Runtime/ResourceLoader.js',
'./newIDE/app/resources/GDJS/Runtime/ResourceCache.js',
'./newIDE/app/resources/GDJS/Runtime/timemanager.js',
@@ -120,6 +121,10 @@ module.exports = function (config) {
'./newIDE/app/resources/GDJS/Runtime/Extensions/TextInput/textinputruntimeobject-pixi-renderer.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/TextObject/textruntimeobject.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/TextObject/textruntimeobject-pixi-renderer.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/A_RuntimeObject3D.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/A_RuntimeObject3DRenderer.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/Cube3DRuntimeObject.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/TopDownMovementBehavior/topdownmovementruntimebehavior.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/TweenBehavior/TweenManager.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/TweenBehavior/tweentools.js',

View File

@@ -909,6 +909,8 @@ interface Layer {
[Const, Ref] DOMString GetName();
void SetRenderingType([Const] DOMString renderingType);
[Const, Ref] DOMString GetRenderingType();
void SetCameraType([Const] DOMString cameraType);
[Const, Ref] DOMString GetCameraType();
void SetVisibility(boolean visible);
boolean GetVisibility();
void SetLocked(boolean isLocked);
@@ -957,9 +959,11 @@ interface PropertyDescriptor {
[Ref] PropertyDescriptor SetExtraInfo([Const, Ref] VectorString info);
[Ref] VectorString GetExtraInfo();
[Ref] PropertyDescriptor SetHidden(boolean enable);
boolean IsHidden();
[Ref] PropertyDescriptor SetDeprecated(boolean enable);
boolean IsDeprecated();
[Const, Ref] MeasurementUnit GetMeasurementUnit();
[Ref] PropertyDescriptor SetMeasurementUnit([Const, Ref] MeasurementUnit measurementUnit);
boolean IsHidden();
void SerializeTo([Ref] SerializerElement element);
void UnserializeFrom([Const, Ref] SerializerElement element);
@@ -1117,6 +1121,11 @@ interface JsonResource {
};
JsonResource implements Resource;
interface SpineResource {
void SpineResource();
};
SpineResource implements JsonResource;
interface TilemapResource {
void TilemapResource();
};
@@ -1132,6 +1141,11 @@ interface Model3DResource {
};
Model3DResource implements Resource;
interface AtlasResource {
void AtlasResource();
};
AtlasResource implements Resource;
interface InitialInstance {
void InitialInstance();
@@ -1297,6 +1311,12 @@ interface Serializer {
[Value] SerializerElement STATIC_FromJSON([Const] DOMString json);
};
interface ObjectAssetSerializer {
void STATIC_SerializeTo([Ref] Project project, [Const, Ref] gdObject obj,
[Const] DOMString objectFullName, [Ref] SerializerElement element,
[Ref] MapStringString resourcesNewFileNames);
};
interface InstructionsList {
void InstructionsList();
@@ -3028,6 +3048,7 @@ interface ObjectsUsingResourceCollector {
interface ResourcesInUseHelper {
void ResourcesInUseHelper([Ref] ResourcesManager resourcesManager);
[Const, Ref] VectorString GetAllResources();
[Ref] SetString GetAllImages();
[Ref] SetString GetAllAudios();
[Ref] SetString GetAllFonts();
@@ -3199,6 +3220,35 @@ interface Model3DObjectConfiguration {
};
Model3DObjectConfiguration implements ObjectConfiguration;
interface SpineAnimation {
void SpineAnimation();
void SetName([Const] DOMString name);
[Const, Ref] DOMString GetName();
void SetSource([Const] DOMString name);
[Const, Ref] DOMString GetSource();
void SetShouldLoop(boolean shouldLoop);
boolean ShouldLoop();
};
interface SpineObjectConfiguration {
void SpineObjectConfiguration();
void AddAnimation([Const, Ref] SpineAnimation animation);
[Ref] SpineAnimation GetAnimation(unsigned long index);
boolean HasAnimationNamed([Const] DOMString name);
unsigned long GetAnimationsCount();
void RemoveAnimation(unsigned long index);
void RemoveAllAnimations();
boolean HasNoAnimations();
void SwapAnimations(unsigned long first, unsigned long second);
void MoveAnimation(unsigned long oldIndex, unsigned long newIndex);
};
SpineObjectConfiguration implements ObjectConfiguration;
interface Vector2f {
void Vector2f();

View File

@@ -195,6 +195,10 @@ void ObjectJsImplementation::ExposeResources(gd::ArbitraryResourceWorker& worker
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

@@ -86,6 +86,7 @@
#include <GDCore/Project/VariablesContainersList.h>
#include <GDCore/Serialization/Serializer.h>
#include <GDCore/Serialization/SerializerElement.h>
#include <GDCore/IDE/ObjectAssetSerializer.h>
#include <GDJS/Events/Builtin/JsCodeEvent.h>
#include <GDJS/Events/CodeGeneration/BehaviorCodeGenerator.h>
#include <GDJS/Events/CodeGeneration/EventsFunctionsExtensionCodeGenerator.h>
@@ -109,6 +110,8 @@
#include "../../Extensions/TextEntryObject/TextEntryObject.h"
#include "../../Extensions/TextObject/TextObject.h"
#include "../../Extensions/TiledSpriteObject/TiledSpriteObject.h"
#include "../../Extensions/3D/Model3DObjectConfiguration.h"
#include "../../Extensions/Spine/SpineObjectConfiguration.h"
#include "BehaviorJsImplementation.h"
#include "BehaviorSharedDataJsImplementation.h"
#include "ObjectJsImplementation.h"
@@ -539,6 +542,7 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
#define STATIC_GetSafeName GetSafeName
#define STATIC_ToJSON ToJSON
#define STATIC_FromJSON(x) FromJSON(x)
#define STATIC_SerializeTo SerializeTo
#define STATIC_IsObject IsObject
#define STATIC_IsBehavior IsBehavior
#define STATIC_IsExpression IsExpression
@@ -590,6 +594,7 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
#define STATIC_ExposeProjectEvents ExposeProjectEvents
#define STATIC_ExposeProjectObjects ExposeProjectObjects
#define STATIC_ExposeWholeProjectResources ExposeWholeProjectResources
#define STATIC_GetResourceTypes GetResourceTypes
#define STATIC_GetBehaviorMetadata GetBehaviorMetadata
#define STATIC_GetObjectMetadata GetObjectMetadata
@@ -754,6 +759,7 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
#define STATIC_ShiftSentenceParamIndexes ShiftSentenceParamIndexes
#define STATIC_CopyAllResourcesTo CopyAllResourcesTo
#define STATIC_CopyObjectResourcesTo CopyObjectResourcesTo
#define STATIC_IsExtensionLifecycleEventsFunction \
IsExtensionLifecycleEventsFunction

View File

@@ -150,6 +150,9 @@ var adaptNamingConventions = function (gd) {
gd.asModel3DConfiguration = function (evt) {
return gd.castObject(evt, gd.Model3DObjectConfiguration);
};
gd.asSpineConfiguration = function (evt) {
return gd.castObject(evt, gd.SpineObjectConfiguration);
};
gd.asImageResource = function (evt) {
return gd.castObject(evt, gd.ImageResource);

View File

@@ -119,3 +119,4 @@ target_link_libraries(GD PathfindingBehavior)
target_link_libraries(GD PhysicsBehavior)
target_link_libraries(GD ParticleSystem)
target_link_libraries(GD Scene3D)
target_link_libraries(GD SpineObject)

View File

@@ -13,7 +13,7 @@ module.exports = function (grunt) {
let cmakeBinary = 'emcmake cmake';
let cmakeGeneratorArgs = [];
let makeBinary = 'emmake make';
let makeArgs = ['-j 4'];
let makeArgs = ['-j 8'];
// Use more specific paths on Windows
if (isWin) {

View File

@@ -1,5 +1,4 @@
const shell = require('shelljs');
const fs = require('fs');
const path = require('path');
const sourcePath = path.join(__dirname, '../../Binaries/embuild/GDevelop.js');

View File

@@ -199,6 +199,7 @@ type ParticleEmitterObject_RendererType = 0 | 1 | 2`
` asObjectJsImplementation(gdObjectConfiguration): gdObjectJsImplementation;`,
` asCustomObjectConfiguration(gdObjectConfiguration): gdCustomObjectConfiguration;`,
` asModel3DConfiguration(gdObjectConfiguration): gdModel3DObjectConfiguration;`,
` asSpineConfiguration(gdObjectConfiguration): gdSpineObjectConfiguration;`,
'',
` asImageResource(gdResource): gdImageResource;`,
'',
@@ -307,6 +308,12 @@ type ParticleEmitterObject_RendererType = 0 | 1 | 2`
'declare class gdGroupEvent extends gdBaseEvent {',
'types/gdgroupevent.js'
);
shell.sed(
'-i',
'declare class gdAbstractFileSystemJS {',
'declare class gdAbstractFileSystemJS extends gdAbstractFileSystem {',
'types/gdabstractfilesystemjs.js'
);
[
'BaseEvent',
'StandardEvent',
@@ -338,13 +345,13 @@ type ParticleEmitterObject_RendererType = 0 | 1 | 2`
shell.sed(
'-i',
/setKind\(kind: string\): void/,
"setKind(kind: 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D'): void",
"setKind(kind: 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D' | 'atlas' | 'spine'): void",
'types/gdresource.js'
);
shell.sed(
'-i',
/getKind\(\): string/,
"getKind(): 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D'",
"getKind(): 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D' | 'atlas' | 'spine'",
'types/gdresource.js'
);

View File

@@ -796,6 +796,8 @@ export class Layer extends EmscriptenObject {
getName(): string;
setRenderingType(renderingType: string): void;
getRenderingType(): string;
setCameraType(cameraType: string): void;
getCameraType(): string;
setVisibility(visible: boolean): void;
getVisibility(): boolean;
setLocked(isLocked: boolean): void;
@@ -837,9 +839,11 @@ export class PropertyDescriptor extends EmscriptenObject {
setExtraInfo(info: VectorString): PropertyDescriptor;
getExtraInfo(): VectorString;
setHidden(enable: boolean): PropertyDescriptor;
isHidden(): boolean;
setDeprecated(enable: boolean): PropertyDescriptor;
isDeprecated(): boolean;
getMeasurementUnit(): MeasurementUnit;
setMeasurementUnit(measurementUnit: MeasurementUnit): PropertyDescriptor;
isHidden(): boolean;
serializeTo(element: SerializerElement): void;
unserializeFrom(element: SerializerElement): void;
serializeValuesTo(element: SerializerElement): void;
@@ -974,6 +978,10 @@ export class JsonResource extends EmscriptenObject {
constructor(): void;
}
export class SpineResource extends EmscriptenObject {
constructor(): void;
}
export class TilemapResource extends EmscriptenObject {
constructor(): void;
}
@@ -982,6 +990,10 @@ export class TilesetResource extends EmscriptenObject {
constructor(): void;
}
export class AtlasResource extends EmscriptenObject {
constructor(): void;
}
export class InitialInstance extends EmscriptenObject {
constructor(): void;
setObjectName(name: string): void;
@@ -1114,6 +1126,10 @@ export class Serializer extends EmscriptenObject {
static fromJSON(json: string): SerializerElement;
}
export class ObjectAssetSerializer extends EmscriptenObject {
static serializeTo(project: Project, obj: gdObject, objectFullName: string, element: SerializerElement, resourcesNewFileNames: MapStringString): void;
}
export class InstructionsList extends EmscriptenObject {
constructor(): void;
insert(instr: Instruction, pos: number): Instruction;
@@ -2322,6 +2338,7 @@ export class ObjectsUsingResourceCollector extends EmscriptenObject {
export class ResourcesInUseHelper extends EmscriptenObject {
constructor(resourcesManager: ResourcesManager): void;
getAllResources(): VectorString;
getAllImages(): SetString;
getAllAudios(): SetString;
getAllFonts(): SetString;
@@ -2422,6 +2439,29 @@ export class SpriteObject extends EmscriptenObject {
setAdaptCollisionMaskAutomatically(adaptCollisionMaskAutomatically: boolean): void;
}
export class SpineAnimation extends EmscriptenObject {
constructor(): void;
setName(name: string): void;
getName(): string;
setSource(name: string): void;
getSource(): string;
setShouldLoop(shouldLoop: boolean): void;
shouldLoop(): boolean;
}
export class SpineObjectConfiguration extends EmscriptenObject {
constructor(): void;
addAnimation(animation: SpineAnimation): void;
getAnimation(index: number): SpineAnimation;
hasAnimationNamed(name: string): boolean;
getAnimationsCount(): number;
removeAnimation(index: number): void;
removeAllAnimations(): void;
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
}
export class TextObject extends EmscriptenObject {
constructor(): void;
setString(string: string): void;

View File

@@ -1,5 +1,5 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdAbstractFileSystemJS {
declare class gdAbstractFileSystemJS extends gdAbstractFileSystem {
constructor(): void;
mkDir(dir: string): void;
dirExists(dir: string): void;

View File

@@ -0,0 +1,6 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdAtlasResource extends gdResource {
constructor(): void;
delete(): void;
ptr: number;
};

View File

@@ -5,6 +5,8 @@ declare class gdLayer {
getName(): string;
setRenderingType(renderingType: string): void;
getRenderingType(): string;
setCameraType(cameraType: string): void;
getCameraType(): string;
setVisibility(visible: boolean): void;
getVisibility(): boolean;
setLocked(isLocked: boolean): void;

View File

@@ -0,0 +1,6 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdObjectAssetSerializer {
static serializeTo(project: gdProject, obj: gdObject, objectFullName: string, element: gdSerializerElement, resourcesNewFileNames: gdMapStringString): void;
delete(): void;
ptr: number;
};

View File

@@ -15,9 +15,11 @@ declare class gdPropertyDescriptor {
setExtraInfo(info: gdVectorString): gdPropertyDescriptor;
getExtraInfo(): gdVectorString;
setHidden(enable: boolean): gdPropertyDescriptor;
isHidden(): boolean;
setDeprecated(enable: boolean): gdPropertyDescriptor;
isDeprecated(): boolean;
getMeasurementUnit(): gdMeasurementUnit;
setMeasurementUnit(measurementUnit: gdMeasurementUnit): gdPropertyDescriptor;
isHidden(): boolean;
serializeTo(element: gdSerializerElement): void;
unserializeFrom(element: gdSerializerElement): void;
serializeValuesTo(element: gdSerializerElement): void;

View File

@@ -4,8 +4,8 @@ declare class gdResource {
clone(): gdResource;
setName(name: string): void;
getName(): string;
setKind(kind: 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D'): void;
getKind(): 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D';
setKind(kind: 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D' | 'atlas' | 'spine'): void;
getKind(): 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D' | 'atlas' | 'spine';
isUserAdded(): boolean;
setUserAdded(yes: boolean): void;
useFile(): boolean;

View File

@@ -1,6 +1,7 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdResourcesInUseHelper extends gdArbitraryResourceWorker {
constructor(resourcesManager: gdResourcesManager): void;
getAllResources(): gdVectorString;
getAllImages(): gdSetString;
getAllAudios(): gdSetString;
getAllFonts(): gdSetString;

View File

@@ -0,0 +1,12 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdSpineAnimation {
constructor(): void;
setName(name: string): void;
getName(): string;
setSource(name: string): void;
getSource(): string;
setShouldLoop(shouldLoop: boolean): void;
shouldLoop(): boolean;
delete(): void;
ptr: number;
};

View File

@@ -0,0 +1,15 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdSpineObjectConfiguration extends gdObjectConfiguration {
constructor(): void;
addAnimation(animation: gdSpineAnimation): void;
getAnimation(index: number): gdSpineAnimation;
hasAnimationNamed(name: string): boolean;
getAnimationsCount(): number;
removeAnimation(index: number): void;
removeAllAnimations(): void;
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
delete(): void;
ptr: number;
};

View File

@@ -0,0 +1,6 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdSpineResource extends gdJsonResource {
constructor(): void;
delete(): void;
ptr: number;
};

View File

@@ -36,6 +36,7 @@ declare class libGDevelop {
asObjectJsImplementation(gdObjectConfiguration): gdObjectJsImplementation;
asCustomObjectConfiguration(gdObjectConfiguration): gdCustomObjectConfiguration;
asModel3DConfiguration(gdObjectConfiguration): gdModel3DObjectConfiguration;
asSpineConfiguration(gdObjectConfiguration): gdSpineObjectConfiguration;
asImageResource(gdResource): gdImageResource;
@@ -109,9 +110,11 @@ declare class libGDevelop {
BitmapFontResource: Class<gdBitmapFontResource>;
VideoResource: Class<gdVideoResource>;
JsonResource: Class<gdJsonResource>;
SpineResource: Class<gdSpineResource>;
TilemapResource: Class<gdTilemapResource>;
TilesetResource: Class<gdTilesetResource>;
Model3DResource: Class<gdModel3DResource>;
AtlasResource: Class<gdAtlasResource>;
InitialInstance: Class<gdInitialInstance>;
InitialInstancesContainer: Class<gdInitialInstancesContainer>;
HighestZOrderFinder: Class<gdHighestZOrderFinder>;
@@ -122,6 +125,7 @@ declare class libGDevelop {
SerializerElement: Class<gdSerializerElement>;
SharedPtrSerializerElement: Class<gdSharedPtrSerializerElement>;
Serializer: Class<gdSerializer>;
ObjectAssetSerializer: Class<gdObjectAssetSerializer>;
InstructionsList: Class<gdInstructionsList>;
Instruction: Class<gdInstruction>;
Expression: Class<gdExpression>;
@@ -230,6 +234,8 @@ declare class libGDevelop {
SpriteObject: Class<gdSpriteObject>;
Model3DAnimation: Class<gdModel3DAnimation>;
Model3DObjectConfiguration: Class<gdModel3DObjectConfiguration>;
SpineAnimation: Class<gdSpineAnimation>;
SpineObjectConfiguration: Class<gdSpineObjectConfiguration>;
Vector2f: Class<gdVector2f>;
VectorVector2f: Class<gdVectorVector2f>;
TextObject: Class<gdTextObject>;

View File

@@ -16,7 +16,47 @@ skip_tags: true # Don't rebuild on tags.
init:
- ps: Install-Product node 16
- cmd: set NODE_OPTIONS=--max-old-space-size=8192
cache:
- '%APPDATA%\npm-cache' # npm cache
- newIDE\app\node_modules -> newIDE\app\package-lock.json
- newIDE\electron-app\node_modules -> newIDE\electron-app\package-lock.json
- GDevelop.js\node_modules -> GDevelop.js\package-lock.json
install:
# Download and install SSL.com eSigner CKA.
# See https://www.ssl.com/how-to/how-to-integrate-esigner-cka-with-ci-cd-tools-for-automated-code-signing/.
#
# This is necessary because of "signing to be FIPS-140 compliant". See
# https://github.com/electron-userland/electron-builder/issues/6158
#
# Make sure to DISABLE "malware blocker" in SSL.com to avoid errors like:
# Error information: "Error: SignerSign() failed." (-2146893821/0x80090003)
- ps: >-
# Download and Unzip eSignerCKA Setup
Set-StrictMode -Version 'Latest'
Invoke-WebRequest -OutFile eSigner_CKA_Setup.zip "https://github.com/SSLcom/eSignerCKA/releases/download/v1.0.6/SSL.COM-eSigner-CKA_1.0.6.zip"
Expand-Archive -Force eSigner_CKA_Setup.zip
Remove-Item eSigner_CKA_Setup.zip
Move-Item -Destination "eSigner_CKA_Installer.exe" -Path "eSigner_CKA_*\*.exe"
# Install it. See https://www.ssl.com/how-to/how-to-integrate-esigner-cka-with-ci-cd-tools-for-automated-code-signing/
New-Item -ItemType Directory -Force -Path "C:\projects\gdevelop\eSignerCKA"
./eSigner_CKA_Installer.exe /CURRENTUSER /VERYSILENT /SUPPRESSMSGBOXES /DIR="C:\projects\gdevelop\eSignerCKA" | Out-Null
# Disable logger.
# $LogConfig = Get-Content -Path C:\projects\gdevelop\eSignerCKA/log4net.config
# $LogConfig[0] = '<log4net threshold="OFF">'
# $LogConfig | Set-Content -Path C:\projects\gdevelop\eSignerCKA/log4net.config
# Build GDevelop.js (and run tests to ensure it works).
# (in a subshell to avoid Emscripten polluting the Node.js and npm version for the rest of the build)
- cmd: >-
@@ -39,7 +79,7 @@ install:
# setuptools will make distutils available again (but we should migrate our packages probably).
- cmd: >-
pip install setuptools
cd newIDE\app
npm -v && npm install
@@ -50,21 +90,54 @@ install:
cd ..\..
# Package the app for Windows (and sign it with the certificate set in environment variables).
# Package the app for Windows (and sign it).
# Don't sign the appx (it will be signed by the Microsoft Store).
build_script:
- ps: >-
cd newIDE\electron-app
# Prepare certificate. See https://www.ssl.com/how-to/automate-ev-code-signing-with-signtool-or-certutil-esigner/?_gl=1*vuybcy*_gcl_au*MTEwODg1NDM2Mi4xNzA1ODU1NjM4#automated-code-signing
C:\projects\gdevelop\eSignerCKA/eSignerCKATool.exe config -mode product -user "$Env:ESIGNER_USER_NAME" -pass "$Env:ESIGNER_USER_PASSWORD" -totp "$Env:ESIGNER_USER_TOTP" -key "C:\projects\gdevelop\eSignerCKA\master.key" -r
C:\projects\gdevelop\eSignerCKA/eSignerCKATool.exe unload
C:\projects\gdevelop\eSignerCKA/eSignerCKATool.exe load
# Find certificate so we can tell electron-builder which one to use.
$CodeSigningCert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
echo Certificate: $CodeSigningCert
# Use a custom signtool path because of the signtool.exe bundled withy electron-builder not working for some reason.
# Can also be found in versioned folders like "C:/Program Files (x86)/Windows Kits/10/bin/10.0.22000.0/x86/signtool.exe".
$Env:SIGNTOOL_PATH = "C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe"
# Extract thumbprint and subject name of the certificate (will be passed to electron-builder).
$Env:GD_SIGNTOOL_THUMBPRINT = $CodeSigningCert.Thumbprint
$Env:GD_SIGNTOOL_SUBJECT_NAME = ($CodeSigningCert.Subject -replace ", ?", "`n" | ConvertFrom-StringData).CN
# Build the nsis installer (signed: electron-builder will use SignTool.exe with the certificate)
node scripts/build.js --win nsis --publish=never
Remove-Item -Path Env:CSC_LINK ; Remove-Item -Path Env:CSC_KEY_PASSWORD ; node scripts/build.js --skip-app-build --win appx --publish=never
# Build the appx (not signed).
$Env:GD_SIGNTOOL_THUMBPRINT = ''
$Env:GD_SIGNTOOL_SUBJECT_NAME = ''
node scripts/build.js --skip-app-build --win appx --publish=never
cd ..\..
# Clean dist folder to keep only installers/binaries.
- cmd: >-
DEL /F/Q/S newIDE\electron-app\dist\win-unpacked
rmdir /s /q newIDE\electron-app\dist\win-unpacked
# Run a few tests on Windows.
test_script:

View File

@@ -1,6 +1,7 @@
// @flow
import GDevelopJsInitializerDecorator from '../src/stories/GDevelopJsInitializerDecorator';
import i18nProviderDecorator from '../src/stories/I18nProviderDecorator';
import BrowserDropDownMenuDisablerDecorator from '../src/stories/BrowserDropDownMenuDisablerDecorator';
import '../src/UI/icomoon-font.css'; // Styles for Icomoon font.
import './app-level-styling.css';
@@ -35,5 +36,6 @@ export const parameters = {
export const decorators = [
GDevelopJsInitializerDecorator,
i18nProviderDecorator
i18nProviderDecorator,
BrowserDropDownMenuDisablerDecorator
]

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