Compare commits

...

12 Commits

Author SHA1 Message Date
AlexandreSi
96d33a7ab8 Add query param to lobbies iframe url 2024-06-14 16:51:26 +02:00
AlexandreSi
15cb723af9 Add nativeMobileApp attribute tu PreviewExportOptions 2024-06-14 16:41:35 +02:00
AlexandreSi
f0899e45e7 Review changes 2024-06-14 15:35:25 +02:00
AlexandreSi
1be148d5ee Add multiplayer configurator 2024-06-14 15:25:10 +02:00
AlexandreSi
a383c1e896 Rename file 2024-06-14 11:04:03 +02:00
D8H
dc87f138a6 Fix expression validation for variables with brackets (#6652) 2024-06-13 18:38:35 +02:00
Aurélien Vivet
28af860aff Add a new guided lesson: Top-down movement for pixel perfect game (#6650) 2024-06-13 16:12:50 +02:00
AlexandreS
4dbc607314 Support boolean expected value in in-app tutorial (#6654)
Only show in developer changelog
2024-06-13 14:10:02 +02:00
Clément Pasteau
bfefe9baf4 Synchronize 3D objects properties properly over network (#6649) 2024-06-11 12:31:36 +02:00
Florian Rival
2332359c19 Fetch in-app tutorial descriptions/titles instead of hardcoding them (#6647)
Only show in developer changelog
2024-06-10 15:55:27 +02:00
D8H
68b6c443c3 Fix variables editor shortcut from events tabs (#6646) 2024-06-10 15:36:27 +02:00
D8H
37e113baaa Fix a regression on the expression autocompletion layout (#6645)
Don't show in changelog
2024-06-10 10:42:21 +02:00
47 changed files with 1111 additions and 590 deletions

View File

@@ -293,12 +293,14 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
forbidsUsageOfBracketsBecauseParentIsObject = false;
Type currentParentType = parentType;
Type currentChildType = childType;
parentType = Type::NumberOrString;
auto parentParameterExtraInfo = currentParameterExtraInfo;
currentParameterExtraInfo = nullptr;
node.expression->Visit(*this);
currentParameterExtraInfo = parentParameterExtraInfo;
parentType = currentParentType;
childType = currentChildType;
if (node.child) {
node.child->Visit(*this);

View File

@@ -293,6 +293,14 @@ void SetupProjectWithDummyPlatform(gd::Project& project,
.AddCodeOnlyParameter("trueorfalse", "")
.SetFunctionName("setBooleanVariable");
extension->AddStrExpression("ToString",
"",
"",
"",
"")
.AddParameter("number", "")
.SetFunctionName("toString");
platform.AddExtension(extension);
}

View File

@@ -846,6 +846,73 @@ TEST_CASE("ExpressionCodeGenerator", "[common][events]") {
REQUIRE(expressionCodeGenerator.GetOutput() == "getAnyVariable(MySceneStructureVariable).getChild(\"\" + getAnyVariable(MySceneVariable).getAsString()).getAsNumber() + 1");
}
}
SECTION("Variable with bracket accessor and operators") {
{
auto node = parser.ParseExpression(
"MySceneStructureVariable[\"MyUndeclaredChild\"] * 2");
gd::ExpressionCodeGenerator expressionCodeGenerator(
"number", "", codeGenerator, context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() ==
"getAnyVariable(MySceneStructureVariable).getChild("
"\"MyUndeclaredChild\").getAsNumber() * 2");
}
{
auto node =
parser.ParseExpression("MySceneStructureVariable[\"MyUndeclaredChild\"] / 2");
gd::ExpressionCodeGenerator expressionCodeGenerator(
"number", "", codeGenerator, context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() ==
"getAnyVariable(MySceneStructureVariable).getChild("
"\"MyUndeclaredChild\").getAsNumber() / 2");
}
{
auto node =
parser.ParseExpression("MySceneStructureVariable[\"MyUndeclaredChild\"] - 2");
gd::ExpressionCodeGenerator expressionCodeGenerator(
"number", "", codeGenerator, context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() ==
"getAnyVariable(MySceneStructureVariable).getChild("
"\"MyUndeclaredChild\").getAsNumber() - 2");
}
{
auto node = parser.ParseExpression(
"MySceneStructureVariable["
"MySceneStructureVariable2[\"MyUndeclaredChild\"]] * 2");
gd::ExpressionCodeGenerator expressionCodeGenerator(
"number", "", codeGenerator, context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() ==
"getAnyVariable(MySceneStructureVariable).getChild("
"getAnyVariable(MySceneStructureVariable2).getChild("
"\"MyUndeclaredChild\").getAsNumberOrString()).getAsNumber() * 2");
}
{
auto node = parser.ParseExpression(
"MySceneStructureVariable[\"Child\" + "
"ToString(MySceneStructureVariable2[\"MyUndeclaredChild\"] * 2)] * 2");
gd::ExpressionCodeGenerator expressionCodeGenerator(
"number", "", codeGenerator, context);
REQUIRE(node);
node->Visit(expressionCodeGenerator);
REQUIRE(expressionCodeGenerator.GetOutput() ==
"getAnyVariable(MySceneStructureVariable).getChild("
"\"Child\" + "
"toString(getAnyVariable(MySceneStructureVariable2).getChild("
"\"MyUndeclaredChild\").getAsNumber() * 2)).getAsNumber() * 2");
}
}
SECTION("Object variable with non existing object (invalid)") {
auto node =
parser.ParseExpression("MyNonExistingSpriteObject.MyVariable");

View File

@@ -1521,6 +1521,54 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
}
}
SECTION("Variable with bracket accessor and operators") {
{
auto node =
parser.ParseExpression("MySceneStructureVariable[\"MyUndeclaredChild\"] * 2");
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
{
auto node =
parser.ParseExpression("MySceneStructureVariable[\"MyUndeclaredChild\"] / 2");
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
{
auto node =
parser.ParseExpression("MySceneStructureVariable[\"MyUndeclaredChild\"] - 2");
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
{
auto node = parser.ParseExpression(
"MySceneStructureVariable["
"MySceneStructureVariable2[\"MyUndeclaredChild\"]] * 2");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
{
// TODO `ToString` should not be required here.
auto node = parser.ParseExpression(
"MySceneStructureVariable[\"Child\" + "
"ToString(MySceneStructureVariable2[\"MyUndeclaredChild\"] * 2)] * 2");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
}
}
SECTION("Invalid scene variables (1 level, variable does not exist)") {
{
auto node =

View File

@@ -13,6 +13,23 @@ namespace gdjs {
const getValidDimensionValue = (value: float | undefined) =>
value === undefined ? 100 : value <= 0 ? 1 : value;
type Object3DNetworkSyncDataType = {
// z is position on the Z axis, different from zo, which is Z order
z: number;
w: number;
h: number;
d: number;
rx: number;
ry: number;
// no need for rz, as it is the angle from gdjs.RuntimeObject
flipX: boolean;
flipY: boolean;
flipZ: boolean;
};
export type Object3DNetworkSyncData = ObjectNetworkSyncData &
Object3DNetworkSyncDataType;
/**
* Base class for 3D objects.
*/
@@ -109,6 +126,39 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): Object3DNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
z: this.getZ(),
w: this.getWidth(),
h: this.getHeight(),
d: this.getDepth(),
rx: this.getRotationX(),
ry: this.getRotationY(),
flipX: this.isFlippedX(),
flipY: this.isFlippedY(),
flipZ: this.isFlippedZ(),
};
}
updateFromObjectNetworkSyncData(networkSyncData: Object3DNetworkSyncData) {
super.updateFromObjectNetworkSyncData(networkSyncData);
if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z);
if (networkSyncData.w !== undefined) this.setWidth(networkSyncData.w);
if (networkSyncData.h !== undefined) this.setHeight(networkSyncData.h);
if (networkSyncData.d !== undefined) this.setDepth(networkSyncData.d);
if (networkSyncData.rx !== undefined)
this.setRotationX(networkSyncData.rx);
if (networkSyncData.ry !== undefined)
this.setRotationY(networkSyncData.ry);
if (networkSyncData.flipX !== undefined)
this.flipX(networkSyncData.flipX);
if (networkSyncData.flipY !== undefined)
this.flipY(networkSyncData.flipY);
if (networkSyncData.flipZ !== undefined)
this.flipZ(networkSyncData.flipZ);
}
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);

View File

@@ -39,12 +39,6 @@ namespace gdjs {
};
type Cube3DObjectNetworkSyncDataType = {
w: number;
h: number;
d: number;
rx: number;
ry: number;
rz: number;
fo: 'Y' | 'Z';
bfu: 'X' | 'Y';
vfb: integer;
@@ -53,7 +47,7 @@ namespace gdjs {
mt: number;
};
type Cube3DObjectNetworkSyncData = ObjectNetworkSyncData &
type Cube3DObjectNetworkSyncData = Object3DNetworkSyncData &
Cube3DObjectNetworkSyncDataType;
/**
@@ -423,12 +417,6 @@ namespace gdjs {
getObjectNetworkSyncData(): Cube3DObjectNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
w: this.getWidth(),
h: this.getHeight(),
d: this.getDepth(),
rx: this.getRotationX(),
ry: this.getRotationY(),
rz: this.getAngle(),
mt: this._materialType,
fo: this._facesOrientation,
bfu: this._backFaceUpThroughWhichAxisRotation,
@@ -443,24 +431,6 @@ namespace gdjs {
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
if (networkSyncData.w !== undefined) {
this.setWidth(networkSyncData.w);
}
if (networkSyncData.h !== undefined) {
this.setHeight(networkSyncData.h);
}
if (networkSyncData.d !== undefined) {
this.setDepth(networkSyncData.d);
}
if (networkSyncData.rx !== undefined) {
this.setRotationX(networkSyncData.rx);
}
if (networkSyncData.ry !== undefined) {
this.setRotationY(networkSyncData.ry);
}
if (networkSyncData.rz !== undefined) {
this.setAngle(networkSyncData.rz);
}
if (networkSyncData.mt !== undefined) {
this._materialType = networkSyncData.mt;
}

View File

@@ -2,12 +2,6 @@ namespace gdjs {
type Model3DAnimation = { name: string; source: string; loop: boolean };
type Model3DObjectNetworkSyncDataType = {
w: float;
h: float;
d: float;
rx: number;
ry: number;
rz: number;
mt: number;
op: FloatPoint3D | null;
cp: FloatPoint3D | null;
@@ -17,7 +11,7 @@ namespace gdjs {
ap: boolean;
};
type Model3DObjectNetworkSyncData = ObjectNetworkSyncData &
type Model3DObjectNetworkSyncData = Object3DNetworkSyncData &
Model3DObjectNetworkSyncDataType;
/** Base parameters for {@link gdjs.Cube3DRuntimeObject} */
@@ -184,12 +178,6 @@ namespace gdjs {
getObjectNetworkSyncData(): Model3DObjectNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
w: this.getWidth(),
h: this.getHeight(),
d: this.getDepth(),
rx: this.getRotationX(),
ry: this.getRotationY(),
rz: this.getAngle(),
mt: this._materialType,
op: this._originPoint,
cp: this._centerPoint,
@@ -205,24 +193,6 @@ namespace gdjs {
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
if (networkSyncData.w !== undefined) {
this.setWidth(networkSyncData.w);
}
if (networkSyncData.h !== undefined) {
this.setHeight(networkSyncData.h);
}
if (networkSyncData.d !== undefined) {
this.setDepth(networkSyncData.d);
}
if (networkSyncData.rx !== undefined) {
this.setRotationX(networkSyncData.rx);
}
if (networkSyncData.ry !== undefined) {
this.setRotationY(networkSyncData.ry);
}
if (networkSyncData.rz !== undefined) {
this.setAngle(networkSyncData.rz);
}
if (networkSyncData.mt !== undefined) {
this._materialType = networkSyncData.mt;
}

View File

@@ -287,6 +287,7 @@ namespace gdjs {
x: objectNetworkSyncData.x,
y: objectNetworkSyncData.y,
z: objectNetworkSyncData.z,
zo: objectNetworkSyncData.zo,
a: objectNetworkSyncData.a,
hid: objectNetworkSyncData.hid,
if: objectNetworkSyncData.if,
@@ -364,7 +365,7 @@ namespace gdjs {
this._lastSentBasicObjectSyncData = {
x: objectNetworkSyncData.x,
y: objectNetworkSyncData.y,
z: objectNetworkSyncData.z,
zo: objectNetworkSyncData.zo,
a: objectNetworkSyncData.a,
hid: objectNetworkSyncData.hid,
if: objectNetworkSyncData.if,

View File

@@ -104,6 +104,9 @@ namespace gdjs {
'gameVersion',
runtimeGame.getGameData().properties.version
);
if (runtimeGame.getAdditionalOptions().nativeMobileApp) {
url.searchParams.set('nativeMobileApp', 'true');
}
url.searchParams.set(
'isPreview',
runtimeGame.isPreview() ? 'true' : 'false'

View File

@@ -222,6 +222,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
}
runtimeGameOptions.AddChild("projectDataOnlyExport")
.SetBoolValue(options.projectDataOnlyExport);
runtimeGameOptions.AddChild("nativeMobileApp")
.SetBoolValue(options.nativeMobileApp);
runtimeGameOptions.AddChild("websocketDebuggerServerAddress")
.SetStringValue(options.websocketDebuggerServerAddress);
runtimeGameOptions.AddChild("websocketDebuggerServerPort")

View File

@@ -37,6 +37,7 @@ struct PreviewExportOptions {
: project(project_),
exportPath(exportPath_),
useWindowMessageDebuggerClient(false),
nativeMobileApp(false),
projectDataOnlyExport(false),
fullLoadingScreen(false),
isDevelopmentEnvironment(false),
@@ -76,6 +77,15 @@ struct PreviewExportOptions {
return *this;
}
/**
* \brief Set that the preview is launched from a GDevelop native mobile app
* (iOS or Android).
*/
PreviewExportOptions &SetNativeMobileApp(bool enable) {
nativeMobileApp = enable;
return *this;
}
/**
* \brief Set the layout to be run first in the previewed game
*/
@@ -186,6 +196,7 @@ struct PreviewExportOptions {
gd::String externalLayoutName;
gd::String fallbackAuthorUsername;
gd::String fallbackAuthorId;
bool nativeMobileApp;
std::map<gd::String, int> includeFileHashes;
bool projectDataOnlyExport;
bool fullLoadingScreen;

View File

@@ -32,6 +32,8 @@ namespace gdjs {
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
/** if true, export is a partial preview without events. */
projectDataOnlyExport?: boolean;
/** if true, preview is launched from GDevelop native mobile app. */
nativeMobileApp?: boolean;
/** The address of the debugger server, to reach out using WebSocket. */
websocketDebuggerServerAddress?: string;
/** The port of the debugger server, to reach out using WebSocket. */
@@ -267,7 +269,7 @@ namespace gdjs {
* Return the additional options passed to the RuntimeGame when created.
* @returns The additional options, if any.
*/
getAdditionalOptions(): RuntimeGameOptions | null {
getAdditionalOptions(): RuntimeGameOptions {
return this._options;
}

View File

@@ -482,7 +482,7 @@ namespace gdjs {
return {
x: this.x,
y: this.y,
z: this.zOrder,
zo: this.zOrder,
a: this.angle,
hid: this.hidden,
if: this._instantForces.map((force) => force.getNetworkSyncData()),
@@ -509,8 +509,8 @@ namespace gdjs {
if (networkSyncData.y !== undefined) {
this.setY(networkSyncData.y);
}
if (networkSyncData.z !== undefined) {
this.setZOrder(networkSyncData.z);
if (networkSyncData.zo !== undefined) {
this.setZOrder(networkSyncData.zo);
}
if (networkSyncData.a !== undefined) {
this.setAngle(networkSyncData.a);

View File

@@ -45,8 +45,10 @@ declare type BasicObjectNetworkSyncData = {
x: number;
/** The position of the object on the Y axis. */
y: number;
/** The position of the object on the Z axis. */
z: number;
/** The position of the object on the Z axis. Defined only for 3D games */
z?: number;
/** Z order of the object */
zo: number;
/** The angle of the object. */
a: number;
/** If the object is hidden */

View File

@@ -3569,6 +3569,7 @@ interface PreviewExportOptions {
[Ref] PreviewExportOptions SetExternalLayoutName([Const] DOMString externalLayoutName);
[Ref] PreviewExportOptions SetIncludeFileHash([Const] DOMString includeFile, long hash);
[Ref] PreviewExportOptions SetProjectDataOnlyExport(boolean enable);
[Ref] PreviewExportOptions SetNativeMobileApp(boolean enable);
[Ref] PreviewExportOptions SetFullLoadingScreen(boolean enable);
[Ref] PreviewExportOptions SetIsDevelopmentEnvironment(boolean enable);
[Ref] PreviewExportOptions SetNonRuntimeScriptsCacheBurst(unsigned long value);

View File

@@ -2732,6 +2732,7 @@ export class PreviewExportOptions extends EmscriptenObject {
setExternalLayoutName(externalLayoutName: string): PreviewExportOptions;
setIncludeFileHash(includeFile: string, hash: number): PreviewExportOptions;
setProjectDataOnlyExport(enable: boolean): PreviewExportOptions;
setNativeMobileApp(enable: boolean): PreviewExportOptions;
setFullLoadingScreen(enable: boolean): PreviewExportOptions;
setIsDevelopmentEnvironment(enable: boolean): PreviewExportOptions;
setNonRuntimeScriptsCacheBurst(value: number): PreviewExportOptions;

View File

@@ -8,6 +8,7 @@ declare class gdPreviewExportOptions {
setExternalLayoutName(externalLayoutName: string): gdPreviewExportOptions;
setIncludeFileHash(includeFile: string, hash: number): gdPreviewExportOptions;
setProjectDataOnlyExport(enable: boolean): gdPreviewExportOptions;
setNativeMobileApp(enable: boolean): gdPreviewExportOptions;
setFullLoadingScreen(enable: boolean): gdPreviewExportOptions;
setIsDevelopmentEnvironment(enable: boolean): gdPreviewExportOptions;
setNonRuntimeScriptsCacheBurst(value: number): gdPreviewExportOptions;

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,12 @@
[
{
"id": "cameraParallax",
"titleByLocale": { "en": "Let's improve the camera and the background" },
"bulletPointsByLocale": [
{ "en": "Add a background with parallax effect" },
{ "en": "Add an extension" },
{ "en": "Use basic camera movements to follow the player" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/cameraParallax.json",
"availableLocales": [
"en",
@@ -26,15 +32,29 @@
"player": "PlayerObject",
"farBackground": "FarBackground",
"midBackground": "MidBackground"
}
},
"isMiniTutorial": true
},
{
"id": "flingGame",
"titleByLocale": { "en": "Let's make a Fling Game" },
"bulletPointsByLocale": [
{ "en": "Learn to create a game from zero." },
{ "en": "Add a leaderboard to your game." }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/flingGame.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"]
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"],
"isMiniTutorial": false
},
{
"id": "healthBar",
"titleByLocale": {
"en": "Let's communicate to the player the remaining health points"
},
"bulletPointsByLocale": [
{ "en": "Use a prefab for a health bar" },
{ "en": "Update the health bar based on the player's health" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/healthBar.json",
"availableLocales": [
"en",
@@ -55,10 +75,16 @@
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/healthBar/game.json",
"initialProjectData": { "level": "Level", "player": "Player" }
"initialProjectData": { "level": "Level", "player": "Player" },
"isMiniTutorial": true
},
{
"id": "joystick",
"titleByLocale": { "en": "Let's add mobile controls to our game" },
"bulletPointsByLocale": [
{ "en": "Add a joystick prefab" },
{ "en": "Add a behavior" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/joystick.json",
"availableLocales": [
"en",
@@ -82,10 +108,32 @@
"initialProjectData": {
"gameScene": "GameScene",
"ship": "OrangePlayerShip3"
}
},
"isMiniTutorial": true
},
{
"id": "knightPlatformer",
"titleByLocale": { "en": "Let's make a platformer game" },
"bulletPointsByLocale": [
{ "en": "Use a behavior to mark an object as a platform." },
{ "en": "Use another behavior to control the player." }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/knightPlatformer.json",
"availableLocales": ["en"],
"initialProjectData": {
"playerHitBox": "KnightHitBox",
"tiles": "Tiles",
"level1": "Level 1"
},
"isMiniTutorial": true
},
{
"id": "object3d",
"titleByLocale": { "en": "Let's add a 3D object to our game" },
"bulletPointsByLocale": [
{ "en": "Add a 3D Box" },
{ "en": "Add a behavior" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/object3d.json",
"availableLocales": [
"en",
@@ -106,10 +154,17 @@
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/object3d/game.json",
"initialProjectData": { "gameScene": "GameScene", "platform": "Platform" }
"initialProjectData": { "gameScene": "GameScene", "platform": "Platform" },
"isMiniTutorial": true
},
{
"id": "plinkoMultiplier",
"titleByLocale": { "en": "Let's improve a scoring system" },
"bulletPointsByLocale": [
{ "en": "Making objects disappear or appear when colliding" },
{ "en": "Creating, modifying and accessing a scene variable" },
{ "en": "Updating a score accordingly" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/plinkoMultiplier.json",
"availableLocales": [
"en",
@@ -136,10 +191,17 @@
"scoreMultiplier": "ScoreMultiplier",
"ball": "Ball",
"particles": "PegStar_Particle"
}
},
"isMiniTutorial": true
},
{
"id": "timer",
"titleByLocale": { "en": "Let's use time to measure a score" },
"bulletPointsByLocale": [
{ "en": "Create and modify a text" },
{ "en": "Start a timer" },
{ "en": "Use the timer to display a score" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/timer.json",
"availableLocales": [
"en",
@@ -160,6 +222,7 @@
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/timer/game.json",
"initialProjectData": { "gameScene": "GameScene" }
"initialProjectData": { "gameScene": "GameScene" },
"isMiniTutorial": true
}
]

View File

@@ -1454,9 +1454,6 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
isGlobalTabInitiallyOpen={
variablesEditorOpen.isGlobalTabInitiallyOpen
}
globalVariables={eventsFunctionsExtension.getGlobalVariables()}
sceneVariables={eventsFunctionsExtension.getSceneVariables()}
project={project}
projectScopedContainersAccessor={
new ProjectScopedContainersAccessor({
project,
@@ -1466,7 +1463,6 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
open
onCancel={() => this._editVariables(null)}
onApply={() => this._editVariables(null)}
preventRefactoringToDeleteInstructions
/>
)}
{objectMethodSelectorDialogOpen && selectedEventsBasedObject && (

View File

@@ -35,7 +35,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
onChange,
value,
} = props;
const { layout, eventsFunctionsExtension } = scope;
const { layout } = scope;
const enumerateGlobalAndSceneVariables = React.useCallback(
() =>
@@ -109,33 +109,14 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
}
onInstructionTypeChanged={onInstructionTypeChanged}
/>
{editorOpen && project && layout && (
{editorOpen && (
<GlobalAndSceneVariablesDialog
globalVariables={project.getVariables()}
sceneVariables={layout.getVariables()}
project={project}
layout={layout}
projectScopedContainersAccessor={projectScopedContainersAccessor}
open
onCancel={() => setEditorOpen(false)}
onApply={onVariableEditorApply}
isGlobalTabInitiallyOpen={isGlobal}
initiallySelectedVariableName={props.value}
preventRefactoringToDeleteInstructions
/>
)}
{editorOpen && project && eventsFunctionsExtension && (
<GlobalAndSceneVariablesDialog
globalVariables={eventsFunctionsExtension.getGlobalVariables()}
sceneVariables={eventsFunctionsExtension.getSceneVariables()}
project={project}
projectScopedContainersAccessor={projectScopedContainersAccessor}
open
onCancel={() => setEditorOpen(false)}
onApply={onVariableEditorApply}
isGlobalTabInitiallyOpen={isGlobal}
initiallySelectedVariableName={props.value}
preventRefactoringToDeleteInstructions
/>
)}
</React.Fragment>

View File

@@ -117,7 +117,7 @@ const AutocompletionRow = React.forwardRef(
onClick={onClick}
ref={ref}
>
<LineStackLayout noMargin>
<LineStackLayout noMargin expand>
{icon || (iconSrc ? <AutocompletionIcon src={iconSrc} /> : null)}
{secondaryIcon}
<Text style={defaultTextStyle} noMargin align="left">

View File

@@ -43,6 +43,7 @@ type Props = {|
settingsIcon?: React.Node,
moveEventsIntoNewGroup: () => void,
canMoveEventsIntoNewGroup: boolean,
onOpenSceneVariables: () => void,
|};
const Toolbar = React.memo<Props>(function Toolbar({
@@ -69,6 +70,7 @@ const Toolbar = React.memo<Props>(function Toolbar({
settingsIcon,
moveEventsIntoNewGroup,
canMoveEventsIntoNewGroup,
onOpenSceneVariables,
}: Props) {
const shortcutMap = useShortcutMap();
@@ -97,6 +99,7 @@ const Toolbar = React.memo<Props>(function Toolbar({
onOpenSettings={onOpenSettings}
moveEventsIntoNewGroup={moveEventsIntoNewGroup}
canMoveEventsIntoNewGroup={canMoveEventsIntoNewGroup}
onOpenSceneVariables={onOpenSceneVariables}
/>
<ToolbarGroup lastChild>
<IconButton

View File

@@ -29,6 +29,7 @@ type Props = {|
onOpenSettings?: ?() => void,
moveEventsIntoNewGroup: () => void,
canMoveEventsIntoNewGroup: boolean,
onOpenSceneVariables: () => void,
|};
const ToolbarCommands = (props: Props) => {
@@ -95,6 +96,10 @@ const ToolbarCommands = (props: Props) => {
handler: props.onOpenSettings || (() => {}),
});
useCommand('OPEN_SCENE_VARIABLES', true, {
handler: props.onOpenSceneVariables,
});
return null;
};

View File

@@ -115,6 +115,7 @@ import {
import { insertInVariablesContainer } from '../Utils/VariablesUtils';
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope.flow';
import LocalVariablesDialog from '../VariablesList/LocalVariablesDialog';
import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog';
const gd: libGDevelop = global.gd;
@@ -202,6 +203,8 @@ type State = {|
searchResults: ?Array<gdBaseEvent>,
searchFocusOffset: ?number,
layoutVariablesDialogOpen: boolean,
allEventsMetadata: Array<EventMetadata>,
fontSize: number,
@@ -294,6 +297,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
searchResults: null,
searchFocusOffset: null,
layoutVariablesDialogOpen: false,
allEventsMetadata: [],
textEditedEvent: null,
@@ -379,6 +384,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
onToggleSearchPanel={this._toggleSearchPanel}
canMoveEventsIntoNewGroup={hasSomethingSelected(this.state.selection)}
moveEventsIntoNewGroup={this.moveEventsIntoNewGroup}
onOpenSceneVariables={this.editLayoutVariables}
/>
);
}
@@ -699,6 +705,10 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
});
};
editLayoutVariables = (open: boolean = true) => {
this.setState({ layoutVariablesDialogOpen: open });
};
openVariablesEditor = (
eventContext: EventContext,
variableDeclarationContext: VariableDeclarationContext
@@ -2140,6 +2150,16 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
}
/>
)}
{this.state.layoutVariablesDialogOpen && (
<GlobalAndSceneVariablesDialog
projectScopedContainersAccessor={
projectScopedContainersAccessor
}
open
onCancel={() => this.editLayoutVariables(false)}
onApply={() => this.editLayoutVariables(false)}
/>
)}
{this.state.textEditedEvent && (
<EventTextDialog
event={this.state.textEditedEvent}

View File

@@ -42,6 +42,7 @@ function InAppTutorialDialog({
const actions = isLastStep
? [
<DialogPrimaryButton
primary
key="close"
onClick={onApply}
label={<Trans>Close</Trans>}

View File

@@ -11,7 +11,6 @@ import {
type InAppTutorialFlowStepTrigger,
type TranslatedText,
type EditorIdentifier,
isMiniTutorial,
} from '../Utils/GDevelopServices/InAppTutorial';
import {
createOrEnsureBadgeForUser,
@@ -502,7 +501,7 @@ const InAppTutorialOrchestrator = React.forwardRef<
elementWithValueToWatchIfEquals,
setElementWithValueToWatchIfEquals,
] = React.useState<?string>(null);
const inputExpectedValueRef = React.useRef<?string>(null);
const inputExpectedValueRef = React.useRef<?string | boolean>(null);
const [
objectSceneInstancesToWatch,
setObjectSceneInstancesToWatch,
@@ -858,7 +857,11 @@ const InAppTutorialOrchestrator = React.forwardRef<
() => {
if (!currentStep) return;
const { nextStepTrigger, elementToHighlightId } = currentStep;
if (nextStepTrigger && nextStepTrigger.valueEquals) {
if (
nextStepTrigger &&
nextStepTrigger.valueEquals !== undefined &&
nextStepTrigger.valueEquals !== null
) {
if (!elementToHighlightId) return;
inputExpectedValueRef.current = nextStepTrigger.valueEquals;
setElementWithValueToWatchIfEquals(elementToHighlightId);
@@ -938,17 +941,29 @@ const InAppTutorialOrchestrator = React.forwardRef<
const elementToWatch = document.querySelector(
elementWithValueToWatchIfEquals
);
if (!elementToWatch) return;
const inputExpectedValue = inputExpectedValueRef.current;
if (!inputExpectedValue) return;
if (inputExpectedValue === null) return;
// We trim all spaces to not be picky about the user input inside expressions.
// Ex: "1 + 1" === "1+1"
const inputValue = getInputValue(elementToWatch);
if (
elementToWatch &&
getInputValue(elementToWatch).replace(/\s/g, '') ===
typeof inputExpectedValue === 'boolean' &&
inputExpectedValue === inputValue
) {
goToNextStep();
return;
}
if (
typeof inputExpectedValue === 'string' &&
typeof inputValue === 'string' &&
// We trim all spaces to not be picky about the user input inside expressions.
// Ex: "1 + 1" === "1+1"
inputValue.replace(/\s/g, '') ===
inputExpectedValue.replace(/\s/g, '')
) {
goToNextStep();
return;
}
},
[goToNextStep, elementWithValueToWatchIfEquals]
@@ -1018,10 +1033,7 @@ const InAppTutorialOrchestrator = React.forwardRef<
currentStep && currentStep.isTriggerFlickering ? 500 : null
);
const isRunningMiniTutorial = React.useMemo(
() => isMiniTutorial(tutorial.id),
[tutorial.id]
);
const isRunningMiniTutorial = tutorial.isMiniTutorial;
const isTouchScreen = useScreenType() === 'touch';

View File

@@ -274,7 +274,16 @@ function InAppTutorialStepDisplayer({
const getFillAutomaticallyFunction = React.useCallback(
() => {
if (!nextStepTrigger || !nextStepTrigger.valueEquals) {
if (!nextStepTrigger || !('valueEquals' in nextStepTrigger)) {
return undefined;
}
// $FlowIgnore - checked above that valueEquals in in nextStepTrigger.
const { valueEquals } = nextStepTrigger;
if (
valueEquals === null ||
valueEquals === undefined ||
typeof valueEquals !== 'string'
) {
return undefined;
}
@@ -293,7 +302,7 @@ function InAppTutorialStepDisplayer({
const valueSetter = valuePropertyDescriptor.set;
if (!valueSetter) return undefined;
return () => {
valueSetter.call(elementWithId, nextStepTrigger.valueEquals);
valueSetter.call(elementWithId, valueEquals);
// Trigger blur to make sure the value is taken into account
// by the React input.
elementWithId.dispatchEvent(new Event('blur', { bubbles: true }));

View File

@@ -30,6 +30,10 @@ export class ProjectScopedContainersAccessor {
this._eventPath = eventPath;
}
getScope(): EventsScope {
return this._scope;
}
get(): gdProjectScopedContainers {
let projectScopedContainers;
const {

View File

@@ -22,7 +22,7 @@ import { duplicateLeaderboard } from '../Utils/GDevelopServices/Play';
import { registerGame } from '../Utils/GDevelopServices/Game';
import { toNewGdMapStringString } from '../Utils/MapStringString';
const gd = global.gd;
const gd: libGDevelop = global.gd;
type ReplacePromptDialogProps = {|
leaderboardsToReplace: ?Array<string>,
@@ -181,7 +181,7 @@ export const LeaderboardReplacerProgressDialog = ({
type RetryOrAbandonCallback = () => void;
type UseLeaderboardReplacerOutput = {
type UseLeaderboardReplacerOutput = {|
/**
* Launch search through the whole project for leaderboard ids to replace.
*/
@@ -191,7 +191,7 @@ type UseLeaderboardReplacerOutput = {
* Render, if needed, the dialog that will show the progress of leaderboard replacement.
*/
renderLeaderboardReplacerDialog: () => React.Node,
};
|};
type ErroredLeaderboard = {
leaderboardId: string,
@@ -319,6 +319,7 @@ export const useLeaderboardReplacer = (): UseLeaderboardReplacerOutput => {
gd.ProjectBrowserHelper.exposeProjectEvents(
project,
// $FlowIgnore - eventsLeaderboardReplacer inherits from ArbitraryEventsWorker
eventsLeaderboardReplacer
);
eventsLeaderboardReplacer.delete();
@@ -373,6 +374,7 @@ export const useLeaderboardReplacer = (): UseLeaderboardReplacerOutput => {
setGameId(sourceGameId);
const leaderboardsLister = new gd.EventsLeaderboardsLister(project);
// $FlowIgnore - leaderboardsLister inherits from ArbitraryEventsWorker
gd.ProjectBrowserHelper.exposeProjectEvents(project, leaderboardsLister);
const leaderboardIds = leaderboardsLister.getLeaderboardIds();
setLeaderboardsToReplace(leaderboardIds.toNewVectorString().toJSArray());

View File

@@ -20,6 +20,7 @@ import {
JOYSTICK_IN_APP_TUTORIAL_ID,
OBJECT_3D_IN_APP_TUTORIAL_ID,
KNIGHT_PLATFORMER_IN_APP_TUTORIAL_ID,
TOP_DOWN_RPG_PIXEL_PERFECT_ID,
guidedLessonsIds,
} from '../../../../Utils/GDevelopServices/InAppTutorial';
import MultiplierScore from './Icons/MultiplierScore';
@@ -36,6 +37,7 @@ import ColoredLinearProgress from '../../../../UI/ColoredLinearProgress';
import Trophy from '../../../../UI/CustomSvgIcons/Trophy';
import Object3D from './Icons/Object3D';
import Platformer from './Icons/Platformer';
import TopDownRPGMovement from './Icons/TopDownRPGMovement';
const getColumnsFromWindowSize = (
windowSize: WindowSizeType,
@@ -164,6 +166,13 @@ const GuidedLessons = ({ selectInAppTutorial, lessonsIds }: Props) => {
durationInMinutes: 1,
renderImage: props => <Platformer {...props} />,
},
{
id: TOP_DOWN_RPG_PIXEL_PERFECT_ID,
title: t`Top-Down RPG Pixel Perfect`,
description: t`Learn how to make a character move like in the retro Pokemon games.`,
durationInMinutes: 2,
renderImage: props => <TopDownRPGMovement {...props} />,
},
].filter(item => displayedGuidedLessonsIds.includes(item.id));
return (

View File

@@ -0,0 +1,123 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon
{...props}
width="120"
height="138"
viewBox="0 0 120 138"
fill="none"
>
<ellipse cx="59" cy="90.1111" rx="44" ry="30.5556" fill="#0B8735" />
<ellipse cx="59" cy="85.2222" rx="44" ry="30.5556" fill="#0CB746" />
<path
d="M49.8127 78.3526C49.2016 76.5191 50.0674 70.4583 50.5767 67.6572L57.0703 68.0391C56.8157 70.713 56.1536 76.5191 55.5424 78.3526C54.7785 80.6444 50.5767 80.6444 49.8127 78.3526Z"
fill="#C58C48"
/>
<path
d="M70.6114 73.0048C71.2226 71.1713 70.3567 65.1106 69.8474 62.3094L63.3538 62.6914C63.6085 65.3653 64.2706 71.1713 64.8817 73.0048C65.6457 75.2967 69.8474 75.2967 70.6114 73.0048Z"
fill="#B17834"
/>
<path
d="M40.1279 48.9777C42.6357 45.7984 45.6393 44.5198 47.1169 44.2751C50.1965 43.6133 50.9091 45.1658 48.9367 47.0111C47.6662 48.1997 46.0795 51.4426 45.0733 53.7771C44.663 54.729 43.9798 55.6315 42.99 55.9392C42.3259 56.1457 41.5064 56.2371 40.6145 55.9065C40.4388 55.8413 40.2741 55.7617 40.1199 55.6704C37.8809 54.3455 38.5167 51.0203 40.1279 48.9777Z"
fill="#C58C48"
/>
<path
d="M40.2698 48.5671C42.7669 45.7892 45.1954 44.5932 47.1169 44.275C50.1965 43.6132 50.8862 44.7396 49.6862 47.1594C48.7359 49.0754 47.0302 49.9388 46.1946 52.0767C45.6771 52.3143 45.0054 50.2072 43.1933 49.5354C41.3812 48.8635 40.3759 49.2076 40.2698 48.5671Z"
fill="#C14177"
/>
<path
d="M45.868 63.4081C46.3237 57.3472 48.1888 51.9507 49.6999 48.688C50.2978 47.3971 51.6233 46.6483 53.046 46.6483H66.3512C67.3702 46.6483 68.3735 47.0191 69.0075 47.8167C72.133 51.7492 73.2024 58.1152 73.6908 62.9642C73.9247 65.2857 72.0542 67.2751 69.7209 67.2751H49.6517C47.4944 67.2751 45.7062 65.5593 45.868 63.4081Z"
fill="#D46996"
/>
<path
d="M45.7717 65.4009C45.8342 62.4152 46.2692 59.5472 46.8789 56.9618C52.2764 60.7815 68.3195 60.3995 72.7942 56.9618C73.4135 59.8924 73.6995 62.8209 73.8841 65.1266C73.9915 66.4679 73.3812 67.7512 72.1231 68.2284C70.0982 68.9964 66.3 69.949 59.916 69.949C53.2463 69.949 49.399 68.9092 47.4476 68.1265C46.3454 67.6843 45.7469 66.5883 45.7717 65.4009Z"
fill="#2F6B96"
/>
<path
d="M75.6757 53.4042C74.2556 49.6121 71.7843 47.4793 70.4514 46.7961C67.7198 45.2277 66.5681 46.4893 67.8847 48.8477C68.7327 50.3668 69.2562 53.939 69.5034 56.469C69.6042 57.5007 69.98 58.5684 70.8291 59.163C71.3987 59.562 72.1514 59.8987 73.1017 59.8554C73.2889 59.8469 73.47 59.8212 73.6447 59.7813C76.1808 59.2014 76.5882 55.8405 75.6757 53.4042Z"
fill="#C58C48"
/>
<path
d="M75.6656 52.97C74.1335 49.5635 72.1847 47.6845 70.4514 46.7961C67.7198 45.2277 66.7197 46.0904 67.1257 48.7607C67.4471 50.8751 68.8087 52.2171 68.9534 54.508C69.3739 54.8919 70.6555 53.0896 72.5862 53.0017C74.5169 52.9138 75.3695 53.5477 75.6656 52.97Z"
fill="#C14177"
/>
<circle
cx="75.6383"
cy="31.8125"
r="4.20176"
transform="rotate(20.5422 75.6383 31.8125)"
fill="#2C3048"
/>
<circle
cx="69.5266"
cy="23.409"
r="4.20176"
transform="rotate(20.5422 69.5266 23.409)"
fill="#2C3048"
/>
<circle
cx="4.20176"
cy="4.20176"
r="4.20176"
transform="matrix(-0.936414 0.350898 0.350898 0.936414 44.4845 26.4035)"
fill="#2C3048"
/>
<circle
cx="4.20176"
cy="4.20176"
r="4.20176"
transform="matrix(-0.936414 0.350898 0.350898 0.936414 50.5961 18)"
fill="#2C3048"
/>
<path
d="M40.817 37.8628C40.817 35.4181 43.1089 35.0616 44.2548 35.1889L45.4008 41.6826C43.4909 41.6826 40.817 40.9186 40.817 37.8628Z"
fill="#B17834"
/>
<path
d="M77.105 37.8628C77.105 35.4181 74.8131 35.0616 73.6672 35.1889L72.5212 41.6826C74.4311 41.6826 77.105 40.9186 77.105 37.8628Z"
fill="#B17834"
/>
<path
d="M73.6671 36.3349C73.6671 44.3515 67.8413 50.0862 58.77 50.0862C49.6987 50.0862 43.8729 44.3515 43.8729 36.3349C43.8729 28.3184 49.6987 22.5837 58.77 22.5837C67.8413 22.5837 73.6671 28.3184 73.6671 36.3349Z"
fill="#C58C48"
/>
<path
d="M73.6671 36.3349C70.2293 28.3134 60.2979 32.5151 58.77 29.0773C57.2421 32.5151 48.8386 26.7855 43.8728 36.3349C41.199 28.3134 52.6583 19.9099 58.77 22.5837C64.8816 20.2918 76.341 28.6954 73.6671 36.3349Z"
fill="#2C3253"
/>
<path
d="M32 79.5229C30.8333 79.3563 28.4 78.2229 28 75.0229C29.3333 74.8563 32 75.5229 32 79.5229Z"
fill="#00A337"
/>
<path
d="M32 79.5229C32.875 79.4124 34.7 78.6607 35 76.5382C34 76.4276 32 76.8698 32 79.5229Z"
fill="#00A337"
/>
<path
d="M87.4793 96.9354C88.6205 96.6415 90.9144 95.2474 90.96 92.0228C89.6165 92.0038 87.0393 92.9597 87.4793 96.9354Z"
fill="#00A337"
/>
<path
d="M87.4792 96.9355C86.5974 96.9218 84.7008 96.3754 84.1692 94.2988C85.1509 94.0789 87.1874 94.2984 87.4792 96.9355Z"
fill="#00A337"
/>
<path
d="M82.2275 71.8942C83.3941 71.7275 85.8275 70.5942 86.2275 67.3942C84.8941 67.2275 82.2275 67.8942 82.2275 71.8942Z"
fill="#00A337"
/>
<path
d="M82 72C80.8333 71.8526 78.4 70.8503 78 68.0203C79.3333 67.8729 82 68.4625 82 72Z"
fill="#00A337"
/>
<path
d="M57.2275 106.523C58.3941 106.356 60.8275 105.223 61.2275 102.023C59.8941 101.856 57.2275 102.523 57.2275 106.523Z"
fill="#00A337"
/>
<path
d="M57 106.629C55.8333 106.481 53.4 105.479 53 102.649C54.3333 102.502 57 103.091 57 106.629Z"
fill="#00A337"
/>
</SvgIcon>
));

View File

@@ -1,22 +1,16 @@
// @flow
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import Dialog, { DialogPrimaryButton } from '../../../../UI/Dialog';
import FlatButton from '../../../../UI/FlatButton';
import { Line } from '../../../../UI/Grid';
import { ColumnStackLayout } from '../../../../UI/Layout';
import Text from '../../../../UI/Text';
import {
FLING_GAME_IN_APP_TUTORIAL_ID,
PLINKO_MULTIPLIER_IN_APP_TUTORIAL_ID,
TIMER_IN_APP_TUTORIAL_ID,
CAMERA_PARALLAX_IN_APP_TUTORIAL_ID,
HEALTH_BAR_IN_APP_TUTORIAL_ID,
JOYSTICK_IN_APP_TUTORIAL_ID,
OBJECT_3D_IN_APP_TUTORIAL_ID,
isMiniTutorial,
} from '../../../../Utils/GDevelopServices/InAppTutorial';
import InAppTutorialContext from '../../../../InAppTutorial/InAppTutorialContext';
import { type InAppTutorialShortHeader } from '../../../../Utils/GDevelopServices/InAppTutorial';
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
const styles = {
imgContainer: {
@@ -24,104 +18,110 @@ const styles = {
},
};
const getGuidedLessonContent = ({
learningKeys,
const getDialogContentByCompletionStatus = ({
i18n,
status,
startTutorial,
onClose,
isProjectOpened,
tutorialShortHeader,
tutorialId,
}: {
learningKeys: React.Node[],
}) => (
<>
<Text>
<Trans>You're about to start this guided lesson.</Trans>
</Text>
<Text>
<Trans>In this tutorial you will learn:</Trans>
</Text>
{learningKeys.map((learningKey, index) => (
<Text displayAsListItem noMargin key={index}>
{learningKey}
</Text>
))}
</>
);
i18n: I18nType,
status: 'notStarted' | 'started' | 'complete',
startTutorial: (scenario: 'resume' | 'startOver' | 'start') => Promise<void>,
onClose: () => void,
isProjectOpened?: boolean,
tutorialShortHeader: InAppTutorialShortHeader,
tutorialId: string,
}) => {
const resumeTutorial = () => startTutorial('resume');
const startOverTutorial = () => startTutorial('startOver');
const startTutorialForFirstTime = () => startTutorial('start');
const titleAndContentByKey = {
[FLING_GAME_IN_APP_TUTORIAL_ID]: {
title: <Trans>Let's make a Fling Game</Trans>,
if (status === 'started') {
return {
title: <Trans>Welcome back!</Trans>,
content: (
<Text>
<Trans>Let's finish your game, shall we?</Trans>
</Text>
),
primaryAction: {
label: <Trans>Let's go</Trans>,
onClick: resumeTutorial,
},
secondaryAction: {
label: isProjectOpened ? (
<Trans>No, close project</Trans>
) : (
<Trans>Back</Trans>
),
onClick: onClose,
},
tertiaryAction: {
label: <Trans>Restart tutorial</Trans>,
onClick: startOverTutorial,
},
};
} else if (status === 'complete') {
return {
title: <Trans>Restart the Tutorial</Trans>,
content: (
<>
<Text>
<Trans>
You're about to restart this multichapter guided lesson.
</Trans>
</Text>
<Text>
<Trans>
GDevelop will save your progress, so you can take a break if you
need.
</Trans>
</Text>
</>
),
primaryAction: {
label: <Trans>Restart</Trans>,
onClick: startOverTutorial,
},
secondaryAction: { label: <Trans>Back</Trans>, onClick: onClose },
tertiaryAction: null,
};
}
return {
title: selectMessageByLocale(i18n, tutorialShortHeader.titleByLocale),
content: (
<>
<Text>
<Trans>
You're about to start the first chapter of this guided lesson.
</Trans>
</Text>
<Text>
<Trans>
GDevelop will save your progress so you can take a break when you
need it.
</Trans>
<Trans>In this tutorial you will learn:</Trans>
</Text>
{tutorialShortHeader.bulletPointsByLocale.map(
(bulletPointByLocale, index) => (
<Text displayAsListItem noMargin key={index}>
{selectMessageByLocale(i18n, bulletPointByLocale)}
</Text>
)
)}
{!tutorialShortHeader.isMiniTutorial && (
<Text>
<Trans>
This is a multichapter tutorial. GDevelop will save your progress
so you can take a break when you need it.
</Trans>
</Text>
)}
</>
),
},
[PLINKO_MULTIPLIER_IN_APP_TUTORIAL_ID]: {
title: <Trans>Let's improve a scoring system</Trans>,
content: getGuidedLessonContent({
learningKeys: [
<Trans>Making objects disappear or appear when colliding</Trans>,
<Trans>Creating, modifying and accessing a scene variable</Trans>,
<Trans>Updating a score accordingly</Trans>,
],
}),
},
[TIMER_IN_APP_TUTORIAL_ID]: {
title: <Trans>Let's use time to measure a score</Trans>,
content: getGuidedLessonContent({
learningKeys: [
<Trans>Create and modify a text</Trans>,
<Trans>Start a timer</Trans>,
<Trans>Use the timer to display a score</Trans>,
],
}),
},
[CAMERA_PARALLAX_IN_APP_TUTORIAL_ID]: {
title: <Trans>Let's improve the camera and the background</Trans>,
content: getGuidedLessonContent({
learningKeys: [
<Trans>Add a background with parallax effect</Trans>,
<Trans>Add an extension</Trans>,
<Trans>Use basic camera movements to follow the player</Trans>,
],
}),
},
[HEALTH_BAR_IN_APP_TUTORIAL_ID]: {
title: (
<Trans>Let's communicate to the player the remaining health points</Trans>
),
content: getGuidedLessonContent({
learningKeys: [
<Trans>Use a prefab for a health bar</Trans>,
<Trans>Update the health bar based on the player's health</Trans>,
],
}),
},
[JOYSTICK_IN_APP_TUTORIAL_ID]: {
title: <Trans>Let's add mobile controls to our game</Trans>,
content: getGuidedLessonContent({
learningKeys: [
<Trans>Add a joystick prefab</Trans>,
<Trans>Add a behavior</Trans>,
],
}),
},
[OBJECT_3D_IN_APP_TUTORIAL_ID]: {
title: <Trans>Let's add a 3D object to our game</Trans>,
content: getGuidedLessonContent({
learningKeys: [
<Trans>Add a 3D Box</Trans>,
<Trans>Add a behavior</Trans>,
],
}),
},
primaryAction: {
label: <Trans>Let's go</Trans>,
onClick: startTutorialForFirstTime,
},
secondaryAction: { label: <Trans>Back</Trans>, onClick: onClose },
tertiaryAction: null,
};
};
type Props = {|
@@ -143,128 +143,91 @@ const StartInAppTutorialDialog = ({
startTutorial,
isProjectOpening,
}: Props) => {
const resumeTutorial = () => startTutorial('resume');
const startOverTutorial = () => startTutorial('startOver');
const startTutorialForFirstTime = () => startTutorial('start');
const { getInAppTutorialShortHeader } = React.useContext(
InAppTutorialContext
);
const dialogContentByCompletionStatus = {
notStarted: {
title: titleAndContentByKey[tutorialId].title,
content: titleAndContentByKey[tutorialId].content,
primaryAction: {
label: <Trans>Yes</Trans>,
onClick: startTutorialForFirstTime,
},
secondaryAction: { label: <Trans>No</Trans>, onClick: onClose },
tertiaryAction: null,
},
started: {
title: <Trans>Welcome back!</Trans>,
content: (
<Text>
<Trans>Let's finish your Fling Game, shall we?</Trans>
</Text>
),
primaryAction: {
label: <Trans>Let's go</Trans>,
onClick: resumeTutorial,
},
secondaryAction: {
label: isProjectOpened ? (
<Trans>No, close project</Trans>
) : (
<Trans>No</Trans>
),
onClick: onClose,
},
tertiaryAction: {
label: <Trans>Restart tutorial</Trans>,
onClick: startOverTutorial,
},
},
complete: {
title: <Trans>Restart the Tutorial</Trans>,
content: (
<>
<Text>
<Trans>You're about to restart this 3-chapter guided lesson.</Trans>
</Text>
<Text>
<Trans>
GDevelop will save your progress, so you can take a break if you
need.
</Trans>
</Text>
</>
),
primaryAction: { label: <Trans>Yes</Trans>, onClick: startOverTutorial },
secondaryAction: { label: <Trans>No</Trans>, onClick: onClose },
tertiaryAction: null,
},
};
const dialogContent =
dialogContentByCompletionStatus[
// Always show the "not started" dialog for the mini tutorials.
isMiniTutorial(tutorialId) ? 'notStarted' : tutorialCompletionStatus
];
const {
title,
content,
primaryAction,
secondaryAction,
tertiaryAction,
} = dialogContent;
const actions = [
<FlatButton
key="close"
label={secondaryAction.label}
onClick={secondaryAction.onClick}
disabled={isProjectOpening}
/>,
<DialogPrimaryButton
key="start"
label={primaryAction.label}
primary
onClick={primaryAction.onClick}
disabled={isProjectOpening}
/>,
];
const secondaryActions = tertiaryAction
? [
<FlatButton
key="other"
label={tertiaryAction.label}
onClick={tertiaryAction.onClick}
disabled={isProjectOpening}
/>,
]
: undefined;
const tutorialShortHeader = getInAppTutorialShortHeader(tutorialId);
if (!tutorialShortHeader) {
console.error(
`No tutorial short header found for tutorial ${tutorialId} - StartInAppTutorialDialog was opened despite tutorials not being loaded?`
);
return null;
}
return (
<Dialog
title={title}
actions={actions}
secondaryActions={secondaryActions}
open={open}
onRequestClose={onClose}
onApply={primaryAction.onClick}
maxWidth="xs"
cannotBeDismissed
>
<ColumnStackLayout noMargin>
<Line alignItems="center" justifyContent="center" noMargin>
<div style={styles.imgContainer}>
<img alt="hero" src="res/hero.png" width={48} height={48} />
</div>
</Line>
{content}
<Text>
<Trans>Are you ready?</Trans>
</Text>
</ColumnStackLayout>
</Dialog>
<I18n>
{({ i18n }) => {
const dialogContent = getDialogContentByCompletionStatus({
i18n,
status:
// Always show the "not started" dialog for the mini tutorials.
tutorialShortHeader.isMiniTutorial
? 'notStarted'
: tutorialCompletionStatus,
startTutorial,
onClose,
isProjectOpened,
tutorialShortHeader,
tutorialId,
});
const {
title,
content,
primaryAction,
secondaryAction,
tertiaryAction,
} = dialogContent;
const actions = [
<FlatButton
key="close"
label={secondaryAction.label}
onClick={secondaryAction.onClick}
disabled={isProjectOpening}
/>,
<DialogPrimaryButton
key="start"
label={primaryAction.label}
primary
onClick={primaryAction.onClick}
disabled={isProjectOpening}
/>,
];
const secondaryActions = tertiaryAction
? [
<FlatButton
key="other"
label={tertiaryAction.label}
onClick={tertiaryAction.onClick}
disabled={isProjectOpening}
/>,
]
: undefined;
return (
<Dialog
title={title}
actions={actions}
secondaryActions={secondaryActions}
open={open}
onRequestClose={onClose}
onApply={primaryAction.onClick}
maxWidth="xs"
cannotBeDismissed
>
<ColumnStackLayout noMargin>
<Line alignItems="center" justifyContent="center" noMargin>
<div style={styles.imgContainer}>
<img alt="hero" src="res/hero.png" width={48} height={48} />
</div>
</Line>
{content}
</ColumnStackLayout>
</Dialog>
);
}}
</I18n>
);
};

View File

@@ -0,0 +1,60 @@
// @flow
import * as React from 'react';
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import { duplicateLobbyConfiguration } from '../Utils/GDevelopServices/Play';
const gd: libGDevelop = global.gd;
type UseMultiplayerLobbyConfiguratorOutput = {|
configureMultiplayerLobbiesIfNeeded: (
project: gdProject,
sourceGameId: string
) => Promise<void>,
|};
/**
* Hook allowing to duplicate lobby configuration from another project, useful after
* opening a project from an example.
*/
export const useMultiplayerLobbyConfigurator = (): UseMultiplayerLobbyConfiguratorOutput => {
const { profile, getAuthorizationHeader } = React.useContext(
AuthenticatedUserContext
);
const configureMultiplayerLobbiesIfNeeded = React.useCallback(
async (project: gdProject, sourceGameId: string) => {
const isMultiplayerExtensionUsed = gd.UsedExtensionsFinder.scanProject(
project
)
.getUsedExtensions()
.toNewVectorString()
.toJSArray()
.some(extensionName => extensionName === 'Multiplayer');
if (!isMultiplayerExtensionUsed) return;
if (!profile) {
console.warn(
'User is not connected. Aborting multiplayer lobby configuration.'
);
return;
}
try {
await duplicateLobbyConfiguration({
userId: profile.id,
getAuthorizationHeader,
gameId: project.getProjectUuid(),
sourceGameId,
});
} catch (error) {
console.error(
`An error occurred while copying lobby configuration from game with id ${sourceGameId}. Failing silently: `,
error
);
}
},
[getAuthorizationHeader, profile]
);
return {
configureMultiplayerLobbiesIfNeeded,
};
};

View File

@@ -142,7 +142,7 @@ import {
sendInAppTutorialStarted,
sendEventsExtractedAsFunction,
} from '../Utils/Analytics/EventSender';
import { useLeaderboardReplacer } from '../Leaderboard/useLeaderboardReplacer';
import { useLeaderboardReplacer } from '../Leaderboard/UseLeaderboardReplacer';
import useAlertDialog from '../UI/Alert/useAlertDialog';
import NewProjectSetupDialog from '../ProjectCreation/NewProjectSetupDialog';
import {
@@ -164,10 +164,6 @@ import {
useStableUpToDateRef,
} from '../Utils/UseStableUpToDateCallback';
import { emptyStorageProvider } from '../ProjectsStorage/ProjectStorageProviders';
import {
isMiniTutorial,
allInAppTutorialIds,
} from '../Utils/GDevelopServices/InAppTutorial';
import CustomDragLayer from '../UI/DragAndDrop/CustomDragLayer';
import CloudProjectRecoveryDialog from '../ProjectsStorage/CloudStorageProvider/CloudProjectRecoveryDialog';
import CloudProjectSaveChoiceDialog from '../ProjectsStorage/CloudStorageProvider/CloudProjectSaveChoiceDialog';
@@ -182,6 +178,7 @@ import useVersionHistory from '../VersionHistory/UseVersionHistory';
import { ProjectManagerDrawer } from '../ProjectManager/ProjectManagerDrawer';
import DiagnosticReportDialog from '../ExportAndShare/DiagnosticReportDialog';
import useSaveReminder from './UseSaveReminder';
import { useMultiplayerLobbyConfigurator } from './UseMultiplayerLobbyConfigurator';
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
@@ -414,6 +411,9 @@ const MainFrame = (props: Props) => {
findLeaderboardsToReplace,
renderLeaderboardReplacerDialog,
} = useLeaderboardReplacer();
const {
configureMultiplayerLobbiesIfNeeded,
} = useMultiplayerLobbyConfigurator();
const eventsFunctionsExtensionsState = React.useContext(
EventsFunctionsExtensionsContext
);
@@ -683,15 +683,6 @@ const MainFrame = (props: Props) => {
getEditorOpeningOptions,
});
useOpenInitialDialog({
openInAppTutorialDialog: (tutorialId: string) => {
if (allInAppTutorialIds.includes(tutorialId)) {
selectInAppTutorial(tutorialId);
}
},
openProfileDialog,
});
const _closeSnackMessage = React.useCallback(
() => {
setState(state => ({
@@ -1161,6 +1152,7 @@ const MainFrame = (props: Props) => {
setNewProjectSetupDialogOpen(false);
closeExampleStoreDialog({ deselectExampleAndGameTemplate: true });
findLeaderboardsToReplace(project, oldProjectId);
configureMultiplayerLobbiesIfNeeded(project, oldProjectId);
openSceneOrProjectManager({
currentProject: project,
editorTabs: editorTabs,
@@ -2378,7 +2370,7 @@ const MainFrame = (props: Props) => {
preferences.insertRecentProjectFile(fileMetadataAndStorageProviderName);
if (
currentlyRunningInAppTutorial &&
!isMiniTutorial(currentlyRunningInAppTutorial.id) && // Don't save the progress of mini-tutorials
!currentlyRunningInAppTutorial.isMiniTutorial && // Don't save the progress of mini-tutorials
inAppTutorialOrchestratorRef.current
) {
preferences.saveTutorialProgress({
@@ -2570,7 +2562,7 @@ const MainFrame = (props: Props) => {
);
if (
currentlyRunningInAppTutorial &&
!isMiniTutorial(currentlyRunningInAppTutorial.id) && // Don't save the progress of mini-tutorials
!currentlyRunningInAppTutorial.isMiniTutorial && // Don't save the progress of mini-tutorials
inAppTutorialOrchestratorRef.current
) {
preferences.saveTutorialProgress({
@@ -2747,6 +2739,11 @@ const MainFrame = (props: Props) => {
[preferences, authenticatedUser.profile]
);
useOpenInitialDialog({
openInAppTutorialDialog: selectInAppTutorial,
openProfileDialog,
});
const onChangeProjectName = async (newName: string): Promise<void> => {
if (!currentProject || !currentFileMetadata) return;
const storageProviderOperations = getStorageProviderOperations();

View File

@@ -48,6 +48,7 @@ type Props = {|
settingsIcon?: React.Node,
canRenameObject: boolean,
onRenameObject: () => void,
onOpenSceneVariables: () => void,
|};
const Toolbar = React.memo<Props>(function Toolbar(props) {
@@ -70,6 +71,7 @@ const Toolbar = React.memo<Props>(function Toolbar(props) {
canDeleteSelection={props.selectedInstancesCount !== 0}
canRenameObject={props.canRenameObject}
onRenameObject={props.onRenameObject}
onOpenSceneVariables={props.onOpenSceneVariables}
/>
<ToolbarGroup lastChild>
<IconButton

View File

@@ -38,6 +38,7 @@ type Props = {|
settingsIcon: React.Node,
canRenameObject: boolean,
onRenameObject: () => void,
onOpenSceneVariables: () => void,
|};
const Toolbar = React.memo<Props>(function(props) {
@@ -60,6 +61,7 @@ const Toolbar = React.memo<Props>(function(props) {
canDeleteSelection={props.selectedInstancesCount !== 0}
canRenameObject={props.canRenameObject}
onRenameObject={props.onRenameObject}
onOpenSceneVariables={props.onOpenSceneVariables}
/>
<IconButton
size="small"

View File

@@ -18,6 +18,7 @@ type Props = {|
setupGrid: () => void,
canRenameObject: boolean,
onRenameObject: () => void,
onOpenSceneVariables: () => void,
|};
const ToolbarCommands = (props: Props) => {
@@ -69,6 +70,10 @@ const ToolbarCommands = (props: Props) => {
handler: props.onRenameObject,
});
useCommand('OPEN_SCENE_VARIABLES', true, {
handler: props.onOpenSceneVariables,
});
return null;
};

View File

@@ -10,7 +10,6 @@ type Props = {|
onEditObject: (object: gdObject) => void,
onEditObjectVariables: (object: gdObject) => void,
onOpenSceneProperties: () => void,
onOpenSceneVariables: () => void,
onEditObjectGroup: (group: gdObjectGroup) => void,
onEditLayerEffects: (layer: gdLayer) => void,
onEditLayer: (layer: gdLayer) => void,
@@ -23,7 +22,6 @@ const UseSceneEditorCommands = (props: Props) => {
onEditObject,
onEditObjectVariables,
onOpenSceneProperties,
onOpenSceneVariables,
onEditObjectGroup,
onEditLayerEffects,
onEditLayer,
@@ -33,10 +31,6 @@ const UseSceneEditorCommands = (props: Props) => {
handler: onOpenSceneProperties,
});
useCommand('OPEN_SCENE_VARIABLES', true, {
handler: onOpenSceneVariables,
});
useObjectsListCommands({
project,
layout,

View File

@@ -306,6 +306,7 @@ export default class SceneEditor extends React.Component<Props, State> {
this.state.selectedObjectFolderOrObjectsWithContext.length === 1
}
onRenameObject={this._startRenamingSelectedObject}
onOpenSceneVariables={this.editLayoutVariables}
/>
);
} else {
@@ -337,6 +338,7 @@ export default class SceneEditor extends React.Component<Props, State> {
this.state.selectedObjectFolderOrObjectsWithContext.length === 1
}
onRenameObject={this._startRenamingSelectedObject}
onOpenSceneVariables={this.editLayoutVariables}
/>
);
}
@@ -1684,7 +1686,6 @@ export default class SceneEditor extends React.Component<Props, State> {
this.editObject(object, 'variables');
}}
onOpenSceneProperties={this.openSceneProperties}
onOpenSceneVariables={this.editLayoutVariables}
onEditObjectGroup={this.editGroup}
onEditLayerEffects={this.editLayerEffects}
onEditLayer={this.editLayer}

View File

@@ -19,6 +19,7 @@ export const JOYSTICK_IN_APP_TUTORIAL_ID = 'joystick';
export const TIMER_IN_APP_TUTORIAL_ID = 'timer';
export const OBJECT_3D_IN_APP_TUTORIAL_ID = 'object3d';
export const KNIGHT_PLATFORMER_IN_APP_TUTORIAL_ID = 'knightPlatformer';
export const TOP_DOWN_RPG_PIXEL_PERFECT_ID = 'topDownRPGPixelPerfect';
export const guidedLessonsIds = [
PLINKO_MULTIPLIER_IN_APP_TUTORIAL_ID,
@@ -28,18 +29,18 @@ export const guidedLessonsIds = [
JOYSTICK_IN_APP_TUTORIAL_ID,
OBJECT_3D_IN_APP_TUTORIAL_ID,
KNIGHT_PLATFORMER_IN_APP_TUTORIAL_ID,
TOP_DOWN_RPG_PIXEL_PERFECT_ID,
];
const fullTutorialIds = [FLING_GAME_IN_APP_TUTORIAL_ID];
export const allInAppTutorialIds = [...guidedLessonsIds, ...fullTutorialIds];
export type InAppTutorialShortHeader = {|
id: string,
titleByLocale: MessageByLocale,
bulletPointsByLocale: Array<MessageByLocale>,
contentUrl: string,
availableLocales: Array<string>,
initialTemplateUrl?: string,
initialProjectData?: { [key: string]: string },
isMiniTutorial?: boolean,
|};
export type EditorIdentifier =
@@ -66,7 +67,7 @@ export type InAppTutorialFlowStepTrigger =
| InAppTutorialFlowStepDOMChangeTrigger
| {| editorIsActive: string |}
| {| valueHasChanged: true |}
| {| valueEquals: string |}
| {| valueEquals: string | boolean |}
| {| instanceAddedOnScene: string, instancesCount?: number |}
| {| objectAddedInLayout: true |}
| {| previewLaunched: true |}
@@ -76,7 +77,7 @@ export type InAppTutorialFlowStepFormattedTrigger =
| InAppTutorialFlowStepDOMChangeTrigger
| {| editorIsActive: string |}
| {| valueHasChanged: true |}
| {| valueEquals: string |}
| {| valueEquals: string | boolean |}
| {| instanceAddedOnScene: string, instancesCount?: number |}
| {| objectAddedInLayout: true |}
| {| previewLaunched: true |}
@@ -139,6 +140,7 @@ export type InAppTutorial = {|
},
endDialog: InAppTutorialDialog,
availableLocales?: Array<string>,
isMiniTutorial?: boolean,
|};
const fetchLocalFileIfDesktop = async (filename: string): Promise<?Object> => {
@@ -197,14 +199,3 @@ export const fetchInAppTutorial = async (
const response = await axios.get(shortHeader.contentUrl);
return response.data;
};
export const isMiniTutorial = (tutorialId: string) =>
[
PLINKO_MULTIPLIER_IN_APP_TUTORIAL_ID,
TIMER_IN_APP_TUTORIAL_ID,
CAMERA_PARALLAX_IN_APP_TUTORIAL_ID,
HEALTH_BAR_IN_APP_TUTORIAL_ID,
JOYSTICK_IN_APP_TUTORIAL_ID,
OBJECT_3D_IN_APP_TUTORIAL_ID,
KNIGHT_PLATFORMER_IN_APP_TUTORIAL_ID,
].includes(tutorialId);

View File

@@ -502,3 +502,26 @@ export const updateLobbyConfiguration = async (
);
return response.data;
};
export const duplicateLobbyConfiguration = async ({
getAuthorizationHeader,
userId,
gameId,
sourceGameId,
}: {|
getAuthorizationHeader: () => Promise<string>,
userId: string,
gameId: string,
sourceGameId: string,
|}): Promise<LobbyConfiguration> => {
const authorizationHeader = await getAuthorizationHeader();
const response = await axios.post(
`${GDevelopPlayApi.baseUrl}/game/${gameId}/lobby-configuration/action/copy`,
{ sourceGameId },
{
headers: { Authorization: authorizationHeader },
params: { userId },
}
);
return response.data;
};

View File

@@ -8,37 +8,39 @@ import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/Even
type Props = {|
open: boolean,
globalVariables: gdVariablesContainer,
sceneVariables: gdVariablesContainer,
project: gdProject,
layout?: ?gdLayout,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
onApply: (selectedVariableName: string | null) => void,
onCancel: () => void,
hotReloadPreviewButtonProps?: ?HotReloadPreviewButtonProps,
/**
* If set to true, a deleted variable won't trigger a confirmation asking if the
* project must be refactored to delete any reference to it.
*/
preventRefactoringToDeleteInstructions?: boolean,
isGlobalTabInitiallyOpen?: boolean,
initiallySelectedVariableName?: string,
|};
const GlobalAndSceneVariablesDialog = ({
globalVariables,
sceneVariables,
project,
layout,
projectScopedContainersAccessor,
open,
onCancel,
onApply,
hotReloadPreviewButtonProps,
preventRefactoringToDeleteInstructions,
isGlobalTabInitiallyOpen,
initiallySelectedVariableName,
}: Props) => {
const {
project,
layout,
eventsFunctionsExtension,
} = projectScopedContainersAccessor.getScope();
let globalVariables = null;
let sceneVariables = null;
if (layout) {
globalVariables = project.getVariables();
sceneVariables = layout.getVariables();
} else if (eventsFunctionsExtension) {
globalVariables = eventsFunctionsExtension.getGlobalVariables();
sceneVariables = eventsFunctionsExtension.getSceneVariables();
}
const onComputeAllSceneVariableNames = React.useCallback(
() =>
layout
@@ -65,7 +67,7 @@ const GlobalAndSceneVariablesDialog = ({
const tabs = React.useMemo(
() =>
[
{
sceneVariables && {
id: 'scene-variables',
label: <Trans>Scene variables</Trans>,
variablesContainer: sceneVariables,
@@ -77,7 +79,7 @@ const GlobalAndSceneVariablesDialog = ({
),
onComputeAllVariableNames: onComputeAllSceneVariableNames,
},
{
globalVariables && {
id: 'global-variables',
label: <Trans>Global variables</Trans>,
variablesContainer: globalVariables,
@@ -119,9 +121,7 @@ const GlobalAndSceneVariablesDialog = ({
initiallySelectedVariableName={initiallySelectedVariableName}
helpPagePath={'/all-features/variables/scene-variables'}
hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
preventRefactoringToDeleteInstructions={
preventRefactoringToDeleteInstructions
}
preventRefactoringToDeleteInstructions={true}
id="global-and-scene-variables-dialog"
/>
);

View File

@@ -4,29 +4,147 @@ import { action } from '@storybook/addon-actions';
import { type StoryDecorator } from '@storybook/react';
import InAppTutorialContext from '../InAppTutorial/InAppTutorialContext';
const inAppTutorialShortHeaders = [
{
id: 'cameraParallax',
titleByLocale: { en: "Let's improve the camera and the background" },
bulletPointsByLocale: [
{ en: 'Add a background with parallax effect' },
{ en: 'Add an extension' },
{ en: 'Use basic camera movements to follow the player' },
],
contentUrl:
'https://resources.gdevelop-app.com/in-app-tutorials/cameraParallax.json',
availableLocales: [
'en',
'fr',
'ar',
'de',
'es',
'it',
'ja',
'ko',
'pl',
'pt',
'th',
'ru',
'sl',
'sq',
'uk',
'zh',
],
initialTemplateUrl:
'https://resources.gdevelop-app.com/in-app-tutorials/templates/cameraParallax/game.json',
initialProjectData: {
cameraScene: 'CameraScene',
player: 'PlayerObject',
farBackground: 'FarBackground',
midBackground: 'MidBackground',
},
isMiniTutorial: true,
},
{
id: 'flingGame',
titleByLocale: { en: "Let's make a Fling Game" },
bulletPointsByLocale: [
{ en: 'Learn to create a game from zero.' },
{ en: 'Add a leaderboard to your game.' },
],
contentUrl:
'https://resources.gdevelop-app.com/in-app-tutorials/flingGame.json',
availableLocales: ['en', 'fr', 'es', 'pt', 'th', 'ar'],
isMiniTutorial: false,
},
{
id: 'healthBar',
titleByLocale: {
en: "Let's communicate to the player the remaining health points",
},
bulletPointsByLocale: [
{ en: 'Use a prefab for a health bar' },
{ en: "Update the health bar based on the player's health" },
],
contentUrl:
'https://resources.gdevelop-app.com/in-app-tutorials/healthBar.json',
availableLocales: [
'en',
'fr',
'ar',
'de',
'es',
'it',
'ja',
'ko',
'pl',
'pt',
'th',
'ru',
'sl',
'sq',
'uk',
'zh',
],
initialTemplateUrl:
'https://resources.gdevelop-app.com/in-app-tutorials/templates/healthBar/game.json',
initialProjectData: { level: 'Level', player: 'Player' },
isMiniTutorial: true,
},
{
id: 'joystick',
titleByLocale: { en: "Let's add mobile controls to our game" },
bulletPointsByLocale: [
{ en: 'Add a joystick prefab' },
{ en: 'Add a behavior' },
],
contentUrl:
'https://resources.gdevelop-app.com/in-app-tutorials/joystick.json',
availableLocales: [
'en',
'fr',
'ar',
'de',
'es',
'it',
'ja',
'ko',
'pl',
'pt',
'th',
'ru',
'sl',
'sq',
'uk',
'zh',
],
initialTemplateUrl:
'https://resources.gdevelop-app.com/in-app-tutorials/templates/joystick/game.json',
initialProjectData: {
gameScene: 'GameScene',
ship: 'OrangePlayerShip3',
},
isMiniTutorial: true,
},
];
const inAppTutorialDecorator: StoryDecorator = (Story, context) => {
return (
<InAppTutorialContext.Provider
value={{
currentlyRunningInAppTutorial: null,
getInAppTutorialShortHeader: (tutorialId: string) => ({
id: 'flingGame',
availableLocales: ['en', 'fr-FR'],
contentUrl: 'fakeContentUrl',
}),
getInAppTutorialShortHeader: (tutorialId: string) => {
return (
inAppTutorialShortHeaders.find(
header => header.id === tutorialId
) || null
);
},
startTutorial: async () => {
action('Start tutorial')();
},
endTutorial: () => {
action('End tutorial')();
},
inAppTutorialShortHeaders: [
{
id: 'flingGame',
availableLocales: ['en', 'fr-FR'],
contentUrl: 'fakeContentUrl',
},
],
inAppTutorialShortHeaders,
startStepIndex: 0,
startProjectData: {},
inAppTutorialsFetchingError: null,

View File

@@ -24,8 +24,8 @@ import {
indieUserProfile,
} from '../../../fixtures/GDevelopServicesTestData';
import { GDevelopAssetApi } from '../../../Utils/GDevelopServices/ApiConfigs';
import InAppTutorialContext from '../../../InAppTutorial/InAppTutorialContext';
import fakeResourceManagementProps from '../../FakeResourceManagement';
import inAppTutorialDecorator from '../../InAppTutorialDecorator';
const apiDataServerSideError = {
mockData: [
@@ -80,79 +80,46 @@ const WrappedHomePage = ({
getTutorialProgress: () => tutorialProgress,
}}
>
<InAppTutorialContext.Provider
value={{
inAppTutorialShortHeaders: [
{
id: 'flingGame',
contentUrl: 'fakeUrl',
availableLocales: ['en', 'fr-FR'],
},
],
getInAppTutorialShortHeader: (tutorialId: string) => ({
id: 'flingGame',
contentUrl: 'fakeUrl',
availableLocales: ['en', 'fr-FR'],
}),
currentlyRunningInAppTutorial: null,
startTutorial: async () => {
action('start tutorial');
},
startProjectData: {},
endTutorial: () => {
action('end tutorial');
},
startStepIndex: 0,
inAppTutorialsFetchingError,
fetchInAppTutorials: async () => {
action('fetch tutorials')();
},
onLoadInAppTutorialFromLocalFile: async () => {
action('load in app tutorial from local file')();
},
}}
>
<AuthenticatedUserContext.Provider value={user}>
<ExampleStoreStateProvider>
<TutorialStateProvider>
<HomePage
project={project}
fileMetadata={null}
isActive={true}
projectItemName={null}
setToolbar={() => {}}
canOpen={true}
storageProviders={[CloudStorageProvider]}
onChooseProject={() => action('onChooseProject')()}
onOpenRecentFile={() => action('onOpenRecentFile')()}
onOpenExampleStore={() => action('onOpenExampleStore')()}
onSelectExampleShortHeader={() =>
action('onSelectExampleShortHeader')()
}
onPreviewPrivateGameTemplateListingData={() =>
action('onPreviewPrivateGameTemplateListingData')()
}
onOpenPrivateGameTemplateListingData={() =>
action('onOpenPrivateGameTemplateListingData')()
}
onOpenProjectManager={() => action('onOpenProjectManager')()}
onOpenLanguageDialog={() => action('open language dialog')()}
onOpenNewProjectSetupDialog={() =>
action('onOpenNewProjectSetupDialog')()
}
canSave={true}
onSave={() => action('onSave')()}
selectInAppTutorial={() => action('select in app tutorial')()}
onOpenProfile={() => action('open profile')()}
onOpenPreferences={() => action('open preferences')()}
onOpenAbout={() => action('open about')()}
resourceManagementProps={fakeResourceManagementProps}
canInstallPrivateAsset={() => true}
/>
</TutorialStateProvider>
</ExampleStoreStateProvider>
</AuthenticatedUserContext.Provider>
</InAppTutorialContext.Provider>
<AuthenticatedUserContext.Provider value={user}>
<ExampleStoreStateProvider>
<TutorialStateProvider>
<HomePage
project={project}
fileMetadata={null}
isActive={true}
projectItemName={null}
setToolbar={() => {}}
canOpen={true}
storageProviders={[CloudStorageProvider]}
onChooseProject={() => action('onChooseProject')()}
onOpenRecentFile={() => action('onOpenRecentFile')()}
onOpenExampleStore={() => action('onOpenExampleStore')()}
onSelectExampleShortHeader={() =>
action('onSelectExampleShortHeader')()
}
onPreviewPrivateGameTemplateListingData={() =>
action('onPreviewPrivateGameTemplateListingData')()
}
onOpenPrivateGameTemplateListingData={() =>
action('onOpenPrivateGameTemplateListingData')()
}
onOpenProjectManager={() => action('onOpenProjectManager')()}
onOpenLanguageDialog={() => action('open language dialog')()}
onOpenNewProjectSetupDialog={() =>
action('onOpenNewProjectSetupDialog')()
}
canSave={true}
onSave={() => action('onSave')()}
selectInAppTutorial={() => action('select in app tutorial')()}
onOpenProfile={() => action('open profile')()}
onOpenPreferences={() => action('open preferences')()}
onOpenAbout={() => action('open about')()}
resourceManagementProps={fakeResourceManagementProps}
canInstallPrivateAsset={() => true}
/>
</TutorialStateProvider>
</ExampleStoreStateProvider>
</AuthenticatedUserContext.Provider>
</PreferencesContext.Provider>
</FixedHeightFlexContainer>
);
@@ -160,7 +127,7 @@ const WrappedHomePage = ({
export default {
title: 'HomePage',
component: WrappedHomePage,
decorators: [GDevelopJsInitializerDecorator],
decorators: [GDevelopJsInitializerDecorator, inAppTutorialDecorator],
};
export const BuildSectionLoading = () => (

View File

@@ -12,7 +12,7 @@ export default {
decorators: [inAppTutorialDecorator, paperDecorator],
};
export const Default = () => {
export const MultichapterTutorial = () => {
return (
<StartInAppTutorialDialog
open
@@ -25,6 +25,19 @@ export const Default = () => {
);
};
export const Default = () => {
return (
<StartInAppTutorialDialog
open
tutorialId="joystick"
tutorialCompletionStatus={'notStarted'}
startTutorial={action('Start tutorial')}
onClose={() => action('On close dialog')()}
isProjectOpening={false}
/>
);
};
export const Opening = () => {
return (
<StartInAppTutorialDialog

View File

@@ -6,7 +6,7 @@ import paperDecorator from '../../PaperDecorator';
import {
LeaderboardReplacerProgressDialog,
ReplacePromptDialog,
} from '../../../Leaderboard/useLeaderboardReplacer';
} from '../../../Leaderboard/UseLeaderboardReplacer';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
import { fakeSilverAuthenticatedUser } from '../../../fixtures/GDevelopServicesTestData';