mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
24 Commits
limit-buil
...
fix-admob-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3476c043f8 | ||
![]() |
e85229d38e | ||
![]() |
e7381a7229 | ||
![]() |
257ba78486 | ||
![]() |
fb3d821368 | ||
![]() |
a5068d8fcf | ||
![]() |
d20b3228cf | ||
![]() |
aae54ef331 | ||
![]() |
caa5105651 | ||
![]() |
5eccdde332 | ||
![]() |
c34d758ccf | ||
![]() |
4e57d6aee9 | ||
![]() |
74d208f736 | ||
![]() |
d88b76becf | ||
![]() |
4a0e2481e6 | ||
![]() |
612b041da4 | ||
![]() |
3df7cd8e80 | ||
![]() |
7ba96ed0ab | ||
![]() |
689cd14947 | ||
![]() |
de50dc7967 | ||
![]() |
1aa2afaf85 | ||
![]() |
9120a52a08 | ||
![]() |
b0ffaebe91 | ||
![]() |
64a4a0b3a1 |
24
Core/GDCore/IDE/Events/InstructionsCountEvaluator.cpp
Normal file
24
Core/GDCore/IDE/Events/InstructionsCountEvaluator.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "InstructionsCountEvaluator.h"
|
||||
|
||||
#include "GDCore/Events/Instruction.h"
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/IDE/WholeProjectRefactorer.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
const int InstructionsCountEvaluator::ScanProject(gd::Project &project) {
|
||||
InstructionsCountEvaluator worker(project);
|
||||
gd::WholeProjectRefactorer::ExposeProjectEventsWithoutExtensions(project,
|
||||
worker);
|
||||
return worker.instructionCount;
|
||||
};
|
||||
|
||||
// Instructions scanner
|
||||
|
||||
bool InstructionsCountEvaluator::DoVisitInstruction(
|
||||
gd::Instruction &instruction, bool isCondition) {
|
||||
instructionCount++;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace gd
|
50
Core/GDCore/IDE/Events/InstructionsCountEvaluator.h
Normal file
50
Core/GDCore/IDE/Events/InstructionsCountEvaluator.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#ifndef GDCORE_INSTRUCTIONS_COUNT_EVALUATOR_H
|
||||
#define GDCORE_INSTRUCTIONS_COUNT_EVALUATOR_H
|
||||
#include <set>
|
||||
|
||||
#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h"
|
||||
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryObjectsWorker.h"
|
||||
#include "GDCore/String.h"
|
||||
|
||||
namespace gd {
|
||||
class Project;
|
||||
class Object;
|
||||
class Behavior;
|
||||
} // namespace gd
|
||||
|
||||
namespace gd {
|
||||
|
||||
/**
|
||||
* @brief Count the number of instructions in a project excluding extensions.
|
||||
*
|
||||
* This is used by the examples repository to evaluate examples size.
|
||||
*
|
||||
*/
|
||||
class GD_CORE_API InstructionsCountEvaluator : public ArbitraryEventsWorker {
|
||||
public:
|
||||
/**
|
||||
* Return the number of instructions in the project excluding extensions.
|
||||
*/
|
||||
static const int ScanProject(gd::Project &project);
|
||||
|
||||
private:
|
||||
InstructionsCountEvaluator(gd::Project &project_)
|
||||
: project(project_), instructionCount(0){};
|
||||
gd::Project &project;
|
||||
int instructionCount;
|
||||
|
||||
// Instructions Visitor
|
||||
bool DoVisitInstruction(gd::Instruction &instruction,
|
||||
bool isCondition) override;
|
||||
};
|
||||
|
||||
}; // namespace gd
|
||||
|
||||
#endif
|
@@ -49,14 +49,8 @@ void WholeProjectRefactorer::ExposeProjectEvents(
|
||||
// See also gd::Project::ExposeResources for a method that traverse the whole
|
||||
// project (this time for resources).
|
||||
|
||||
// Add layouts events
|
||||
for (std::size_t s = 0; s < project.GetLayoutsCount(); s++) {
|
||||
worker.Launch(project.GetLayout(s).GetEvents());
|
||||
}
|
||||
// Add external events events
|
||||
for (std::size_t s = 0; s < project.GetExternalEventsCount(); s++) {
|
||||
worker.Launch(project.GetExternalEvents(s).GetEvents());
|
||||
}
|
||||
ExposeProjectEventsWithoutExtensions(project, worker);
|
||||
|
||||
// Add events based extensions
|
||||
for (std::size_t e = 0; e < project.GetEventsFunctionsExtensionsCount();
|
||||
e++) {
|
||||
@@ -90,6 +84,18 @@ void WholeProjectRefactorer::ExposeProjectEvents(
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::ExposeProjectEventsWithoutExtensions(
|
||||
gd::Project& project, gd::ArbitraryEventsWorker& worker) {
|
||||
// Add layouts events
|
||||
for (std::size_t s = 0; s < project.GetLayoutsCount(); s++) {
|
||||
worker.Launch(project.GetLayout(s).GetEvents());
|
||||
}
|
||||
// Add external events events
|
||||
for (std::size_t s = 0; s < project.GetExternalEventsCount(); s++) {
|
||||
worker.Launch(project.GetExternalEvents(s).GetEvents());
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::ExposeProjectEvents(
|
||||
gd::Project& project, gd::ArbitraryEventsWorkerWithContext& worker) {
|
||||
// See also gd::Project::ExposeResources for a method that traverse the whole
|
||||
|
@@ -47,6 +47,14 @@ class GD_CORE_API WholeProjectRefactorer {
|
||||
*/
|
||||
static void ExposeProjectEvents(gd::Project& project,
|
||||
gd::ArbitraryEventsWorker& worker);
|
||||
/**
|
||||
* \brief Call the specified worker on all events of the project (layout and
|
||||
* external events) but not events from extensions.
|
||||
*
|
||||
* Only use this for stats.
|
||||
*/
|
||||
static void ExposeProjectEventsWithoutExtensions(gd::Project& project,
|
||||
gd::ArbitraryEventsWorker& worker);
|
||||
|
||||
/**
|
||||
* \brief Call the specified worker on all events of the project (layout,
|
||||
|
40
Core/tests/InstructionCountEvaluator.cpp
Normal file
40
Core/tests/InstructionCountEvaluator.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#include "GDCore/IDE/Events/InstructionsCountEvaluator.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "DummyPlatform.h"
|
||||
#include "GDCore/Events/Builtin/StandardEvent.h"
|
||||
#include "GDCore/Events/Event.h"
|
||||
#include "GDCore/Extensions/Platform.h"
|
||||
#include "GDCore/Project/Layout.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "catch.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
TEST_CASE("InstructionsCountEvaluator", "[events]") {
|
||||
|
||||
SECTION("Can count 1 action in a layout") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
auto &layout1 = project.InsertNewLayout("Layout1", 0);
|
||||
|
||||
// Add an event with an action.
|
||||
gd::StandardEvent event;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomething");
|
||||
instruction.SetParametersCount(1);
|
||||
event.GetActions().Insert(instruction);
|
||||
layout1.GetEvents().InsertEvent(event);
|
||||
|
||||
REQUIRE(gd::InstructionsCountEvaluator::ScanProject(project) == 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
@@ -123,6 +123,20 @@ namespace gdjs {
|
||||
false
|
||||
);
|
||||
|
||||
document.addEventListener('pause', async () => {
|
||||
logger.info('App paused, hiding banner if any.');
|
||||
if (banner) {
|
||||
banner.hide();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('resume', async () => {
|
||||
logger.info('App resumed, showing banner again.');
|
||||
if (banner) {
|
||||
banner.show();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to know if we are on mobile and admob is correctly initialized.
|
||||
*/
|
||||
@@ -302,20 +316,46 @@ namespace gdjs {
|
||||
await hideBanner();
|
||||
}
|
||||
|
||||
const bannerId = localStorage.getItem('lastBannerId');
|
||||
|
||||
logger.info(`bannerId ${bannerId}`);
|
||||
|
||||
bannerConfigured = false;
|
||||
bannerLoaded = false;
|
||||
|
||||
if (bannerId) {
|
||||
logger.info('bannerId is set, trying to reuse it');
|
||||
try {
|
||||
const result = admob.BannerAd.getAdById(adUnitId);
|
||||
if (result) {
|
||||
logger.info('bannerId is valid, reusing it');
|
||||
} else {
|
||||
logger.info('result null');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.info(error);
|
||||
logger.info('bannerId is invalid, not reusing it');
|
||||
}
|
||||
}
|
||||
|
||||
banner = new admob.BannerAd({
|
||||
adUnitId,
|
||||
position: atTop ? 'top' : 'bottom',
|
||||
size: bannerRequestedAdSizeType,
|
||||
id: 123,
|
||||
});
|
||||
|
||||
banner.on('load', () => {
|
||||
console.log(banner);
|
||||
console.log(admob);
|
||||
|
||||
banner.on('load', (event) => {
|
||||
logger.info('banner loaded', JSON.stringify(event));
|
||||
bannerShowing = true;
|
||||
bannerLoaded = true;
|
||||
localStorage.setItem('lastBannerId', event.adId);
|
||||
});
|
||||
banner.on('loadfail', () => {
|
||||
banner.on('loadfail', (event) => {
|
||||
logger.info('banner did not load properly', JSON.stringify(event));
|
||||
bannerShowing = false;
|
||||
bannerLoaded = false;
|
||||
bannerErrored = true;
|
||||
|
@@ -686,7 +686,7 @@ namespace gdjs {
|
||||
const isDev = runtimeScene
|
||||
.getGame()
|
||||
.isUsingGDevelopDevelopmentEnvironment();
|
||||
const targetUrl = `https://liluo.io/games/${gameId}/leaderboard/${leaderboardId}?inGameEmbedded=true${
|
||||
const targetUrl = `https://gd.games/games/${gameId}/leaderboard/${leaderboardId}?inGameEmbedded=true${
|
||||
isDev ? '&dev=true' : ''
|
||||
}`;
|
||||
checkLeaderboardAvailability(targetUrl).then(
|
||||
|
@@ -34,6 +34,7 @@ namespace gdjs {
|
||||
): {
|
||||
rootContainer: HTMLDivElement;
|
||||
loaderContainer: HTMLDivElement;
|
||||
iframeContainer: HTMLDivElement;
|
||||
} {
|
||||
const rootContainer = document.createElement('div');
|
||||
rootContainer.id = 'authentication-root-container';
|
||||
@@ -45,17 +46,17 @@ namespace gdjs {
|
||||
rootContainer.style.zIndex = '2';
|
||||
rootContainer.style.pointerEvents = 'all';
|
||||
|
||||
const subContainer = document.createElement('div');
|
||||
subContainer.id = 'authentication-sub-container';
|
||||
subContainer.style.backgroundColor = '#FFFFFF';
|
||||
subContainer.style.position = 'absolute';
|
||||
subContainer.style.top = '16px';
|
||||
subContainer.style.bottom = '16px';
|
||||
subContainer.style.left = '16px';
|
||||
subContainer.style.right = '16px';
|
||||
subContainer.style.borderRadius = '8px';
|
||||
subContainer.style.boxShadow = '0px 4px 4px rgba(0, 0, 0, 0.25)';
|
||||
subContainer.style.padding = '16px';
|
||||
const frameContainer = document.createElement('div');
|
||||
frameContainer.id = 'authentication-frame-container';
|
||||
frameContainer.style.backgroundColor = '#FFFFFF';
|
||||
frameContainer.style.position = 'absolute';
|
||||
frameContainer.style.top = '16px';
|
||||
frameContainer.style.bottom = '16px';
|
||||
frameContainer.style.left = '16px';
|
||||
frameContainer.style.right = '16px';
|
||||
frameContainer.style.borderRadius = '8px';
|
||||
frameContainer.style.boxShadow = '0px 4px 4px rgba(0, 0, 0, 0.25)';
|
||||
frameContainer.style.padding = '16px';
|
||||
|
||||
const _closeContainer: HTMLDivElement = document.createElement('div');
|
||||
_closeContainer.style.cursor = 'pointer';
|
||||
@@ -106,11 +107,57 @@ namespace gdjs {
|
||||
|
||||
loaderContainer.appendChild(_loader);
|
||||
|
||||
subContainer.appendChild(_closeContainer);
|
||||
subContainer.appendChild(loaderContainer);
|
||||
rootContainer.appendChild(subContainer);
|
||||
const iframeContainer: HTMLDivElement = document.createElement('div');
|
||||
iframeContainer.style.display = 'flex';
|
||||
iframeContainer.style.flexDirection = 'column';
|
||||
iframeContainer.style.height = '100%';
|
||||
iframeContainer.style.width = '100%';
|
||||
iframeContainer.style.justifyContent = 'stretch';
|
||||
iframeContainer.style.alignItems = 'stretch';
|
||||
iframeContainer.style.display = 'none';
|
||||
|
||||
return { rootContainer, loaderContainer };
|
||||
frameContainer.appendChild(_closeContainer);
|
||||
frameContainer.appendChild(loaderContainer);
|
||||
frameContainer.appendChild(iframeContainer);
|
||||
rootContainer.appendChild(frameContainer);
|
||||
|
||||
return { rootContainer, loaderContainer, iframeContainer };
|
||||
};
|
||||
|
||||
export const displayIframeInsideAuthenticationContainer = (
|
||||
iframeContainer: HTMLDivElement,
|
||||
loaderContainer: HTMLDivElement,
|
||||
textContainer: HTMLDivElement,
|
||||
url: string
|
||||
) => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute(
|
||||
'sandbox',
|
||||
'allow-forms allow-modals allow-orientation-lock allow-popups allow-same-origin allow-scripts'
|
||||
);
|
||||
iframe.addEventListener('load', () => {
|
||||
iframeContainer.style.display = 'flex';
|
||||
loaderContainer.style.display = 'none';
|
||||
});
|
||||
iframe.addEventListener('loaderror', () => {
|
||||
addAuthenticationUrlToTextsContainer(() => {
|
||||
// Try again.
|
||||
iframeContainer.removeChild(iframe);
|
||||
iframeContainer.style.display = 'none';
|
||||
loaderContainer.style.display = 'flex';
|
||||
displayIframeInsideAuthenticationContainer(
|
||||
iframeContainer,
|
||||
loaderContainer,
|
||||
textContainer,
|
||||
url
|
||||
);
|
||||
}, textContainer);
|
||||
});
|
||||
iframe.src = url;
|
||||
iframe.style.flex = '1';
|
||||
iframe.style.border = '0';
|
||||
|
||||
iframeContainer.appendChild(iframe);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -18,6 +18,7 @@ namespace gdjs {
|
||||
let _authenticationInAppWindow: Window | null = null; // For Cordova.
|
||||
let _authenticationRootContainer: HTMLDivElement | null = null;
|
||||
let _authenticationLoaderContainer: HTMLDivElement | null = null;
|
||||
let _authenticationIframeContainer: HTMLDivElement | null = null;
|
||||
let _authenticationTextContainer: HTMLDivElement | null = null;
|
||||
let _authenticationBanner: HTMLDivElement | null = null;
|
||||
let _initialAuthenticationTimeoutId: NodeJS.Timeout | null = null;
|
||||
@@ -38,12 +39,12 @@ namespace gdjs {
|
||||
});
|
||||
|
||||
// If the extension is used, register an eventlistener to know if the user is
|
||||
// logged in while playing the game on Liluo.io.
|
||||
// logged in while playing the game on GDevelop games platform.
|
||||
// Then send a message to the parent iframe to say that the player auth is ready.
|
||||
gdjs.registerFirstRuntimeSceneLoadedCallback(
|
||||
(runtimeScene: RuntimeScene) => {
|
||||
if (getPlatform(runtimeScene) !== 'web') {
|
||||
// Automatic authentication is only valid when the game is hosted in Liluo.io.
|
||||
// Automatic authentication is only valid when the game is hosted on GDevelop games platform.
|
||||
return;
|
||||
}
|
||||
removeAuthenticationCallbacks(); // Remove any callback that could have been registered before.
|
||||
@@ -64,9 +65,9 @@ namespace gdjs {
|
||||
{
|
||||
id: 'playerAuthReady',
|
||||
},
|
||||
'*' // We could restrict to liluo.io but it's not necessary as the message is not sensitive, and it allows easy debugging.
|
||||
'*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.
|
||||
);
|
||||
// If no answer after 3 seconds, assume that the game is not embedded in Liluo.io, and remove the listener.
|
||||
// If no answer after 3 seconds, assume that the game is not embedded in GDevelop games platform, and remove the listener.
|
||||
_initialAuthenticationTimeoutId = setTimeout(() => {
|
||||
logger.info('Removing initial authentication listener.');
|
||||
removeAuthenticationCallbacks();
|
||||
@@ -85,12 +86,16 @@ namespace gdjs {
|
||||
runtimeGame: gdjs.RuntimeGame;
|
||||
gameId: string;
|
||||
connectionId?: string;
|
||||
}) =>
|
||||
`https://liluo.io/auth?gameId=${gameId}${
|
||||
}) => {
|
||||
// Uncomment to test the case of a failing loading:
|
||||
// return 'https://gd.games.wronglink';
|
||||
|
||||
return `https://gd.games/auth?gameId=${gameId}${
|
||||
connectionId ? `&connectionId=${connectionId}` : ''
|
||||
}${
|
||||
runtimeGame.isUsingGDevelopDevelopmentEnvironment() ? '&dev=true' : ''
|
||||
}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper returning the platform.
|
||||
@@ -98,14 +103,31 @@ namespace gdjs {
|
||||
const getPlatform = (
|
||||
runtimeScene: RuntimeScene
|
||||
): 'electron' | 'cordova' | 'web' => {
|
||||
const electron = runtimeScene.getGame().getRenderer().getElectron();
|
||||
const runtimeGame = runtimeScene.getGame();
|
||||
const electron = runtimeGame.getRenderer().getElectron();
|
||||
if (electron) {
|
||||
return 'electron';
|
||||
}
|
||||
if (typeof cordova !== 'undefined') return 'cordova';
|
||||
|
||||
return 'web';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if, in some exceptional cases, we allow authentication
|
||||
* to be done through a iframe.
|
||||
* This is usually discouraged as the user can't verify that the authentication
|
||||
* window is a genuine one. It's only to be used in trusted contexts.
|
||||
*/
|
||||
const shouldAuthenticationUseIframe = (runtimeScene: RuntimeScene) => {
|
||||
const runtimeGameOptions = runtimeScene.getGame().getAdditionalOptions();
|
||||
return (
|
||||
runtimeGameOptions &&
|
||||
runtimeGameOptions.isPreview &&
|
||||
runtimeGameOptions.allowAuthenticationUsingIframeForPreview
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if a user token is present in the local storage.
|
||||
*/
|
||||
@@ -509,7 +531,7 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Helper to recompute the authentication banner.
|
||||
* This is useful if the user is already logged in on Liluo.io
|
||||
* This is useful if the user is already logged on GDevelop games platform
|
||||
* and we want to display the banner with the username.
|
||||
*/
|
||||
const refreshAuthenticationBannerIfAny = function (
|
||||
@@ -685,6 +707,47 @@ namespace gdjs {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to handle authentication iframe on web.
|
||||
* We open an iframe, and listen to messages posted back to the game window.
|
||||
*/
|
||||
const openAuthenticationIframeForWeb = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
gameId: string
|
||||
) => {
|
||||
if (
|
||||
!_authenticationIframeContainer ||
|
||||
!_authenticationLoaderContainer ||
|
||||
!_authenticationTextContainer
|
||||
) {
|
||||
console.error(
|
||||
"Can't open an authentication iframe - no iframe container, loader container or text container was opened for it."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
});
|
||||
|
||||
// Listen to messages posted by the authentication window, so that we can
|
||||
// know when the user is authenticated.
|
||||
_authenticationMessageCallback = (event: MessageEvent) => {
|
||||
receiveAuthenticationMessage(runtimeScene, event, {
|
||||
checkOrigin: true,
|
||||
});
|
||||
};
|
||||
window.addEventListener('message', _authenticationMessageCallback, true);
|
||||
|
||||
authComponents.displayIframeInsideAuthenticationContainer(
|
||||
_authenticationIframeContainer,
|
||||
_authenticationLoaderContainer,
|
||||
_authenticationTextContainer,
|
||||
targetUrl
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to display the authentication window to the user.
|
||||
*/
|
||||
@@ -725,11 +788,13 @@ namespace gdjs {
|
||||
const {
|
||||
rootContainer,
|
||||
loaderContainer,
|
||||
iframeContainer,
|
||||
} = authComponents.computeAuthenticationContainer(
|
||||
onAuthenticationContainerDismissed
|
||||
);
|
||||
_authenticationRootContainer = rootContainer;
|
||||
_authenticationLoaderContainer = loaderContainer;
|
||||
_authenticationIframeContainer = iframeContainer;
|
||||
|
||||
// Display the authentication window right away, to show a loader
|
||||
// while the call for game registration is happening.
|
||||
@@ -769,7 +834,11 @@ namespace gdjs {
|
||||
break;
|
||||
case 'web':
|
||||
default:
|
||||
openAuthenticationWindowForWeb(runtimeScene, _gameId);
|
||||
if (shouldAuthenticationUseIframe(runtimeScene)) {
|
||||
openAuthenticationIframeForWeb(runtimeScene, _gameId);
|
||||
} else {
|
||||
openAuthenticationWindowForWeb(runtimeScene, _gameId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -815,6 +884,7 @@ namespace gdjs {
|
||||
|
||||
_authenticationRootContainer = null;
|
||||
_authenticationLoaderContainer = null;
|
||||
_authenticationIframeContainer = null;
|
||||
_authenticationTextContainer = null;
|
||||
};
|
||||
|
||||
|
@@ -722,7 +722,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
_("Center of rotation"),
|
||||
_("Change the center of rotation of an object relatively to the "
|
||||
"object origin."),
|
||||
_("Change the center of rotation of _PARAM0_: _PARAM1_; _PARAM2_"),
|
||||
_("Change the center of rotation of _PARAM0_ to _PARAM1_, _PARAM2_"),
|
||||
_("Angle"),
|
||||
"res/actions/position24_black.png",
|
||||
"res/actions/position_black.png")
|
||||
|
@@ -474,7 +474,7 @@ namespace gdjs {
|
||||
/**
|
||||
* The center of rotation is defined relatively
|
||||
* to the drawing origin (the object position).
|
||||
* This avoid the center to move on the drawing
|
||||
* This avoids the center to move on the drawing
|
||||
* when new shapes push the bounds.
|
||||
*
|
||||
* When no custom center is defined, it will move
|
||||
|
@@ -855,9 +855,9 @@ module.exports = {
|
||||
.addParameter('expression', _('To Hue'), '', false)
|
||||
.addParameter('yesorno', _('Animate Hue'), '', false)
|
||||
.setDefaultValue('yes')
|
||||
.addParameter('expression', _('To Saturation (-1 to ignore)'), '', false)
|
||||
.addParameter('expression', _('To Saturation (0 to 100, -1 to ignore)'), '', false)
|
||||
.setDefaultValue('-1')
|
||||
.addParameter('expression', _('To Lightness (-1 to ignore)'), '', false)
|
||||
.addParameter('expression', _('To Lightness (0 to 100, -1 to ignore)'), '', false)
|
||||
.setDefaultValue('-1')
|
||||
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
|
||||
.setDefaultValue('linear')
|
||||
|
@@ -190,6 +190,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
|
||||
runtimeGameOptions.AddChild("gdevelopResourceToken")
|
||||
.SetStringValue(options.gdevelopResourceToken);
|
||||
}
|
||||
runtimeGameOptions.AddChild("allowAuthenticationUsingIframeForPreview")
|
||||
.SetBoolValue(options.allowAuthenticationUsingIframeForPreview);
|
||||
|
||||
// Pass in the options the list of scripts files - useful for hot-reloading.
|
||||
auto &scriptFilesElement = runtimeGameOptions.AddChild("scriptFiles");
|
||||
@@ -589,8 +591,9 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
||||
InsertUnique(includesFiles, "runtimescene.js");
|
||||
InsertUnique(includesFiles, "scenestack.js");
|
||||
InsertUnique(includesFiles, "force.js");
|
||||
InsertUnique(includesFiles, "RuntimeLayer.js");
|
||||
InsertUnique(includesFiles, "layer.js");
|
||||
InsertUnique(includesFiles, "RuntimeSceneLayer.js");
|
||||
InsertUnique(includesFiles, "RuntimeCustomObjectLayer.js");
|
||||
InsertUnique(includesFiles, "timer.js");
|
||||
InsertUnique(includesFiles, "runtimewatermark.js");
|
||||
InsertUnique(includesFiles, "runtimegame.js");
|
||||
|
@@ -40,7 +40,8 @@ struct PreviewExportOptions {
|
||||
isDevelopmentEnvironment(false),
|
||||
nonRuntimeScriptsCacheBurst(0),
|
||||
fallbackAuthorId(""),
|
||||
fallbackAuthorUsername(""){};
|
||||
fallbackAuthorUsername(""),
|
||||
allowAuthenticationUsingIframeForPreview(false){};
|
||||
|
||||
/**
|
||||
* \brief Set the address of the debugger server that the game should reach
|
||||
@@ -160,6 +161,20 @@ struct PreviewExportOptions {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if, in some exceptional cases, we allow authentication
|
||||
* to be done through a iframe.
|
||||
* This is usually discouraged as the user can't verify that the
|
||||
* authentication window is a genuine one. It's only to be used in trusted
|
||||
* contexts.
|
||||
*/
|
||||
PreviewExportOptions &SetAllowAuthenticationUsingIframeForPreview(
|
||||
bool allowAuthenticationUsingIframeForPreview_) {
|
||||
allowAuthenticationUsingIframeForPreview =
|
||||
allowAuthenticationUsingIframeForPreview_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
gd::Project &project;
|
||||
gd::String exportPath;
|
||||
gd::String websocketDebuggerServerAddress;
|
||||
@@ -176,6 +191,7 @@ struct PreviewExportOptions {
|
||||
unsigned int nonRuntimeScriptsCacheBurst;
|
||||
gd::String electronRemoteRequirePath;
|
||||
gd::String gdevelopResourceToken;
|
||||
bool allowAuthenticationUsingIframeForPreview;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -27,11 +27,15 @@ namespace gdjs {
|
||||
_untransformedHitBoxes: gdjs.Polygon[] = [];
|
||||
/** The dimension of this object is calculated from its children AABBs. */
|
||||
_unrotatedAABB: AABB = { min: [0, 0], max: [0, 0] };
|
||||
_scaleX: number = 1;
|
||||
_scaleY: number = 1;
|
||||
_scaleX: float = 1;
|
||||
_scaleY: float = 1;
|
||||
_flippedX: boolean = false;
|
||||
_flippedY: boolean = false;
|
||||
opacity: float = 255;
|
||||
_customCenter: FloatPoint | null = null;
|
||||
_localTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
|
||||
_localInverseTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
|
||||
_isLocalTransformationDirty: boolean = true;
|
||||
|
||||
/**
|
||||
* @param parent The container the object belongs to
|
||||
@@ -149,8 +153,9 @@ namespace gdjs {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
|
||||
//Update the current hitboxes with the frame custom hit boxes
|
||||
//and apply transformations.
|
||||
// Update the current hitboxes with the frame custom hit boxes
|
||||
// and apply transformations.
|
||||
const localTransformation = this.getLocalTransformation();
|
||||
for (let i = 0; i < this._untransformedHitBoxes.length; ++i) {
|
||||
if (i >= this.hitBoxes.length) {
|
||||
this.hitBoxes.push(new gdjs.Polygon());
|
||||
@@ -163,9 +168,8 @@ namespace gdjs {
|
||||
if (j >= this.hitBoxes[i].vertices.length) {
|
||||
this.hitBoxes[i].vertices.push([0, 0]);
|
||||
}
|
||||
this.applyObjectTransformation(
|
||||
this._untransformedHitBoxes[i].vertices[j][0],
|
||||
this._untransformedHitBoxes[i].vertices[j][1],
|
||||
localTransformation.transform(
|
||||
this._untransformedHitBoxes[i].vertices[j],
|
||||
this.hitBoxes[i].vertices[j]
|
||||
);
|
||||
}
|
||||
@@ -181,11 +185,6 @@ namespace gdjs {
|
||||
_updateUntransformedHitBoxes() {
|
||||
this._isUntransformedHitBoxesDirty = false;
|
||||
|
||||
const oldUnrotatedMinX = this._unrotatedAABB.min[0];
|
||||
const oldUnrotatedMinY = this._unrotatedAABB.min[1];
|
||||
const oldUnrotatedMaxX = this._unrotatedAABB.max[0];
|
||||
const oldUnrotatedMaxY = this._unrotatedAABB.max[1];
|
||||
|
||||
this._untransformedHitBoxes.length = 0;
|
||||
if (this._instanceContainer.getAdhocListOfAllInstances().length === 0) {
|
||||
this._unrotatedAABB.min[0] = 0;
|
||||
@@ -218,20 +217,6 @@ namespace gdjs {
|
||||
}
|
||||
this.hitBoxes.length = this._untransformedHitBoxes.length;
|
||||
}
|
||||
|
||||
// The default camera center depends on the object dimensions so checking
|
||||
// the AABB center is not enough.
|
||||
if (
|
||||
this._unrotatedAABB.min[0] !== oldUnrotatedMinX ||
|
||||
this._unrotatedAABB.min[1] !== oldUnrotatedMinY ||
|
||||
this._unrotatedAABB.max[0] !== oldUnrotatedMaxX ||
|
||||
this._unrotatedAABB.max[1] !== oldUnrotatedMaxY
|
||||
) {
|
||||
this._instanceContainer.onObjectUnscaledCenterChanged(
|
||||
(oldUnrotatedMinX + oldUnrotatedMaxX) / 2,
|
||||
(oldUnrotatedMinY + oldUnrotatedMaxY) / 2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Position:
|
||||
@@ -246,37 +231,52 @@ namespace gdjs {
|
||||
* @param result Array that will be updated with the result
|
||||
* (x and y position of the point in parent coordinates).
|
||||
*/
|
||||
applyObjectTransformation(x: float, y: float, result: number[]) {
|
||||
let cx = this.getCenterX();
|
||||
let cy = this.getCenterY();
|
||||
applyObjectTransformation(x: float, y: float, destination: FloatPoint) {
|
||||
const source = destination;
|
||||
source[0] = x;
|
||||
source[1] = y;
|
||||
this.getLocalTransformation().transform(source, destination);
|
||||
}
|
||||
|
||||
// Flipping
|
||||
if (this._flippedX) {
|
||||
x = x + (cx - x) * 2;
|
||||
}
|
||||
if (this._flippedY) {
|
||||
y = y + (cy - y) * 2;
|
||||
/**
|
||||
* Return the affine transformation that represents
|
||||
* flipping, scale, rotation and translation of the object.
|
||||
* @returns the affine transformation.
|
||||
*/
|
||||
getLocalTransformation(): gdjs.AffineTransformation {
|
||||
if (this._isLocalTransformationDirty) {
|
||||
this._updateLocalTransformation();
|
||||
}
|
||||
return this._localTransformation;
|
||||
}
|
||||
|
||||
// Scale
|
||||
getLocalInverseTransformation(): gdjs.AffineTransformation {
|
||||
if (this._isLocalTransformationDirty) {
|
||||
this._updateLocalTransformation();
|
||||
}
|
||||
return this._localInverseTransformation;
|
||||
}
|
||||
|
||||
_updateLocalTransformation() {
|
||||
const absScaleX = Math.abs(this._scaleX);
|
||||
const absScaleY = Math.abs(this._scaleY);
|
||||
x *= absScaleX;
|
||||
y *= absScaleY;
|
||||
cx *= absScaleX;
|
||||
cy *= absScaleY;
|
||||
const centerX = this.getUnscaledCenterX() * absScaleX;
|
||||
const centerY = this.getUnscaledCenterY() * absScaleY;
|
||||
const angleInRadians = (this.angle * Math.PI) / 180;
|
||||
|
||||
// Rotation
|
||||
const angleInRadians = (this.angle / 180) * Math.PI;
|
||||
const cosValue = Math.cos(angleInRadians);
|
||||
const sinValue = Math.sin(angleInRadians);
|
||||
const xToCenterXDelta = x - cx;
|
||||
const yToCenterYDelta = y - cy;
|
||||
x = cx + cosValue * xToCenterXDelta - sinValue * yToCenterYDelta;
|
||||
y = cy + sinValue * xToCenterXDelta + cosValue * yToCenterYDelta;
|
||||
result.length = 2;
|
||||
result[0] = x + this.x;
|
||||
result[1] = y + this.y;
|
||||
this._localTransformation.setToTranslation(this.x, this.y);
|
||||
this._localTransformation.rotateAround(angleInRadians, centerX, centerY);
|
||||
if (this._flippedX) {
|
||||
this._localTransformation.flipX(centerX);
|
||||
}
|
||||
if (this._flippedY) {
|
||||
this._localTransformation.flipY(centerY);
|
||||
}
|
||||
this._localTransformation.scale(absScaleX, absScaleY);
|
||||
|
||||
this._localInverseTransformation.copyFrom(this._localTransformation);
|
||||
this._localInverseTransformation.invert();
|
||||
this._isLocalTransformationDirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,53 +290,51 @@ namespace gdjs {
|
||||
* @param result Array that will be updated with the result
|
||||
* (x and y position of the point in object coordinates).
|
||||
*/
|
||||
applyObjectInverseTransformation(x: float, y: float, result: number[]) {
|
||||
x -= this.getCenterXInScene();
|
||||
y -= this.getCenterYInScene();
|
||||
|
||||
const absScaleX = Math.abs(this._scaleX);
|
||||
const absScaleY = Math.abs(this._scaleY);
|
||||
|
||||
// Rotation
|
||||
const angleInRadians = (this.angle / 180) * Math.PI;
|
||||
const cosValue = Math.cos(-angleInRadians);
|
||||
const sinValue = Math.sin(-angleInRadians);
|
||||
const oldX = x;
|
||||
x = cosValue * x - sinValue * y;
|
||||
y = sinValue * oldX + cosValue * y;
|
||||
|
||||
// Scale
|
||||
x /= absScaleX;
|
||||
y /= absScaleY;
|
||||
|
||||
// Flipping
|
||||
if (this._flippedX) {
|
||||
x = -x;
|
||||
}
|
||||
if (this._flippedY) {
|
||||
y = -y;
|
||||
}
|
||||
|
||||
const positionToCenterX =
|
||||
this.getUnscaledWidth() / 2 + this._unrotatedAABB.min[0];
|
||||
const positionToCenterY =
|
||||
this.getUnscaledHeight() / 2 + this._unrotatedAABB.min[1];
|
||||
result[0] = x + positionToCenterX;
|
||||
result[1] = y + positionToCenterY;
|
||||
applyObjectInverseTransformation(
|
||||
x: float,
|
||||
y: float,
|
||||
destination: FloatPoint
|
||||
) {
|
||||
const source = destination;
|
||||
source[0] = x;
|
||||
source[1] = y;
|
||||
this.getLocalInverseTransformation().transform(source, destination);
|
||||
}
|
||||
|
||||
getDrawableX(): float {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return this.x + this._unrotatedAABB.min[0] * this._scaleX;
|
||||
const absScaleX = this.getScaleX();
|
||||
if (!this._flippedX) {
|
||||
return this.x + this._unrotatedAABB.min[0] * absScaleX;
|
||||
} else {
|
||||
return (
|
||||
this.x +
|
||||
(-this._unrotatedAABB.min[0] -
|
||||
this.getUnscaledWidth() +
|
||||
2 * this.getUnscaledCenterX()) *
|
||||
absScaleX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getDrawableY(): float {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return this.y + this._unrotatedAABB.min[1] * this._scaleY;
|
||||
const absScaleY = this.getScaleY();
|
||||
if (!this._flippedY) {
|
||||
return this.y + this._unrotatedAABB.min[1] * absScaleY;
|
||||
} else {
|
||||
return (
|
||||
this.y +
|
||||
(-this._unrotatedAABB.min[1] -
|
||||
this.getUnscaledHeight() +
|
||||
2 * this.getUnscaledCenterY()) *
|
||||
absScaleY
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,6 +361,9 @@ namespace gdjs {
|
||||
* @returns the center X from the local origin (0;0).
|
||||
*/
|
||||
getUnscaledCenterX(): float {
|
||||
if (this._customCenter) {
|
||||
return this._customCenter[0];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
@@ -373,12 +374,57 @@ namespace gdjs {
|
||||
* @returns the center Y from the local origin (0;0).
|
||||
*/
|
||||
getUnscaledCenterY(): float {
|
||||
if (this._customCenter) {
|
||||
return this._customCenter[1];
|
||||
}
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return (this._unrotatedAABB.min[1] + this._unrotatedAABB.max[1]) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* The center of rotation is defined relatively to the origin (the object
|
||||
* position).
|
||||
* This avoids the center to move when children push the bounds.
|
||||
*
|
||||
* When no custom center is defined, it will move
|
||||
* to stay at the center of the children bounds.
|
||||
*
|
||||
* @param x coordinate of the custom center
|
||||
* @param y coordinate of the custom center
|
||||
*/
|
||||
setRotationCenter(x: float, y: float) {
|
||||
if (!this._customCenter) {
|
||||
this._customCenter = [0, 0];
|
||||
}
|
||||
this._customCenter[0] = x;
|
||||
this._customCenter[1] = y;
|
||||
|
||||
this._isLocalTransformationDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
getCenterX(): float {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return (
|
||||
(this.getUnscaledCenterX() - this._unrotatedAABB.min[0]) *
|
||||
this.getScaleX()
|
||||
);
|
||||
}
|
||||
|
||||
getCenterY(): float {
|
||||
if (this._isUntransformedHitBoxesDirty) {
|
||||
this._updateUntransformedHitBoxes();
|
||||
}
|
||||
return (
|
||||
(this.getUnscaledCenterY() - this._unrotatedAABB.min[1]) *
|
||||
this.getScaleY()
|
||||
);
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this.getUnscaledWidth() * this.getScaleX();
|
||||
}
|
||||
@@ -417,6 +463,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
this.x = x;
|
||||
this._isLocalTransformationDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
this.getRenderer().updateX();
|
||||
}
|
||||
@@ -426,6 +473,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
this.y = y;
|
||||
this._isLocalTransformationDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
this.getRenderer().updateY();
|
||||
}
|
||||
@@ -435,6 +483,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
this.angle = angle;
|
||||
this._isLocalTransformationDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
this.getRenderer().updateAngle();
|
||||
}
|
||||
@@ -456,6 +505,7 @@ namespace gdjs {
|
||||
}
|
||||
this._scaleX = newScale * (this._flippedX ? -1 : 1);
|
||||
this._scaleY = newScale * (this._flippedY ? -1 : 1);
|
||||
this._isLocalTransformationDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
this.getRenderer().update();
|
||||
}
|
||||
@@ -473,6 +523,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
this._scaleX = newScale * (this._flippedX ? -1 : 1);
|
||||
this._isLocalTransformationDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
this.getRenderer().update();
|
||||
}
|
||||
|
@@ -43,6 +43,13 @@ namespace gdjs {
|
||||
this._debuggerRenderer = new gdjs.DebuggerRenderer(this);
|
||||
}
|
||||
|
||||
addLayer(layerData: LayerData) {
|
||||
this._layers.put(
|
||||
layerData.name,
|
||||
new gdjs.RuntimeCustomObjectLayer(layerData, this)
|
||||
);
|
||||
}
|
||||
|
||||
createObject(objectName: string): gdjs.RuntimeObject | null {
|
||||
const result = super.createObject(objectName);
|
||||
this._customObject.onChildrenLocationChanged();
|
||||
@@ -293,21 +300,6 @@ namespace gdjs {
|
||||
this._customObject.onChildrenLocationChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the object dimensions are changed.
|
||||
*
|
||||
* It adapts the layers camera positions.
|
||||
*/
|
||||
onObjectUnscaledCenterChanged(oldOriginX: float, oldOriginY: float): void {
|
||||
for (const name in this._layers.items) {
|
||||
if (this._layers.items.hasOwnProperty(name)) {
|
||||
/** @type gdjs.Layer */
|
||||
const theLayer: gdjs.Layer = this._layers.items[name];
|
||||
theLayer.onGameResolutionResized(oldOriginX, oldOriginY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
convertCoords(x: float, y: float, result: FloatPoint): FloatPoint {
|
||||
// The result parameter used to be optional.
|
||||
let position = result || [0, 0];
|
||||
|
104
GDJS/Runtime/RuntimeCustomObjectLayer.ts
Normal file
104
GDJS/Runtime/RuntimeCustomObjectLayer.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* GDevelop JS Platform
|
||||
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
/**
|
||||
* Represents a layer of a custom object. It doesn't allow to move any camera
|
||||
* because it doesn't make sense inside an object.
|
||||
*/
|
||||
export class RuntimeCustomObjectLayer extends gdjs.RuntimeLayer {
|
||||
/**
|
||||
* @param layerData The data used to initialize the layer
|
||||
* @param instanceContainer The container in which the layer is used
|
||||
*/
|
||||
constructor(
|
||||
layerData: LayerData,
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
) {
|
||||
super(layerData, instanceContainer);
|
||||
}
|
||||
|
||||
onGameResolutionResized(
|
||||
oldGameResolutionOriginX: float,
|
||||
oldGameResolutionOriginY: float
|
||||
): void {}
|
||||
|
||||
getCameraX(cameraId?: integer): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getCameraY(cameraId?: integer): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
setCameraX(x: float, cameraId?: integer): void {}
|
||||
|
||||
setCameraY(y: float, cameraId?: integer): void {}
|
||||
|
||||
getCameraWidth(cameraId?: integer): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getCameraHeight(cameraId?: integer): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
setCameraZoom(newZoom: float, cameraId?: integer): void {}
|
||||
|
||||
getCameraZoom(cameraId?: integer): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getCameraRotation(cameraId?: integer): float {
|
||||
return 0;
|
||||
}
|
||||
|
||||
setCameraRotation(rotation: float, cameraId?: integer): void {}
|
||||
|
||||
convertCoords(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer,
|
||||
result: FloatPoint
|
||||
): FloatPoint {
|
||||
// TODO EBO use an AffineTransformation to avoid chained calls.
|
||||
// The result parameter used to be optional.
|
||||
return this._runtimeScene.convertCoords(x, y, result || [0, 0]);
|
||||
}
|
||||
|
||||
convertInverseCoords(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer,
|
||||
result: FloatPoint
|
||||
): FloatPoint {
|
||||
// TODO EBO use an AffineTransformation to avoid chained calls.
|
||||
// The result parameter used to be optional.
|
||||
return this._runtimeScene.convertInverseCoords(x, y, result || [0, 0]);
|
||||
}
|
||||
|
||||
applyLayerInverseTransformation(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer,
|
||||
result: FloatPoint
|
||||
): FloatPoint {
|
||||
result[0] = x;
|
||||
result[1] = y;
|
||||
return result;
|
||||
}
|
||||
|
||||
applyLayerTransformation(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer,
|
||||
result: FloatPoint
|
||||
): FloatPoint {
|
||||
result[0] = x;
|
||||
result[1] = y;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ namespace gdjs {
|
||||
_objects: Hashtable<ObjectData>;
|
||||
_objectsCtor: Hashtable<typeof RuntimeObject>;
|
||||
|
||||
_layers: Hashtable<Layer>;
|
||||
_layers: Hashtable<RuntimeLayer>;
|
||||
_layersCameraCoordinates: Record<string, [float, float, float, float]> = {};
|
||||
|
||||
// Options for the debug draw:
|
||||
@@ -601,7 +601,7 @@ namespace gdjs {
|
||||
* @param name The name of the layer
|
||||
* @returns The layer, or the base layer if not found
|
||||
*/
|
||||
getLayer(name: string): gdjs.Layer {
|
||||
getLayer(name: string): gdjs.RuntimeLayer {
|
||||
if (this._layers.containsKey(name)) {
|
||||
return this._layers.get(name);
|
||||
}
|
||||
@@ -620,9 +620,7 @@ namespace gdjs {
|
||||
* Add a layer.
|
||||
* @param layerData The data to construct the layer
|
||||
*/
|
||||
addLayer(layerData: LayerData) {
|
||||
this._layers.put(layerData.name, new gdjs.Layer(layerData, this));
|
||||
}
|
||||
abstract addLayer(layerData: LayerData);
|
||||
|
||||
/**
|
||||
* Remove a layer. All {@link gdjs.RuntimeObject} on this layer will
|
||||
@@ -647,7 +645,7 @@ namespace gdjs {
|
||||
* @param index The new position in the list of layers
|
||||
*/
|
||||
setLayerIndex(layerName: string, index: integer): void {
|
||||
const layer: gdjs.Layer = this._layers.get(layerName);
|
||||
const layer: gdjs.RuntimeLayer = this._layers.get(layerName);
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
490
GDJS/Runtime/RuntimeLayer.ts
Normal file
490
GDJS/Runtime/RuntimeLayer.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
/*
|
||||
* GDevelop JS Platform
|
||||
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
/**
|
||||
* Represents a layer of a container, used to display objects.
|
||||
*
|
||||
* Viewports and multiple cameras are not supported.
|
||||
*/
|
||||
export abstract class RuntimeLayer implements EffectsTarget {
|
||||
_name: string;
|
||||
_timeScale: float = 1;
|
||||
_defaultZOrder: integer = 0;
|
||||
_hidden: boolean;
|
||||
_initialEffectsData: Array<EffectData>;
|
||||
|
||||
_runtimeScene: gdjs.RuntimeInstanceContainer;
|
||||
_effectsManager: gdjs.EffectsManager;
|
||||
|
||||
// Lighting layer properties.
|
||||
_isLightingLayer: boolean;
|
||||
_followBaseLayerCamera: boolean;
|
||||
_clearColor: Array<integer>;
|
||||
|
||||
_rendererEffects: Record<string, PixiFiltersTools.Filter> = {};
|
||||
_renderer: gdjs.LayerRenderer;
|
||||
|
||||
/**
|
||||
* @param layerData The data used to initialize the layer
|
||||
* @param instanceContainer The container in which the layer is used
|
||||
*/
|
||||
constructor(
|
||||
layerData: LayerData,
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
) {
|
||||
this._name = layerData.name;
|
||||
this._hidden = !layerData.visibility;
|
||||
this._initialEffectsData = layerData.effects || [];
|
||||
this._runtimeScene = instanceContainer;
|
||||
this._effectsManager = instanceContainer.getGame().getEffectsManager();
|
||||
this._isLightingLayer = layerData.isLightingLayer;
|
||||
this._followBaseLayerCamera = layerData.followBaseLayerCamera;
|
||||
this._clearColor = [
|
||||
layerData.ambientLightColorR / 255,
|
||||
layerData.ambientLightColorG / 255,
|
||||
layerData.ambientLightColorB / 255,
|
||||
1.0,
|
||||
];
|
||||
this._renderer = new gdjs.LayerRenderer(
|
||||
this,
|
||||
instanceContainer.getRenderer(),
|
||||
instanceContainer.getGame().getRenderer().getPIXIRenderer()
|
||||
);
|
||||
this.show(!this._hidden);
|
||||
for (let i = 0; i < layerData.effects.length; ++i) {
|
||||
this.addEffect(layerData.effects[i]);
|
||||
}
|
||||
}
|
||||
|
||||
getRenderer(): gdjs.LayerRenderer {
|
||||
return this._renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default Z order to be attributed to objects created on this layer
|
||||
* (usually from events generated code).
|
||||
*/
|
||||
getDefaultZOrder(): float {
|
||||
return this._defaultZOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default Z order to be attributed to objects created on this layer.
|
||||
* @param defaultZOrder The Z order to use when creating a new object from events.
|
||||
*/
|
||||
setDefaultZOrder(defaultZOrder: integer): void {
|
||||
this._defaultZOrder = defaultZOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the RuntimeScene whenever the game resolution size is changed.
|
||||
* Updates the layer width/height and position.
|
||||
*/
|
||||
abstract onGameResolutionResized(
|
||||
oldGameResolutionOriginX: float,
|
||||
oldGameResolutionOriginY: float
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Returns the scene the layer belongs to directly or indirectly
|
||||
* @returns the scene the layer belongs to directly or indirectly
|
||||
*/
|
||||
getRuntimeScene(): gdjs.RuntimeScene {
|
||||
return this._runtimeScene.getScene();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at each frame, after events are run and before rendering.
|
||||
*/
|
||||
updatePreRender(instanceContainer?: gdjs.RuntimeInstanceContainer): void {
|
||||
if (this._followBaseLayerCamera) {
|
||||
this.followBaseLayer();
|
||||
}
|
||||
this._renderer.updatePreRender();
|
||||
this._effectsManager.updatePreRender(this._rendererEffects, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the layer
|
||||
* @return The name of the layer
|
||||
*/
|
||||
getName(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the camera center X position.
|
||||
*
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
* @return The x position of the camera
|
||||
*/
|
||||
abstract getCameraX(cameraId?: integer): float;
|
||||
|
||||
/**
|
||||
* Change the camera center Y position.
|
||||
*
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
* @return The y position of the camera
|
||||
*/
|
||||
abstract getCameraY(cameraId?: integer): float;
|
||||
|
||||
/**
|
||||
* Set the camera center X position.
|
||||
*
|
||||
* @param x The new x position
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
*/
|
||||
abstract setCameraX(x: float, cameraId?: integer): void;
|
||||
|
||||
/**
|
||||
* Set the camera center Y position.
|
||||
*
|
||||
* @param y The new y position
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
*/
|
||||
abstract setCameraY(y: float, cameraId?: integer): void;
|
||||
|
||||
/**
|
||||
* Get the camera width (which can be different than the game resolution width
|
||||
* if the camera is zoomed).
|
||||
*
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
* @return The width of the camera
|
||||
*/
|
||||
abstract getCameraWidth(cameraId?: integer): float;
|
||||
|
||||
/**
|
||||
* Get the camera height (which can be different than the game resolution height
|
||||
* if the camera is zoomed).
|
||||
*
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
* @return The height of the camera
|
||||
*/
|
||||
abstract getCameraHeight(cameraId?: integer): float;
|
||||
|
||||
/**
|
||||
* Show (or hide) the layer.
|
||||
* @param enable true to show the layer, false to hide it.
|
||||
*/
|
||||
show(enable: boolean): void {
|
||||
this._hidden = !enable;
|
||||
this._renderer.updateVisibility(enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the layer is visible.
|
||||
*
|
||||
* @return true if the layer is visible.
|
||||
*/
|
||||
isVisible(): boolean {
|
||||
return !this._hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the zoom of a camera.
|
||||
*
|
||||
* @param newZoom The new zoom. Must be superior to 0. 1 is the default zoom.
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
*/
|
||||
abstract setCameraZoom(newZoom: float, cameraId?: integer): void;
|
||||
|
||||
/**
|
||||
* Get the zoom of a camera.
|
||||
*
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
* @return The zoom.
|
||||
*/
|
||||
abstract getCameraZoom(cameraId?: integer): float;
|
||||
|
||||
/**
|
||||
* Get the rotation of the camera, expressed in degrees.
|
||||
*
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
* @return The rotation, in degrees.
|
||||
*/
|
||||
abstract getCameraRotation(cameraId?: integer): float;
|
||||
|
||||
/**
|
||||
* Set the rotation of the camera, expressed in degrees.
|
||||
* The rotation is made around the camera center.
|
||||
*
|
||||
* @param rotation The new rotation, in degrees.
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
*/
|
||||
abstract setCameraRotation(rotation: float, cameraId?: integer): void;
|
||||
|
||||
/**
|
||||
* Convert a point from the canvas coordinates (for example,
|
||||
* the mouse position) to the container coordinates.
|
||||
*
|
||||
* @param x The x position, in canvas coordinates.
|
||||
* @param y The y position, in canvas coordinates.
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
* @param result The point instance that is used to return the result.
|
||||
*/
|
||||
abstract convertCoords(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer,
|
||||
result: FloatPoint
|
||||
): FloatPoint;
|
||||
|
||||
/**
|
||||
* Return an array containing the coordinates of the point passed as parameter
|
||||
* in parent coordinate coordinates (as opposed to the layer local coordinates).
|
||||
*
|
||||
* All transformations (scale, rotation) are supported.
|
||||
*
|
||||
* @param x The X position of the point, in layer coordinates.
|
||||
* @param y The Y position of the point, in layer coordinates.
|
||||
* @param result Array that will be updated with the result
|
||||
* (x and y position of the point in parent coordinates).
|
||||
*/
|
||||
abstract applyLayerTransformation(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer,
|
||||
result: FloatPoint
|
||||
): FloatPoint;
|
||||
|
||||
/**
|
||||
* Convert a point from the container coordinates (for example,
|
||||
* an object position) to the canvas coordinates.
|
||||
*
|
||||
* @param x The x position, in container coordinates.
|
||||
* @param y The y position, in container coordinates.
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
* @param result The point instance that is used to return the result.
|
||||
*/
|
||||
abstract convertInverseCoords(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer,
|
||||
result: FloatPoint
|
||||
): FloatPoint;
|
||||
|
||||
/**
|
||||
* Return an array containing the coordinates of the point passed as parameter
|
||||
* in layer local coordinates (as opposed to the parent coordinates).
|
||||
*
|
||||
* All transformations (scale, rotation) are supported.
|
||||
*
|
||||
* @param x The X position of the point, in parent coordinates.
|
||||
* @param y The Y position of the point, in parent coordinates.
|
||||
* @param result Array that will be updated with the result
|
||||
* @param result The point instance that is used to return the result.
|
||||
* (x and y position of the point in layer coordinates).
|
||||
*/
|
||||
abstract applyLayerInverseTransformation(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer,
|
||||
result: FloatPoint
|
||||
): FloatPoint;
|
||||
|
||||
getWidth(): float {
|
||||
return this._runtimeScene.getViewportWidth();
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return this._runtimeScene.getViewportHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the initial effects data for the layer. Only to
|
||||
* be used by renderers.
|
||||
* @deprecated
|
||||
*/
|
||||
getInitialEffectsData(): EffectData[] {
|
||||
return this._initialEffectsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new effect, or replace the one with the same name.
|
||||
* @param effectData The data of the effect to add.
|
||||
*/
|
||||
addEffect(effectData: EffectData): void {
|
||||
this._effectsManager.addEffect(
|
||||
effectData,
|
||||
this._rendererEffects,
|
||||
this._renderer.getRendererObject(),
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the effect with the specified name
|
||||
* @param effectName The name of the effect.
|
||||
*/
|
||||
removeEffect(effectName: string): void {
|
||||
this._effectsManager.removeEffect(
|
||||
this._rendererEffects,
|
||||
this._renderer.getRendererObject(),
|
||||
effectName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an effect parameter value (for parameters that are numbers).
|
||||
* @param name The name of the effect to update.
|
||||
* @param parameterName The name of the parameter to update.
|
||||
* @param value The new value (number).
|
||||
*/
|
||||
setEffectDoubleParameter(
|
||||
name: string,
|
||||
parameterName: string,
|
||||
value: float
|
||||
): void {
|
||||
this._effectsManager.setEffectDoubleParameter(
|
||||
this._rendererEffects,
|
||||
name,
|
||||
parameterName,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an effect parameter value (for parameters that are strings).
|
||||
* @param name The name of the effect to update.
|
||||
* @param parameterName The name of the parameter to update.
|
||||
* @param value The new value (string).
|
||||
*/
|
||||
setEffectStringParameter(
|
||||
name: string,
|
||||
parameterName: string,
|
||||
value: string
|
||||
): void {
|
||||
this._effectsManager.setEffectStringParameter(
|
||||
this._rendererEffects,
|
||||
name,
|
||||
parameterName,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an effect parameter value (for parameters that are booleans).
|
||||
* @param name The name of the effect to update.
|
||||
* @param parameterName The name of the parameter to update.
|
||||
* @param value The new value (boolean).
|
||||
*/
|
||||
setEffectBooleanParameter(
|
||||
name: string,
|
||||
parameterName: string,
|
||||
value: boolean
|
||||
): void {
|
||||
this._effectsManager.setEffectBooleanParameter(
|
||||
this._rendererEffects,
|
||||
name,
|
||||
parameterName,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable an effect.
|
||||
* @param name The name of the effect to enable or disable.
|
||||
* @param enable true to enable, false to disable
|
||||
*/
|
||||
enableEffect(name: string, enable: boolean): void {
|
||||
this._effectsManager.enableEffect(this._rendererEffects, name, enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an effect is enabled
|
||||
* @param name The name of the effect
|
||||
* @return true if the effect is enabled, false otherwise.
|
||||
*/
|
||||
isEffectEnabled(name: string): boolean {
|
||||
return this._effectsManager.isEffectEnabled(this._rendererEffects, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an effect exists on this layer
|
||||
* @param name The name of the effect
|
||||
* @return true if the effect exists, false otherwise.
|
||||
*/
|
||||
hasEffect(name: string): boolean {
|
||||
return this._effectsManager.hasEffect(this._rendererEffects, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time scale for the objects on the layer:
|
||||
* time will be slower if time scale is < 1, faster if > 1.
|
||||
* @param timeScale The new time scale (must be positive).
|
||||
*/
|
||||
setTimeScale(timeScale: float): void {
|
||||
if (timeScale >= 0) {
|
||||
this._timeScale = timeScale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time scale for the objects on the layer.
|
||||
*/
|
||||
getTimeScale(): float {
|
||||
return this._timeScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the time elapsed since the last frame,
|
||||
* in milliseconds, for objects on the layer.
|
||||
*
|
||||
* @param instanceContainer The instance container the layer belongs to (deprecated - can be omitted).
|
||||
*/
|
||||
getElapsedTime(instanceContainer?: gdjs.RuntimeInstanceContainer): float {
|
||||
const container = instanceContainer || this._runtimeScene;
|
||||
return container.getElapsedTime() * this._timeScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the position, rotation and scale (zoom) of the layer camera to be the same as the base layer camera.
|
||||
*/
|
||||
followBaseLayer(): void {
|
||||
const baseLayer = this._runtimeScene.getLayer('');
|
||||
this.setCameraX(baseLayer.getCameraX());
|
||||
this.setCameraY(baseLayer.getCameraY());
|
||||
this.setCameraRotation(baseLayer.getCameraRotation());
|
||||
this.setCameraZoom(baseLayer.getCameraZoom());
|
||||
}
|
||||
|
||||
/**
|
||||
* The clear color is defined in the format [r, g, b], with components in the range of 0 to 1.
|
||||
* @return the clear color of layer in the range of [0, 1].
|
||||
*/
|
||||
getClearColor(): Array<integer> {
|
||||
return this._clearColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the clear color in format [r, g, b], with components in the range of 0 to 1.;
|
||||
* @param r Red color component in the range 0-255.
|
||||
* @param g Green color component in the range 0-255.
|
||||
* @param b Blue color component in the range 0-255.
|
||||
*/
|
||||
setClearColor(r: integer, g: integer, b: integer): void {
|
||||
this._clearColor[0] = r / 255;
|
||||
this._clearColor[1] = g / 255;
|
||||
this._clearColor[2] = b / 255;
|
||||
this._renderer.updateClearColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether layer's camera follows base layer's camera or not.
|
||||
*/
|
||||
setFollowBaseLayerCamera(follow: boolean): void {
|
||||
this._followBaseLayerCamera = follow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the layer is a lighting layer, false otherwise.
|
||||
* @return true if it is a lighting layer, false otherwise.
|
||||
*/
|
||||
isLightingLayer(): boolean {
|
||||
return this._isLightingLayer;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* GDevelop JS Platform
|
||||
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
/**
|
||||
* Represents a layer of a scene, used to display objects.
|
||||
*
|
||||
* Viewports and multiple cameras are not supported.
|
||||
*
|
||||
* It does some optimizations but works exactly the same as
|
||||
* {@link gdjs.Layer}.
|
||||
*/
|
||||
export class RuntimeSceneLayer extends gdjs.Layer {
|
||||
/**
|
||||
* @param layerData The data used to initialize the layer
|
||||
* @param scene The scene in which the layer is used
|
||||
*/
|
||||
constructor(layerData: LayerData, scene: gdjs.RuntimeScene) {
|
||||
super(layerData, scene);
|
||||
}
|
||||
|
||||
convertCoords(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer = 0,
|
||||
result: FloatPoint
|
||||
): FloatPoint {
|
||||
// The result parameter used to be optional.
|
||||
let position = result || [0, 0];
|
||||
x -= this.getRuntimeScene()._cachedGameResolutionWidth / 2;
|
||||
y -= this.getRuntimeScene()._cachedGameResolutionHeight / 2;
|
||||
x /= Math.abs(this._zoomFactor);
|
||||
y /= Math.abs(this._zoomFactor);
|
||||
|
||||
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
|
||||
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
|
||||
const tmp = x;
|
||||
const cosValue = Math.cos(angleInRadians);
|
||||
const sinValue = Math.sin(angleInRadians);
|
||||
x = cosValue * x - sinValue * y;
|
||||
y = sinValue * tmp + cosValue * y;
|
||||
position[0] = x + this.getCameraX(cameraId);
|
||||
position[1] = y + this.getCameraY(cameraId);
|
||||
return position;
|
||||
}
|
||||
|
||||
convertInverseCoords(
|
||||
x: float,
|
||||
y: float,
|
||||
cameraId: integer = 0,
|
||||
result: FloatPoint
|
||||
): FloatPoint {
|
||||
// The result parameter used to be optional.
|
||||
let position = result || [0, 0];
|
||||
x -= this.getCameraX(cameraId);
|
||||
y -= this.getCameraY(cameraId);
|
||||
|
||||
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
|
||||
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
|
||||
const tmp = x;
|
||||
const cosValue = Math.cos(-angleInRadians);
|
||||
const sinValue = Math.sin(-angleInRadians);
|
||||
x = cosValue * x - sinValue * y;
|
||||
y = sinValue * tmp + cosValue * y;
|
||||
x *= Math.abs(this._zoomFactor);
|
||||
y *= Math.abs(this._zoomFactor);
|
||||
position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2;
|
||||
position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2;
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
@@ -339,6 +339,7 @@ namespace gdjs {
|
||||
// Avoid circular reference from behavior to parent runtimeObject
|
||||
// Exclude rendering related objects:
|
||||
'_renderer',
|
||||
'_gameRenderer',
|
||||
'_imageManager',
|
||||
// Exclude PIXI textures:
|
||||
'baseTexture',
|
||||
|
@@ -1037,7 +1037,7 @@ namespace gdjs {
|
||||
_hotReloadRuntimeLayer(
|
||||
oldLayer: LayerData,
|
||||
newLayer: LayerData,
|
||||
runtimeLayer: gdjs.Layer
|
||||
runtimeLayer: gdjs.RuntimeLayer
|
||||
): void {
|
||||
// Properties
|
||||
if (oldLayer.visibility !== newLayer.visibility) {
|
||||
@@ -1073,7 +1073,7 @@ namespace gdjs {
|
||||
_hotReloadRuntimeLayerEffects(
|
||||
oldEffectsData: EffectData[],
|
||||
newEffectsData: EffectData[],
|
||||
runtimeLayer: gdjs.Layer
|
||||
runtimeLayer: gdjs.RuntimeLayer
|
||||
): void {
|
||||
oldEffectsData.forEach((oldEffectData) => {
|
||||
const name = oldEffectData.name;
|
||||
@@ -1115,7 +1115,7 @@ namespace gdjs {
|
||||
_hotReloadRuntimeLayerEffect(
|
||||
oldEffectData: EffectData,
|
||||
newEffectData: EffectData,
|
||||
runtimeLayer: gdjs.Layer,
|
||||
runtimeLayer: gdjs.RuntimeLayer,
|
||||
effectName: string
|
||||
): void {
|
||||
// We consider oldEffectData.effectType and newEffectData.effectType
|
||||
|
@@ -5,32 +5,16 @@
|
||||
*/
|
||||
namespace gdjs {
|
||||
/**
|
||||
* Represents a layer of a container, used to display objects.
|
||||
* Represents a layer of a scene, used to display objects.
|
||||
*
|
||||
* Viewports and multiple cameras are not supported.
|
||||
*/
|
||||
export class Layer implements EffectsTarget {
|
||||
_name: string;
|
||||
export class Layer extends gdjs.RuntimeLayer {
|
||||
_cameraRotation: float = 0;
|
||||
_zoomFactor: float = 1;
|
||||
_timeScale: float = 1;
|
||||
_defaultZOrder: integer = 0;
|
||||
_hidden: boolean;
|
||||
_initialEffectsData: Array<EffectData>;
|
||||
_cameraX: float;
|
||||
_cameraY: float;
|
||||
|
||||
_runtimeScene: gdjs.RuntimeInstanceContainer;
|
||||
_effectsManager: gdjs.EffectsManager;
|
||||
|
||||
// Lighting layer properties.
|
||||
_isLightingLayer: boolean;
|
||||
_followBaseLayerCamera: boolean;
|
||||
_clearColor: Array<integer>;
|
||||
|
||||
_rendererEffects: Record<string, PixiFiltersTools.Filter> = {};
|
||||
_renderer: gdjs.LayerRenderer;
|
||||
|
||||
/**
|
||||
* @param layerData The data used to initialize the layer
|
||||
* @param instanceContainer The container in which the layer is used
|
||||
@@ -39,50 +23,10 @@ namespace gdjs {
|
||||
layerData: LayerData,
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
) {
|
||||
this._name = layerData.name;
|
||||
this._hidden = !layerData.visibility;
|
||||
this._initialEffectsData = layerData.effects || [];
|
||||
this._runtimeScene = instanceContainer;
|
||||
super(layerData, instanceContainer);
|
||||
|
||||
this._cameraX = this.getWidth() / 2;
|
||||
this._cameraY = this.getHeight() / 2;
|
||||
this._effectsManager = instanceContainer.getGame().getEffectsManager();
|
||||
this._isLightingLayer = layerData.isLightingLayer;
|
||||
this._followBaseLayerCamera = layerData.followBaseLayerCamera;
|
||||
this._clearColor = [
|
||||
layerData.ambientLightColorR / 255,
|
||||
layerData.ambientLightColorG / 255,
|
||||
layerData.ambientLightColorB / 255,
|
||||
1.0,
|
||||
];
|
||||
this._renderer = new gdjs.LayerRenderer(
|
||||
this,
|
||||
instanceContainer.getRenderer(),
|
||||
instanceContainer.getGame().getRenderer().getPIXIRenderer()
|
||||
);
|
||||
this.show(!this._hidden);
|
||||
for (let i = 0; i < layerData.effects.length; ++i) {
|
||||
this.addEffect(layerData.effects[i]);
|
||||
}
|
||||
}
|
||||
|
||||
getRenderer(): gdjs.LayerRenderer {
|
||||
return this._renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default Z order to be attributed to objects created on this layer
|
||||
* (usually from events generated code).
|
||||
*/
|
||||
getDefaultZOrder(): float {
|
||||
return this._defaultZOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default Z order to be attributed to objects created on this layer.
|
||||
* @param defaultZOrder The Z order to use when creating a new object from events.
|
||||
*/
|
||||
setDefaultZOrder(defaultZOrder: integer): void {
|
||||
this._defaultZOrder = defaultZOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,33 +51,6 @@ namespace gdjs {
|
||||
this._renderer.updatePosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scene the layer belongs to directly or indirectly
|
||||
* @returns the scene the layer belongs to directly or indirectly
|
||||
*/
|
||||
getRuntimeScene(): gdjs.RuntimeScene {
|
||||
return this._runtimeScene.getScene();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at each frame, after events are run and before rendering.
|
||||
*/
|
||||
updatePreRender(instanceContainer?: gdjs.RuntimeInstanceContainer): void {
|
||||
if (this._followBaseLayerCamera) {
|
||||
this.followBaseLayer();
|
||||
}
|
||||
this._renderer.updatePreRender();
|
||||
this._effectsManager.updatePreRender(this._rendererEffects, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the layer
|
||||
* @return The name of the layer
|
||||
*/
|
||||
getName(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the camera center X position.
|
||||
*
|
||||
@@ -202,24 +119,6 @@ namespace gdjs {
|
||||
return this.getHeight() / this._zoomFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show (or hide) the layer.
|
||||
* @param enable true to show the layer, false to hide it.
|
||||
*/
|
||||
show(enable: boolean): void {
|
||||
this._hidden = !enable;
|
||||
this._renderer.updateVisibility(enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the layer is visible.
|
||||
*
|
||||
* @return true if the layer is visible.
|
||||
*/
|
||||
isVisible(): boolean {
|
||||
return !this._hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the zoom of a camera.
|
||||
*
|
||||
@@ -278,16 +177,25 @@ namespace gdjs {
|
||||
cameraId: integer = 0,
|
||||
result: FloatPoint
|
||||
): FloatPoint {
|
||||
// This code duplicates applyLayerInverseTransformation for performance reasons;
|
||||
|
||||
// The result parameter used to be optional.
|
||||
let position = result || [0, 0];
|
||||
// TODO EBO use an AffineTransformation to avoid chained calls.
|
||||
position = this._runtimeScene.convertCoords(x, y, position);
|
||||
return this.applyLayerInverseTransformation(
|
||||
position[0],
|
||||
position[1],
|
||||
cameraId,
|
||||
position
|
||||
);
|
||||
x -= this.getRuntimeScene()._cachedGameResolutionWidth / 2;
|
||||
y -= this.getRuntimeScene()._cachedGameResolutionHeight / 2;
|
||||
x /= Math.abs(this._zoomFactor);
|
||||
y /= Math.abs(this._zoomFactor);
|
||||
|
||||
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
|
||||
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
|
||||
const tmp = x;
|
||||
const cosValue = Math.cos(angleInRadians);
|
||||
const sinValue = Math.sin(angleInRadians);
|
||||
x = cosValue * x - sinValue * y;
|
||||
y = sinValue * tmp + cosValue * y;
|
||||
position[0] = x + this.getCameraX(cameraId);
|
||||
position[1] = y + this.getCameraY(cameraId);
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,14 +249,25 @@ namespace gdjs {
|
||||
cameraId: integer = 0,
|
||||
result: FloatPoint
|
||||
): FloatPoint {
|
||||
// This code duplicates applyLayerTransformation for performance reasons;
|
||||
|
||||
// The result parameter used to be optional.
|
||||
let position = result || [0, 0];
|
||||
// TODO EBO use an AffineTransformation to avoid chained calls.
|
||||
this.applyLayerTransformation(x, y, cameraId, position);
|
||||
return this._runtimeScene.convertInverseCoords(
|
||||
position[0],
|
||||
position[1],
|
||||
position
|
||||
);
|
||||
x -= this.getCameraX(cameraId);
|
||||
y -= this.getCameraY(cameraId);
|
||||
|
||||
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
|
||||
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
|
||||
const tmp = x;
|
||||
const cosValue = Math.cos(-angleInRadians);
|
||||
const sinValue = Math.sin(-angleInRadians);
|
||||
x = cosValue * x - sinValue * y;
|
||||
y = sinValue * tmp + cosValue * y;
|
||||
x *= Math.abs(this._zoomFactor);
|
||||
y *= Math.abs(this._zoomFactor);
|
||||
position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2;
|
||||
position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2;
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -388,14 +307,6 @@ namespace gdjs {
|
||||
return result;
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this._runtimeScene.getViewportWidth();
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return this._runtimeScene.getViewportHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* This ensure that the viewport dimensions are up to date.
|
||||
*
|
||||
@@ -406,199 +317,5 @@ namespace gdjs {
|
||||
// This will update dimensions.
|
||||
this._runtimeScene.getViewportWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the initial effects data for the layer. Only to
|
||||
* be used by renderers.
|
||||
* @deprecated
|
||||
*/
|
||||
getInitialEffectsData(): EffectData[] {
|
||||
return this._initialEffectsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new effect, or replace the one with the same name.
|
||||
* @param effectData The data of the effect to add.
|
||||
*/
|
||||
addEffect(effectData: EffectData): void {
|
||||
this._effectsManager.addEffect(
|
||||
effectData,
|
||||
this._rendererEffects,
|
||||
this._renderer.getRendererObject(),
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the effect with the specified name
|
||||
* @param effectName The name of the effect.
|
||||
*/
|
||||
removeEffect(effectName: string): void {
|
||||
this._effectsManager.removeEffect(
|
||||
this._rendererEffects,
|
||||
this._renderer.getRendererObject(),
|
||||
effectName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an effect parameter value (for parameters that are numbers).
|
||||
* @param name The name of the effect to update.
|
||||
* @param parameterName The name of the parameter to update.
|
||||
* @param value The new value (number).
|
||||
*/
|
||||
setEffectDoubleParameter(
|
||||
name: string,
|
||||
parameterName: string,
|
||||
value: float
|
||||
): void {
|
||||
this._effectsManager.setEffectDoubleParameter(
|
||||
this._rendererEffects,
|
||||
name,
|
||||
parameterName,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an effect parameter value (for parameters that are strings).
|
||||
* @param name The name of the effect to update.
|
||||
* @param parameterName The name of the parameter to update.
|
||||
* @param value The new value (string).
|
||||
*/
|
||||
setEffectStringParameter(
|
||||
name: string,
|
||||
parameterName: string,
|
||||
value: string
|
||||
): void {
|
||||
this._effectsManager.setEffectStringParameter(
|
||||
this._rendererEffects,
|
||||
name,
|
||||
parameterName,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an effect parameter value (for parameters that are booleans).
|
||||
* @param name The name of the effect to update.
|
||||
* @param parameterName The name of the parameter to update.
|
||||
* @param value The new value (boolean).
|
||||
*/
|
||||
setEffectBooleanParameter(
|
||||
name: string,
|
||||
parameterName: string,
|
||||
value: boolean
|
||||
): void {
|
||||
this._effectsManager.setEffectBooleanParameter(
|
||||
this._rendererEffects,
|
||||
name,
|
||||
parameterName,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable an effect.
|
||||
* @param name The name of the effect to enable or disable.
|
||||
* @param enable true to enable, false to disable
|
||||
*/
|
||||
enableEffect(name: string, enable: boolean): void {
|
||||
this._effectsManager.enableEffect(this._rendererEffects, name, enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an effect is enabled
|
||||
* @param name The name of the effect
|
||||
* @return true if the effect is enabled, false otherwise.
|
||||
*/
|
||||
isEffectEnabled(name: string): boolean {
|
||||
return this._effectsManager.isEffectEnabled(this._rendererEffects, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an effect exists on this layer
|
||||
* @param name The name of the effect
|
||||
* @return true if the effect exists, false otherwise.
|
||||
*/
|
||||
hasEffect(name: string): boolean {
|
||||
return this._effectsManager.hasEffect(this._rendererEffects, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time scale for the objects on the layer:
|
||||
* time will be slower if time scale is < 1, faster if > 1.
|
||||
* @param timeScale The new time scale (must be positive).
|
||||
*/
|
||||
setTimeScale(timeScale: float): void {
|
||||
if (timeScale >= 0) {
|
||||
this._timeScale = timeScale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time scale for the objects on the layer.
|
||||
*/
|
||||
getTimeScale(): float {
|
||||
return this._timeScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the time elapsed since the last frame,
|
||||
* in milliseconds, for objects on the layer.
|
||||
*
|
||||
* @param instanceContainer The instance container the layer belongs to (deprecated - can be omitted).
|
||||
*/
|
||||
getElapsedTime(instanceContainer?: gdjs.RuntimeInstanceContainer): float {
|
||||
const container = instanceContainer || this._runtimeScene;
|
||||
return container.getElapsedTime() * this._timeScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the position, rotation and scale (zoom) of the layer camera to be the same as the base layer camera.
|
||||
*/
|
||||
followBaseLayer(): void {
|
||||
const baseLayer = this._runtimeScene.getLayer('');
|
||||
this.setCameraX(baseLayer.getCameraX());
|
||||
this.setCameraY(baseLayer.getCameraY());
|
||||
this.setCameraRotation(baseLayer.getCameraRotation());
|
||||
this.setCameraZoom(baseLayer.getCameraZoom());
|
||||
}
|
||||
|
||||
/**
|
||||
* The clear color is defined in the format [r, g, b], with components in the range of 0 to 1.
|
||||
* @return the clear color of layer in the range of [0, 1].
|
||||
*/
|
||||
getClearColor(): Array<integer> {
|
||||
return this._clearColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the clear color in format [r, g, b], with components in the range of 0 to 1.;
|
||||
* @param r Red color component in the range 0-255.
|
||||
* @param g Green color component in the range 0-255.
|
||||
* @param b Blue color component in the range 0-255.
|
||||
*/
|
||||
setClearColor(r: integer, g: integer, b: integer): void {
|
||||
this._clearColor[0] = r / 255;
|
||||
this._clearColor[1] = g / 255;
|
||||
this._clearColor[2] = b / 255;
|
||||
this._renderer.updateClearColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether layer's camera follows base layer's camera or not.
|
||||
*/
|
||||
setFollowBaseLayerCamera(follow: boolean): void {
|
||||
this._followBaseLayerCamera = follow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the layer is a lighting layer, false otherwise.
|
||||
* @return true if it is a lighting layer, false otherwise.
|
||||
*/
|
||||
isLightingLayer(): boolean {
|
||||
return this._isLightingLayer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -67,11 +67,12 @@ namespace gdjs {
|
||||
this._pixiContainer.pivot.x = this._object.getUnscaledCenterX();
|
||||
this._pixiContainer.pivot.y = this._object.getUnscaledCenterY();
|
||||
this._pixiContainer.position.x =
|
||||
this._object.getDrawableX() +
|
||||
this._object.getX() +
|
||||
this._pixiContainer.pivot.x * Math.abs(this._object._scaleX);
|
||||
this._pixiContainer.position.y =
|
||||
this._object.getDrawableY() +
|
||||
this._object.getY() +
|
||||
this._pixiContainer.pivot.y * Math.abs(this._object._scaleY);
|
||||
|
||||
this._pixiContainer.rotation = gdjs.toRad(this._object.angle);
|
||||
this._pixiContainer.scale.x = this._object._scaleX;
|
||||
this._pixiContainer.scale.y = this._object._scaleY;
|
||||
@@ -126,7 +127,7 @@ namespace gdjs {
|
||||
return null;
|
||||
}
|
||||
|
||||
setLayerIndex(layer: gdjs.Layer, index: float): void {
|
||||
setLayerIndex(layer: gdjs.RuntimeLayer, index: float): void {
|
||||
const layerPixiRenderer: gdjs.LayerPixiRenderer = layer.getRenderer();
|
||||
let layerPixiObject:
|
||||
| PIXI.Container
|
||||
|
@@ -20,7 +20,7 @@ namespace gdjs {
|
||||
*
|
||||
* @see gdjs.RuntimeInstanceContainer.setLayerIndex
|
||||
*/
|
||||
setLayerIndex(layer: gdjs.Layer, index: integer): void;
|
||||
setLayerIndex(layer: gdjs.RuntimeLayer, index: integer): void;
|
||||
|
||||
getRendererObject(): PIXI.Container;
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ namespace gdjs {
|
||||
export class LayerPixiRenderer {
|
||||
_pixiContainer: PIXI.Container;
|
||||
|
||||
_layer: gdjs.Layer;
|
||||
_layer: gdjs.RuntimeLayer;
|
||||
_renderTexture: PIXI.RenderTexture | null = null;
|
||||
_lightingSprite: PIXI.Sprite | null = null;
|
||||
_runtimeSceneRenderer: gdjs.RuntimeInstanceContainerRenderer;
|
||||
@@ -35,7 +35,7 @@ namespace gdjs {
|
||||
* @param runtimeInstanceContainerRenderer The scene renderer
|
||||
*/
|
||||
constructor(
|
||||
layer: gdjs.Layer,
|
||||
layer: gdjs.RuntimeLayer,
|
||||
runtimeInstanceContainerRenderer: gdjs.RuntimeInstanceContainerRenderer,
|
||||
pixiRenderer: PIXI.Renderer | null
|
||||
) {
|
||||
|
@@ -104,7 +104,7 @@ namespace gdjs {
|
||||
return this._pixiRenderer;
|
||||
}
|
||||
|
||||
setLayerIndex(layer: gdjs.Layer, index: float): void {
|
||||
setLayerIndex(layer: gdjs.RuntimeLayer, index: float): void {
|
||||
const layerPixiRenderer: gdjs.LayerPixiRenderer = layer.getRenderer();
|
||||
let layerPixiObject:
|
||||
| PIXI.Container
|
||||
|
@@ -48,6 +48,14 @@ namespace gdjs {
|
||||
*/
|
||||
gdevelopResourceToken?: string;
|
||||
|
||||
/**
|
||||
* Check if, in some exceptional cases, we allow authentication
|
||||
* to be done through a iframe.
|
||||
* This is usually discouraged as the user can't verify that the authentication
|
||||
* window is a genuine one. It's only to be used in trusted contexts.
|
||||
*/
|
||||
allowAuthenticationUsingIframeForPreview?: boolean;
|
||||
|
||||
/**
|
||||
* If set, the game should use the specified environment for making calls
|
||||
* to GDevelop APIs ("dev" = development APIs).
|
||||
|
@@ -71,6 +71,10 @@ namespace gdjs {
|
||||
this.onGameResolutionResized();
|
||||
}
|
||||
|
||||
addLayer(layerData: LayerData) {
|
||||
this._layers.put(layerData.name, new gdjs.Layer(layerData, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when the canvas where the scene is rendered has been resized.
|
||||
* See gdjs.RuntimeGame.startGameLoop in particular.
|
||||
@@ -86,8 +90,7 @@ namespace gdjs {
|
||||
: 0;
|
||||
for (const name in this._layers.items) {
|
||||
if (this._layers.items.hasOwnProperty(name)) {
|
||||
/** @type gdjs.Layer */
|
||||
const theLayer: gdjs.Layer = this._layers.items[name];
|
||||
const theLayer: gdjs.RuntimeLayer = this._layers.items[name];
|
||||
theLayer.onGameResolutionResized(
|
||||
oldGameResolutionOriginX,
|
||||
oldGameResolutionOriginY
|
||||
@@ -193,13 +196,6 @@ namespace gdjs {
|
||||
return behaviorSharedData;
|
||||
}
|
||||
|
||||
addLayer(layerData: LayerData) {
|
||||
this._layers.put(
|
||||
layerData.name,
|
||||
new gdjs.RuntimeSceneLayer(layerData, this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a scene is "paused", i.e it will be not be rendered again
|
||||
* for some time, until it's resumed or unloaded.
|
||||
|
@@ -277,8 +277,8 @@ namespace gdjs {
|
||||
linkElement.id = 'watermark-link';
|
||||
|
||||
let targetUrl = this._authorUsername
|
||||
? new URL(`https://liluo.io/${this._authorUsername}`)
|
||||
: new URL('https://liluo.io');
|
||||
? new URL(`https://gd.games/${this._authorUsername}`)
|
||||
: new URL('https://gd.games');
|
||||
|
||||
if (this._isDevEnvironment) {
|
||||
targetUrl.searchParams.set('dev', 'true');
|
||||
|
@@ -6,7 +6,7 @@ describe('gdjs.Layer', function() {
|
||||
|
||||
it('benchmark convertCoords and convertInverseCoords', function() {
|
||||
this.timeout(30000);
|
||||
var layer = new gdjs.RuntimeSceneLayer(
|
||||
var layer = new gdjs.Layer(
|
||||
{ name: 'My layer',
|
||||
visibility: true,
|
||||
effects: [],
|
||||
|
@@ -55,8 +55,9 @@ module.exports = function (config) {
|
||||
'./newIDE/app/resources/GDJS/Runtime/scenestack.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/profiler.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/force.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/RuntimeLayer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/layer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/RuntimeSceneLayer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/RuntimeCustomObjectLayer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/timer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/inputmanager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/runtimegame.js',
|
||||
|
@@ -198,8 +198,8 @@ describe('gdjs.CustomRuntimeObject', function () {
|
||||
expect(customObject.getHitBoxes().length).to.be(2);
|
||||
expect(customObject.getHitBoxes()[0].vertices).to.eql([
|
||||
[32, 32],
|
||||
[31.999999999999993, -31.999999999999996],
|
||||
[96, 31.999999999999996],
|
||||
[32, -32],
|
||||
[96, 32],
|
||||
]);
|
||||
expect(customObject.getHitBoxes()[1].vertices).to.eql([
|
||||
[32, 96],
|
||||
@@ -245,6 +245,12 @@ describe('gdjs.CustomRuntimeObject', function () {
|
||||
customObject.setWidth(32);
|
||||
customObject.setHeight(96);
|
||||
|
||||
// To draw the transformed shapes:
|
||||
// - draw 2 squares side-by-side
|
||||
// - scale them and keep the top-left corner in place
|
||||
// - rotate the shape keeping the center of the scaled drawing in place
|
||||
// - translate it according to the object new position.
|
||||
|
||||
expect(customObject.getWidth()).to.be(32);
|
||||
expect(customObject.getHeight()).to.be(96);
|
||||
|
||||
@@ -253,14 +259,14 @@ describe('gdjs.CustomRuntimeObject', function () {
|
||||
|
||||
expect(customObject.getHitBoxes().length).to.be(2);
|
||||
expect(customObject.getHitBoxes()[0].vertices).to.eql([
|
||||
[-12, 100],
|
||||
[-12, 84],
|
||||
[84, 100],
|
||||
[-24, 64],
|
||||
[-24, 48],
|
||||
[72, 64],
|
||||
]);
|
||||
expect(customObject.getHitBoxes()[1].vertices).to.eql([
|
||||
[-12, 116],
|
||||
[-12, 100],
|
||||
[84, 116],
|
||||
[-24, 80],
|
||||
[-24, 64],
|
||||
[72, 80],
|
||||
]);
|
||||
});
|
||||
|
||||
|
@@ -9,7 +9,7 @@ describe('gdjs.Layer', function() {
|
||||
var runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
|
||||
it('can convert coordinates', function(){
|
||||
var layer = new gdjs.RuntimeSceneLayer({name: 'My layer', visibility: true, effects:[]}, runtimeScene)
|
||||
var layer = new gdjs.Layer({name: 'My layer', visibility: true, effects:[]}, runtimeScene)
|
||||
layer.setCameraX(100, 0);
|
||||
layer.setCameraY(200, 0);
|
||||
layer.setCameraRotation(90, 0);
|
||||
@@ -18,7 +18,7 @@ describe('gdjs.Layer', function() {
|
||||
expect(layer.convertCoords(350, 450, 0)[1]).to.be.within(149.9999, 150.001);
|
||||
});
|
||||
it('can convert inverse coordinates', function(){
|
||||
var layer = new gdjs.RuntimeSceneLayer({name: 'My layer', visibility: true, effects:[]}, runtimeScene)
|
||||
var layer = new gdjs.Layer({name: 'My layer', visibility: true, effects:[]}, runtimeScene)
|
||||
layer.setCameraX(100, 0);
|
||||
layer.setCameraY(200, 0);
|
||||
layer.setCameraRotation(90, 0);
|
||||
|
@@ -55,7 +55,7 @@ describe('gdjs.RuntimeWatermark integration tests', () => {
|
||||
)
|
||||
throw new Error('Watermark DOM elements could not be found.');
|
||||
expect(watermark._linkElement.href).to.be(
|
||||
'https://liluo.io/HelperWesley?utm_source=gdevelop-game&utm_medium=game-watermark&utm_campaign=project-uuid'
|
||||
'https://gd.games/HelperWesley?utm_source=gdevelop-game&utm_medium=game-watermark&utm_campaign=project-uuid'
|
||||
);
|
||||
expect(watermark._containerElement.style.opacity).to.be('0');
|
||||
expect(watermark._backgroundElement.style.opacity).to.be('0');
|
||||
@@ -146,7 +146,7 @@ describe('gdjs.RuntimeWatermark integration tests', () => {
|
||||
throw new Error('Watermark DOM elements could not be found.');
|
||||
|
||||
expect(watermark._linkElement.href).to.be(
|
||||
'https://liluo.io/?utm_source=gdevelop-game&utm_medium=game-watermark&utm_campaign=project-uuid'
|
||||
'https://gd.games/?utm_source=gdevelop-game&utm_medium=game-watermark&utm_campaign=project-uuid'
|
||||
);
|
||||
expect(watermark._containerElement.style.opacity).to.be('0');
|
||||
expect(watermark._backgroundElement.style.opacity).to.be('0');
|
||||
|
@@ -2251,6 +2251,10 @@ interface UsedExtensionsFinder {
|
||||
[Value] UsedExtensionsResult STATIC_ScanProject([Ref] Project project);
|
||||
};
|
||||
|
||||
interface InstructionsCountEvaluator {
|
||||
long STATIC_ScanProject([Ref] Project project);
|
||||
};
|
||||
|
||||
interface ExtensionAndBehaviorMetadata {
|
||||
[Const, Ref] PlatformExtension GetExtension();
|
||||
[Const, Ref] BehaviorMetadata GetMetadata();
|
||||
@@ -3150,6 +3154,7 @@ interface PreviewExportOptions {
|
||||
[Ref] PreviewExportOptions SetNonRuntimeScriptsCacheBurst(unsigned long value);
|
||||
[Ref] PreviewExportOptions SetElectronRemoteRequirePath([Const] DOMString electronRemoteRequirePath);
|
||||
[Ref] PreviewExportOptions SetGDevelopResourceToken([Const] DOMString gdevelopResourceToken);
|
||||
[Ref] PreviewExportOptions SetAllowAuthenticationUsingIframeForPreview(boolean enable);
|
||||
};
|
||||
|
||||
[Prefix="gdjs::"]
|
||||
|
@@ -40,6 +40,7 @@
|
||||
#include <GDCore/IDE/Events/InstructionsTypeRenamer.h>
|
||||
#include <GDCore/IDE/Events/TextFormatting.h>
|
||||
#include <GDCore/IDE/Events/UsedExtensionsFinder.h>
|
||||
#include <GDCore/IDE/Events/InstructionsCountEvaluator.h>
|
||||
#include <GDCore/IDE/EventsFunctionTools.h>
|
||||
#include <GDCore/IDE/Events/EventsVariablesFinder.h>
|
||||
#include <GDCore/IDE/Events/EventsIdentifiersFinder.h>
|
||||
|
6
GDevelop.js/types/gdinstructionscountevaluator.js
Normal file
6
GDevelop.js/types/gdinstructionscountevaluator.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdInstructionsCountEvaluator {
|
||||
static scanProject(project: gdProject): number;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -13,6 +13,7 @@ declare class gdPreviewExportOptions {
|
||||
setNonRuntimeScriptsCacheBurst(value: number): gdPreviewExportOptions;
|
||||
setElectronRemoteRequirePath(electronRemoteRequirePath: string): gdPreviewExportOptions;
|
||||
setGDevelopResourceToken(gdevelopResourceToken: string): gdPreviewExportOptions;
|
||||
setAllowAuthenticationUsingIframeForPreview(enable: boolean): gdPreviewExportOptions;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -155,6 +155,7 @@ declare class libGDevelop {
|
||||
PropertyFunctionGenerator: Class<gdPropertyFunctionGenerator>;
|
||||
UsedExtensionsResult: Class<gdUsedExtensionsResult>;
|
||||
UsedExtensionsFinder: Class<gdUsedExtensionsFinder>;
|
||||
InstructionsCountEvaluator: Class<gdInstructionsCountEvaluator>;
|
||||
ExtensionAndBehaviorMetadata: Class<gdExtensionAndBehaviorMetadata>;
|
||||
ExtensionAndObjectMetadata: Class<gdExtensionAndObjectMetadata>;
|
||||
ExtensionAndEffectMetadata: Class<gdExtensionAndEffectMetadata>;
|
||||
|
@@ -1,8 +1,8 @@
|
||||

|
||||

|
||||
|
||||
GDevelop is a full-featured, no-code, open-source game development software. You can build games for mobile, desktop and the web. GDevelop is fast and easy to use: the game logic is built up using an intuitive and powerful event-based system.
|
||||
|
||||

|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -49,10 +49,10 @@ Status of the tests and builds: [ created with GDevelop.
|
||||
- Find more GDevelop games on [Liluo.io](https://liluo.io).
|
||||
- Find more GDevelop games on [gd.games](https://gd.games).
|
||||
- Suggest your game to be [added to the showcase here](https://docs.google.com/forms/d/e/1FAIpQLSfjiOnkbODuPifSGuzxYY61vB5kyMWdTZSSqkJsv3H6ePRTQA/viewform).
|
||||
|
||||

|
||||

|
||||
|
||||
## License
|
||||
|
||||
|
@@ -26,7 +26,7 @@
|
||||
</pattern>
|
||||
<filter id="filter0_b_1094_3534" x="31.8506" y="33.1113" width="33.5776" height="49.0557" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImage" stdDeviation="2.5"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="2.5"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_1094_3534"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_1094_3534" result="shape"/>
|
||||
</filter>
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
@@ -35,6 +35,9 @@ const generateExtensionFooterText = fullName => {
|
||||
return (
|
||||
`
|
||||
---
|
||||
|
||||
<tip>Learn [[gdevelop5:extensions:search|how to install new extensions]] by following a step-by-step guide.</tip>
|
||||
|
||||
*This page is an auto-generated reference page about the **${fullName}** extension, made by the community of [[https://gdevelop.io/|GDevelop, the open-source, cross-platform game engine designed for everyone]].*` +
|
||||
' ' +
|
||||
'Learn more about [[gdevelop5:extensions|all GDevelop community-made extensions here]].'
|
||||
@@ -49,7 +52,7 @@ const generateAuthorNamesWithLinks = authors => {
|
||||
.map(author => {
|
||||
if (!author.username) return null;
|
||||
|
||||
return `[${author.username}](https://liluo.io/${author.username})`;
|
||||
return `[${author.username}](https://gd.games/${author.username})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
@@ -80,7 +83,7 @@ const getAllExtensionHeaders = async () => {
|
||||
}).`
|
||||
);
|
||||
}
|
||||
return { ...extensionHeader, ...extensionShortHeader};
|
||||
return { ...extensionHeader, ...extensionShortHeader };
|
||||
})
|
||||
);
|
||||
|
||||
@@ -114,10 +117,7 @@ const sortKeys = table => {
|
||||
* @param {ExtensionHeader} extensionHeader The extension header
|
||||
* @param {boolean} isCommunity The tier
|
||||
*/
|
||||
const createExtensionReferencePage = async (
|
||||
extensionHeader,
|
||||
isCommunity
|
||||
) => {
|
||||
const createExtensionReferencePage = async (extensionHeader, isCommunity) => {
|
||||
const folderName = getExtensionFolderName(extensionHeader.name);
|
||||
const referencePageUrl = `${gdevelopWikiUrlRoot}/extensions/${folderName}/reference`;
|
||||
const helpPageUrl = getHelpLink(extensionHeader.helpPath) || referencePageUrl;
|
||||
@@ -166,7 +166,7 @@ does or inspect its content before using it.
|
||||
* Generate a section for an extension.
|
||||
* @param {ExtensionHeader} extensionHeader The extension header
|
||||
*/
|
||||
const generateExtensionSection = (extensionHeader) => {
|
||||
const generateExtensionSection = extensionHeader => {
|
||||
const folderName = getExtensionFolderName(extensionHeader.name);
|
||||
const referencePageUrl = `${gdevelopWikiUrlRoot}/extensions/${folderName}/reference`;
|
||||
const helpPageUrl = getHelpLink(extensionHeader.helpPath) || referencePageUrl;
|
||||
@@ -201,9 +201,7 @@ const generateAllExtensionsSections = extensionsAndExtensionShortHeaders => {
|
||||
|
||||
extensionSectionsContent += `### ${category}\n\n`;
|
||||
for (const extensionHeader of extensions) {
|
||||
extensionSectionsContent += generateExtensionSection(
|
||||
extensionHeader
|
||||
);
|
||||
extensionSectionsContent += generateExtensionSection(extensionHeader);
|
||||
}
|
||||
}
|
||||
return extensionSectionsContent;
|
||||
@@ -231,14 +229,9 @@ GDevelop is built in a flexible way. In addition to [[gdevelop5:all-features|cor
|
||||
|
||||
indexPageContent += '## Reviewed extensions\n\n';
|
||||
for (const extensionHeader of reviewedExtensionHeaders) {
|
||||
await createExtensionReferencePage(
|
||||
extensionHeader,
|
||||
false
|
||||
);
|
||||
await createExtensionReferencePage(extensionHeader, false);
|
||||
}
|
||||
indexPageContent += generateAllExtensionsSections(
|
||||
reviewedExtensionHeaders
|
||||
);
|
||||
indexPageContent += generateAllExtensionsSections(reviewedExtensionHeaders);
|
||||
|
||||
indexPageContent += `## Community extensions
|
||||
|
||||
|
@@ -25,6 +25,8 @@ import RaisedButtonWithSplitMenu from '../../UI/RaisedButtonWithSplitMenu';
|
||||
import Window from '../../Utils/Window';
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
|
||||
import { ExampleDifficultyChip } from '../../UI/ExampleDifficultyChip';
|
||||
import { ExampleSizeChip } from '../../UI/ExampleSizeChip';
|
||||
const isDev = Window.isDev();
|
||||
|
||||
const electron = optionalRequire('electron');
|
||||
@@ -143,17 +145,30 @@ export function ExampleDialog({
|
||||
<ExampleThumbnailOrIcon exampleShortHeader={exampleShortHeader} />
|
||||
) : null}
|
||||
<Column>
|
||||
{exampleShortHeader.authors && (
|
||||
{
|
||||
<Line>
|
||||
{exampleShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip
|
||||
user={author}
|
||||
key={author.id}
|
||||
isClickable
|
||||
/>
|
||||
))}
|
||||
<div style={{ flexWrap: 'wrap' }}>
|
||||
{exampleShortHeader.codeSizeLevel && (
|
||||
<ExampleSizeChip
|
||||
codeSizeLevel={exampleShortHeader.codeSizeLevel}
|
||||
/>
|
||||
)}
|
||||
{exampleShortHeader.difficultyLevel && (
|
||||
<ExampleDifficultyChip
|
||||
codeSizeLevel={exampleShortHeader.difficultyLevel}
|
||||
/>
|
||||
)}
|
||||
{exampleShortHeader.authors &&
|
||||
exampleShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip
|
||||
user={author}
|
||||
key={author.id}
|
||||
isClickable
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Line>
|
||||
)}
|
||||
}
|
||||
<Text noMargin>{exampleShortHeader.shortDescription}</Text>
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
|
@@ -18,6 +18,8 @@ import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import { openExampleInWebApp } from './ExampleDialog';
|
||||
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
|
||||
import { ExampleSizeChip } from '../../UI/ExampleSizeChip';
|
||||
import { ExampleDifficultyChip } from '../../UI/ExampleDifficultyChip';
|
||||
import HighlightedText from '../../UI/Search/HighlightedText';
|
||||
import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem';
|
||||
import { ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
@@ -111,13 +113,26 @@ export const ExampleListItem = ({
|
||||
)}
|
||||
<Column expand>
|
||||
<Text noMargin>{renderExampleField('name')} </Text>
|
||||
{exampleShortHeader.authors && (
|
||||
{
|
||||
<Line>
|
||||
{exampleShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip user={author} key={author.id} />
|
||||
))}
|
||||
<div style={{ flexWrap: 'wrap' }}>
|
||||
{exampleShortHeader.codeSizeLevel && (
|
||||
<ExampleSizeChip
|
||||
codeSizeLevel={exampleShortHeader.codeSizeLevel}
|
||||
/>
|
||||
)}
|
||||
{exampleShortHeader.difficultyLevel && (
|
||||
<ExampleDifficultyChip
|
||||
codeSizeLevel={exampleShortHeader.difficultyLevel}
|
||||
/>
|
||||
)}
|
||||
{exampleShortHeader.authors &&
|
||||
exampleShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip user={author} key={author.id} />
|
||||
))}
|
||||
</div>
|
||||
</Line>
|
||||
)}
|
||||
}
|
||||
<Text noMargin size="body2">
|
||||
{renderExampleField('shortDescription')}
|
||||
</Text>
|
||||
|
@@ -3,6 +3,7 @@ import * as React from 'react';
|
||||
import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example';
|
||||
import { CorsAwareImage } from '../../UI/CorsAwareImage';
|
||||
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import { iconWithBackgroundStyle } from '../../UI/IconContainer';
|
||||
|
||||
const styles = {
|
||||
iconBackground: {
|
||||
@@ -11,9 +12,8 @@ const styles = {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
background: 'linear-gradient(45deg, #FFFFFF33, #FFFFFF)',
|
||||
...iconWithBackgroundStyle,
|
||||
padding: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -18,13 +18,26 @@ export const prepareExamples = (
|
||||
examples: Array<ExampleShortHeader>
|
||||
): Array<ExampleShortHeader> =>
|
||||
examples.sort((example1, example2) => {
|
||||
const isExample1Starter = example1.tags.includes('Starter');
|
||||
const isExample2Starter = example2.tags.includes('Starter');
|
||||
return isExample1Starter && !isExample2Starter
|
||||
? -1
|
||||
: !isExample1Starter && isExample2Starter
|
||||
? 1
|
||||
: 0;
|
||||
let difference =
|
||||
(example2.tags.includes('Starter') ? 1 : 0) -
|
||||
(example1.tags.includes('Starter') ? 1 : 0);
|
||||
if (difference) {
|
||||
return difference;
|
||||
}
|
||||
difference =
|
||||
(example2.tags.includes('game') ? 1 : 0) -
|
||||
(example1.tags.includes('game') ? 1 : 0);
|
||||
if (difference) {
|
||||
return difference;
|
||||
}
|
||||
difference =
|
||||
(example2.previewImageUrls.length ? 1 : 0) -
|
||||
(example1.previewImageUrls.length ? 1 : 0);
|
||||
if (difference) {
|
||||
return difference;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const getExampleName = (exampleShortHeader: ExampleShortHeader) =>
|
||||
|
@@ -182,14 +182,16 @@ const ExtensionInstallDialog = ({
|
||||
<Trans>Version {' ' + extensionShortHeader.version}</Trans>
|
||||
</Text>
|
||||
<Line>
|
||||
{extensionShortHeader.authors &&
|
||||
extensionShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip
|
||||
user={author}
|
||||
key={author.id}
|
||||
isClickable
|
||||
/>
|
||||
))}
|
||||
<div style={{ flexWrap: 'wrap' }}>
|
||||
{extensionShortHeader.authors &&
|
||||
extensionShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip
|
||||
user={author}
|
||||
key={author.id}
|
||||
isClickable
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Line>
|
||||
</Column>
|
||||
</Line>
|
||||
|
@@ -96,9 +96,11 @@ export const ExtensionListItem = ({
|
||||
</LineStackLayout>
|
||||
{extensionShortHeader.authors && (
|
||||
<Line>
|
||||
{extensionShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip user={author} key={author.id} />
|
||||
))}
|
||||
<div style={{ flexWrap: 'wrap' }}>
|
||||
{extensionShortHeader.authors.map(author => (
|
||||
<UserPublicProfileChip user={author} key={author.id} />
|
||||
))}
|
||||
</div>
|
||||
</Line>
|
||||
)}
|
||||
<Text noMargin size="body2">
|
||||
|
@@ -184,6 +184,14 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
},
|
||||
onConnectionErrored: ({ id, errorMessage }) => {
|
||||
this._getLogsManager(id).addLog({
|
||||
type: 'error',
|
||||
timestamp: performance.now(),
|
||||
group: 'Debugger connection',
|
||||
message: 'The debugger connection errored: ' + errorMessage,
|
||||
});
|
||||
},
|
||||
onServerStateChanged: () => {
|
||||
this.setState(
|
||||
{
|
||||
|
@@ -1171,6 +1171,42 @@ export const declareObjectPropertiesInstructionAndExpressions = (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Declare the instructions (actions/conditions) and expressions for the
|
||||
* properties of the given events based object.
|
||||
* This is akin to what would happen by manually declaring a JS extension
|
||||
* (see `JsExtension.js` files of extensions).
|
||||
*/
|
||||
export const declareObjectInternalInstructions = (
|
||||
i18n: I18nType,
|
||||
extension: gdPlatformExtension,
|
||||
objectMetadata: gdObjectMetadata,
|
||||
eventsBasedObject: gdEventsBasedObject
|
||||
): void => {
|
||||
// TODO EBO Use full type to identify object to avoid collision.
|
||||
// Objects are identified by their name alone.
|
||||
const objectType = eventsBasedObject.getName();
|
||||
|
||||
objectMetadata
|
||||
.addScopedAction(
|
||||
'SetRotationCenter',
|
||||
i18n._('Center of rotation'),
|
||||
i18n._(
|
||||
'Change the center of rotation of an object relatively to the object origin.'
|
||||
),
|
||||
i18n._('Change the center of rotation of _PARAM0_ to _PARAM1_, _PARAM2_'),
|
||||
i18n._('Angle'),
|
||||
'res/actions/position24_black.png',
|
||||
'res/actions/position_black.png'
|
||||
)
|
||||
.addParameter('object', i18n._('Object'), objectType)
|
||||
.addParameter('number', i18n._('X position'))
|
||||
.addParameter('number', i18n._('Y position'))
|
||||
.markAsAdvanced()
|
||||
.setPrivate()
|
||||
.setFunctionName('setRotationCenter');
|
||||
};
|
||||
|
||||
/**
|
||||
* Add to the instruction (action/condition) or expression the parameters
|
||||
* expected by the events function.
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
isExtensionLifecycleEventsFunction,
|
||||
declareBehaviorPropertiesInstructionAndExpressions,
|
||||
declareObjectPropertiesInstructionAndExpressions,
|
||||
declareObjectInternalInstructions,
|
||||
} from './MetadataDeclarationHelpers';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
@@ -524,6 +525,12 @@ function generateObject(
|
||||
objectMetadata,
|
||||
eventsBasedObject
|
||||
);
|
||||
declareObjectInternalInstructions(
|
||||
options.i18n,
|
||||
extension,
|
||||
objectMetadata,
|
||||
eventsBasedObject
|
||||
);
|
||||
|
||||
// Declare all the object functions
|
||||
mapFor(0, eventsFunctionsContainer.getEventsFunctionsCount(), i => {
|
||||
|
@@ -34,6 +34,7 @@ import {
|
||||
} from '../../Utils/Behavior';
|
||||
import ExtensionsSearchDialog from '../../AssetStore/ExtensionStore/ExtensionsSearchDialog';
|
||||
import { sendBehaviorAdded } from '../../Utils/Analytics/EventSender';
|
||||
import { useShouldAutofocusInput } from '../../UI/Reponsive/ScreenTypeMeasurer';
|
||||
|
||||
const styles = {
|
||||
fullHeightSelector: {
|
||||
@@ -149,6 +150,7 @@ export default function InstructionEditorDialog({
|
||||
newExtensionDialogOpen,
|
||||
setNewExtensionDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const shouldAutofocusInput = useShouldAutofocusInput();
|
||||
|
||||
// Handle the back button
|
||||
const stepBackFrom = (origin: StepName, windowWidth: WidthType) => {
|
||||
@@ -194,19 +196,19 @@ export default function InstructionEditorDialog({
|
||||
freeInstructionComponentRef.current.reEnumerateInstructions(i18n);
|
||||
};
|
||||
|
||||
// Focus the parameters when showing them
|
||||
const instructionParametersEditor = React.useRef<?InstructionParametersEditorInterface>(
|
||||
null
|
||||
);
|
||||
// Focus the parameters when showing them
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (step === 'parameters') {
|
||||
if (shouldAutofocusInput && step === 'parameters') {
|
||||
if (instructionParametersEditor.current) {
|
||||
instructionParametersEditor.current.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
[step]
|
||||
[step, shouldAutofocusInput]
|
||||
);
|
||||
|
||||
const instructionMetadata = getInstructionMetadata({
|
||||
@@ -244,7 +246,7 @@ export default function InstructionEditorDialog({
|
||||
chooseObject(chosenObjectName);
|
||||
setStep('object-instructions');
|
||||
}}
|
||||
focusOnMount={!instructionType}
|
||||
focusOnMount={shouldAutofocusInput && !instructionType}
|
||||
onSearchStartOrReset={forceUpdate}
|
||||
onClickMore={() => setNewExtensionDialogOpen(true)}
|
||||
i18n={i18n}
|
||||
@@ -266,7 +268,7 @@ export default function InstructionEditorDialog({
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
openInstructionOrExpression={openInstructionOrExpression}
|
||||
ref={instructionParametersEditor}
|
||||
focusOnMount={!!instructionType}
|
||||
focusOnMount={shouldAutofocusInput && !!instructionType}
|
||||
noHelpButton
|
||||
/>
|
||||
);
|
||||
@@ -285,7 +287,7 @@ export default function InstructionEditorDialog({
|
||||
}}
|
||||
selectedType={instructionType}
|
||||
useSubheaders
|
||||
focusOnMount={!instructionType}
|
||||
focusOnMount={shouldAutofocusInput && !instructionType}
|
||||
searchPlaceholderObjectName={chosenObjectName}
|
||||
searchPlaceholderIsCondition={isCondition}
|
||||
onClickMore={() => setNewBehaviorDialogOpen(true)}
|
||||
|
@@ -26,10 +26,6 @@ import {
|
||||
ExplanationHeader,
|
||||
OnlineGameLink,
|
||||
} from '../GenericExporters/OnlineWebExport';
|
||||
import {
|
||||
hasValidSubscriptionPlan,
|
||||
onlineWebExportSizeOptions,
|
||||
} from '../../Utils/GDevelopServices/Usage';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
@@ -156,16 +152,12 @@ export const browserOnlineWebExportPipeline: ExportPipeline<
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
const hasValidSubscription = hasValidSubscriptionPlan(context.subscription);
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
// Higher limit for users with a subscription.
|
||||
sizeOptions: hasValidSubscription
|
||||
? onlineWebExportSizeOptions.subscribed
|
||||
: onlineWebExportSizeOptions.guest,
|
||||
sizeLimit: 250 * 1000 * 1000,
|
||||
});
|
||||
},
|
||||
|
||||
|
@@ -139,13 +139,13 @@ const BuildProgressAndActions = ({
|
||||
t`"${build.name ||
|
||||
shortenUuidForDisplay(
|
||||
build.id
|
||||
)}" will be the new build of this game published on Liluo.io. Continue?`
|
||||
)}" will be the new build of this game published on gd.games. Continue?`
|
||||
)
|
||||
: i18n._(
|
||||
t`"${build.name ||
|
||||
shortenUuidForDisplay(
|
||||
build.id
|
||||
)}" will be unpublished on Liluo.io. Continue?`
|
||||
)}" will be unpublished on gd.games. Continue?`
|
||||
)
|
||||
);
|
||||
if (!answer) return;
|
||||
@@ -272,7 +272,7 @@ const BuildProgressAndActions = ({
|
||||
{game && !!build.s3Key && (
|
||||
<>
|
||||
<Toggle
|
||||
label={<Trans>Publish this build on Liluo.io</Trans>}
|
||||
label={<Trans>Publish this build on gd.games</Trans>}
|
||||
labelPosition="left"
|
||||
toggled={isBuildPublished}
|
||||
onToggle={() => {
|
||||
|
@@ -39,7 +39,6 @@ type ExportHomeProps = {|
|
||||
onlineWebExporter: Exporter,
|
||||
setChosenExporterKey: (key: ExporterKey) => void,
|
||||
setChosenExporterSection: (section: ExporterSection) => void,
|
||||
cantExportBecauseOffline: boolean,
|
||||
project: gdProject,
|
||||
onSaveProject: () => Promise<void>,
|
||||
onChangeSubscription: () => void,
|
||||
@@ -47,13 +46,13 @@ type ExportHomeProps = {|
|
||||
isNavigationDisabled: boolean,
|
||||
setIsNavigationDisabled: (isNavigationDisabled: boolean) => void,
|
||||
onGameUpdated: () => void,
|
||||
showOnlineWebExporterOnly: boolean,
|
||||
|};
|
||||
|
||||
const ExportHome = ({
|
||||
onlineWebExporter,
|
||||
setChosenExporterKey,
|
||||
setChosenExporterSection,
|
||||
cantExportBecauseOffline,
|
||||
project,
|
||||
onSaveProject,
|
||||
onChangeSubscription,
|
||||
@@ -61,14 +60,15 @@ const ExportHome = ({
|
||||
isNavigationDisabled,
|
||||
setIsNavigationDisabled,
|
||||
onGameUpdated,
|
||||
showOnlineWebExporterOnly,
|
||||
}: ExportHomeProps) => {
|
||||
return (
|
||||
<ResponsiveLineStackLayout>
|
||||
<ColumnStackLayout alignItems="center" expand>
|
||||
<div style={styles.titleContainer}>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Publish and share with friends on Liluo.io</Trans>
|
||||
<Text size="block-title" align="center">
|
||||
<Trans>Publish on gd.games, GDevelop's game platform</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</div>
|
||||
@@ -87,53 +87,57 @@ const ExportHome = ({
|
||||
/>
|
||||
</div>
|
||||
</ColumnStackLayout>
|
||||
<ExportHomeSeparator />
|
||||
<ColumnStackLayout alignItems="center" expand>
|
||||
<div style={styles.titleContainer}>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Export and publish on other platforms</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</div>
|
||||
<Line expand>
|
||||
<div style={styles.iconsContainer}>
|
||||
<Chrome style={styles.icon} />
|
||||
<PhoneIphone style={styles.icon} />
|
||||
<LaptopMac style={styles.icon} />
|
||||
</div>
|
||||
</Line>
|
||||
<div style={styles.contentContainer}>
|
||||
<Column alignItems="center">
|
||||
<Line>
|
||||
<Text align="center">
|
||||
<Trans>
|
||||
Export your game to mobile, desktop and web platforms.
|
||||
</Trans>
|
||||
</Text>
|
||||
{showOnlineWebExporterOnly ? null : (
|
||||
<>
|
||||
<ExportHomeSeparator />
|
||||
<ColumnStackLayout alignItems="center" expand>
|
||||
<div style={styles.titleContainer}>
|
||||
<Line>
|
||||
<Text size="block-title" align="center">
|
||||
<Trans>Export and publish on other platforms</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</div>
|
||||
<Line expand>
|
||||
<div style={styles.iconsContainer}>
|
||||
<Chrome style={styles.icon} />
|
||||
<PhoneIphone style={styles.icon} />
|
||||
<LaptopMac style={styles.icon} />
|
||||
</div>
|
||||
</Line>
|
||||
<RaisedButton
|
||||
label={<Trans>Export to other platforms</Trans>}
|
||||
onClick={() => {
|
||||
setChosenExporterSection('automated');
|
||||
setChosenExporterKey('webexport');
|
||||
}}
|
||||
primary
|
||||
disabled={isNavigationDisabled}
|
||||
/>
|
||||
<Spacer />
|
||||
<FlatButton
|
||||
label={<Trans>Build manually</Trans>}
|
||||
primary
|
||||
onClick={() => {
|
||||
setChosenExporterSection('manual');
|
||||
setChosenExporterKey('webexport');
|
||||
}}
|
||||
disabled={isNavigationDisabled}
|
||||
/>
|
||||
</Column>
|
||||
</div>
|
||||
</ColumnStackLayout>
|
||||
<div style={styles.contentContainer}>
|
||||
<Column alignItems="center">
|
||||
<Line>
|
||||
<Text align="center">
|
||||
<Trans>
|
||||
Export your game to mobile, desktop and web platforms.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<RaisedButton
|
||||
label={<Trans>Export to other platforms</Trans>}
|
||||
onClick={() => {
|
||||
setChosenExporterSection('automated');
|
||||
setChosenExporterKey('webexport');
|
||||
}}
|
||||
primary
|
||||
disabled={isNavigationDisabled}
|
||||
/>
|
||||
<Spacer />
|
||||
<FlatButton
|
||||
label={<Trans>Build manually</Trans>}
|
||||
primary
|
||||
onClick={() => {
|
||||
setChosenExporterSection('manual');
|
||||
setChosenExporterKey('webexport');
|
||||
}}
|
||||
disabled={isNavigationDisabled}
|
||||
/>
|
||||
</Column>
|
||||
</div>
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
);
|
||||
};
|
||||
|
@@ -257,7 +257,6 @@ export default class ExportLauncher extends Component<Props, State> {
|
||||
project,
|
||||
updateStepProgress: this._updateStepProgress,
|
||||
exportState: this.state.exportState,
|
||||
subscription: authenticatedUser.subscription,
|
||||
};
|
||||
setStep('export');
|
||||
this.setState({
|
||||
|
@@ -44,8 +44,16 @@ export type ExportDialogWithoutExportsProps = {|
|
||||
|
||||
type Props = {|
|
||||
...ExportDialogWithoutExportsProps,
|
||||
automatedExporters: Array<Exporter>,
|
||||
manualExporters: Array<Exporter>,
|
||||
/**
|
||||
* Use `null` to hide automated exporters.
|
||||
* It should be used with manualExporters set to `null` as well.
|
||||
*/
|
||||
automatedExporters: ?Array<Exporter>,
|
||||
/**
|
||||
* Use `null` to hide manual exporters.
|
||||
* It should be used with automatedExporters set to `null` as well.
|
||||
*/
|
||||
manualExporters: ?Array<Exporter>,
|
||||
onlineWebExporter: Exporter,
|
||||
allExportersRequireOnline?: boolean,
|
||||
|};
|
||||
@@ -83,7 +91,7 @@ const ExportDialog = ({
|
||||
const openBuildDialog = () => {
|
||||
if (!game) {
|
||||
showWarningBox(
|
||||
"Either this game is not registered or you are not its owner, so you can't see the builds or publish a build to the game page on Liluo.io."
|
||||
"Either this game is not registered or you are not its owner, so you can't see the builds or publish a build to the game page on gd.games."
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -121,8 +129,8 @@ const ExportDialog = ({
|
||||
|
||||
if (!project) return null;
|
||||
const exporters = [
|
||||
...automatedExporters,
|
||||
...manualExporters,
|
||||
...(automatedExporters || []),
|
||||
...(manualExporters || []),
|
||||
onlineWebExporter,
|
||||
];
|
||||
|
||||
@@ -132,9 +140,13 @@ const ExportDialog = ({
|
||||
|
||||
if (!exporter || !exporter.exportPipeline) return null;
|
||||
|
||||
const shouldShowOnlineWebExporterOnly =
|
||||
!manualExporters && !automatedExporters;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id="export-dialog"
|
||||
maxWidth={shouldShowOnlineWebExporterOnly ? 'sm' : 'md'}
|
||||
title={
|
||||
chosenExporterSection === 'automated' ? (
|
||||
<Trans>Publish your game</Trans>
|
||||
@@ -183,40 +195,40 @@ const ExportDialog = ({
|
||||
onRequestClose={onClose}
|
||||
open
|
||||
fixedContent={
|
||||
chosenExporterSection === 'automated' ? (
|
||||
<Tabs
|
||||
value={chosenExporterKey}
|
||||
onChange={setChosenExporterKey}
|
||||
options={automatedExporters.map(exporter => ({
|
||||
value: exporter.key,
|
||||
label: exporter.tabName,
|
||||
disabled: isNavigationDisabled,
|
||||
}))}
|
||||
/>
|
||||
) : chosenExporterSection === 'manual' ? (
|
||||
<Tabs
|
||||
value={chosenExporterKey}
|
||||
onChange={setChosenExporterKey}
|
||||
options={manualExporters.map(exporter => ({
|
||||
value: exporter.key,
|
||||
label: exporter.tabName,
|
||||
disabled: isNavigationDisabled,
|
||||
}))}
|
||||
/>
|
||||
automatedExporters && manualExporters ? (
|
||||
chosenExporterSection === 'automated' ? (
|
||||
<Tabs
|
||||
value={chosenExporterKey}
|
||||
onChange={setChosenExporterKey}
|
||||
options={automatedExporters.map(exporter => ({
|
||||
value: exporter.key,
|
||||
label: exporter.tabName,
|
||||
disabled: isNavigationDisabled,
|
||||
}))}
|
||||
/>
|
||||
) : chosenExporterSection === 'manual' ? (
|
||||
<Tabs
|
||||
value={chosenExporterKey}
|
||||
onChange={setChosenExporterKey}
|
||||
options={manualExporters.map(exporter => ({
|
||||
value: exporter.key,
|
||||
label: exporter.tabName,
|
||||
disabled: isNavigationDisabled,
|
||||
}))}
|
||||
/>
|
||||
) : null
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{cantExportBecauseOffline && (
|
||||
{cantExportBecauseOffline ? (
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
You must be online and have a proper internet connection to export
|
||||
your game.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
{chosenExporterSection === 'home' ? (
|
||||
) : chosenExporterSection === 'home' ? (
|
||||
<ExportHome
|
||||
cantExportBecauseOffline={cantExportBecauseOffline}
|
||||
onlineWebExporter={onlineWebExporter}
|
||||
setChosenExporterKey={setChosenExporterKey}
|
||||
setChosenExporterSection={setChosenExporterSection}
|
||||
@@ -227,6 +239,7 @@ const ExportDialog = ({
|
||||
isNavigationDisabled={isNavigationDisabled}
|
||||
setIsNavigationDisabled={setIsNavigationDisabled}
|
||||
onGameUpdated={setGame}
|
||||
showOnlineWebExporterOnly={shouldShowOnlineWebExporterOnly}
|
||||
/>
|
||||
) : (
|
||||
<ExportLauncher
|
||||
|
@@ -3,13 +3,11 @@ import * as React from 'react';
|
||||
import { type Build } from '../Utils/GDevelopServices/Build';
|
||||
import { type AuthenticatedUser } from '../Profile/AuthenticatedUserContext';
|
||||
import { type BuildStep } from './Builds/BuildStepsProgress';
|
||||
import { type Subscription } from '../Utils/GDevelopServices/Usage';
|
||||
|
||||
export type ExportPipelineContext<ExportState> = {|
|
||||
project: gdProject,
|
||||
exportState: ExportState,
|
||||
updateStepProgress: (count: number, total: number) => void,
|
||||
subscription: ?Subscription,
|
||||
|};
|
||||
|
||||
/**
|
||||
|
@@ -19,9 +19,7 @@ import {
|
||||
getGameUrl,
|
||||
updateGame,
|
||||
setGameSlug,
|
||||
getGameSlugs,
|
||||
type Game,
|
||||
type GameSlug,
|
||||
getAclsFromUserIds,
|
||||
setGameUserAcls,
|
||||
} from '../../../Utils/GDevelopServices/Game';
|
||||
@@ -35,6 +33,7 @@ import SocialShareButtons from '../../../UI/ShareDialog/SocialShareButtons';
|
||||
import ShareButton from '../../../UI/ShareDialog/ShareButton';
|
||||
import LinearProgress from '../../../UI/LinearProgress';
|
||||
import CircularProgress from '../../../UI/CircularProgress';
|
||||
import { ResponsiveLineStackLayout } from '../../../UI/Layout';
|
||||
|
||||
type OnlineGameLinkProps = {|
|
||||
build: ?Build,
|
||||
@@ -62,7 +61,6 @@ const OnlineGameLink = ({
|
||||
setIsOnlineGamePropertiesDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const [game, setGame] = React.useState<?Game>(null);
|
||||
const [slug, setSlug] = React.useState<?GameSlug>(null);
|
||||
const [isGameLoading, setIsGameLoading] = React.useState<boolean>(false);
|
||||
const { getAuthorizationHeader, profile } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
@@ -71,7 +69,7 @@ const OnlineGameLink = ({
|
||||
const exportPending = !errored && exportStep !== '' && exportStep !== 'done';
|
||||
const isBuildComplete = build && build.status === 'complete';
|
||||
const isBuildPublished = build && game && build.id === game.publicWebBuildId;
|
||||
const gameUrl = getGameUrl(game, slug);
|
||||
const gameUrl = getGameUrl(game);
|
||||
const buildUrl =
|
||||
exportPending || !isBuildComplete
|
||||
? null
|
||||
@@ -87,16 +85,8 @@ const OnlineGameLink = ({
|
||||
const { id } = profile;
|
||||
try {
|
||||
setIsGameLoading(true);
|
||||
const [game, slugs] = await Promise.all([
|
||||
getGame(getAuthorizationHeader, id, gameId),
|
||||
getGameSlugs(getAuthorizationHeader, id, gameId).catch(err => {
|
||||
console.error('Unable to get the game slug', err);
|
||||
}),
|
||||
]);
|
||||
const game = await getGame(getAuthorizationHeader, id, gameId);
|
||||
setGame(game);
|
||||
if (slugs && slugs.length > 0) {
|
||||
setSlug(slugs[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Unable to load the game', err);
|
||||
} finally {
|
||||
@@ -155,7 +145,6 @@ const OnlineGameLink = ({
|
||||
userSlug,
|
||||
gameSlug
|
||||
);
|
||||
setSlug({ username: userSlug, gameSlug: gameSlug, createdAt: 0 });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Unable to update the game slug:',
|
||||
@@ -210,29 +199,26 @@ const OnlineGameLink = ({
|
||||
try {
|
||||
setIsGameLoading(true);
|
||||
// First update the game.
|
||||
const updatedGame = await updateGame(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
game.id,
|
||||
{
|
||||
gameName: project.getName(),
|
||||
description: project.getDescription(),
|
||||
categories: project.getCategories().toJSArray(),
|
||||
playWithGamepad: project.isPlayableWithGamepad(),
|
||||
playWithKeyboard: project.isPlayableWithKeyboard(),
|
||||
playWithMobile: project.isPlayableWithMobile(),
|
||||
orientation: project.getOrientation(),
|
||||
publicWebBuildId: build.id,
|
||||
thumbnailUrl: getWebBuildThumbnailUrl(project, build.id),
|
||||
discoverable: partialGameChange.discoverable,
|
||||
}
|
||||
);
|
||||
setGame(updatedGame);
|
||||
await updateGame(getAuthorizationHeader, id, game.id, {
|
||||
gameName: project.getName(),
|
||||
description: project.getDescription(),
|
||||
categories: project.getCategories().toJSArray(),
|
||||
playWithGamepad: project.isPlayableWithGamepad(),
|
||||
playWithKeyboard: project.isPlayableWithKeyboard(),
|
||||
playWithMobile: project.isPlayableWithMobile(),
|
||||
orientation: project.getOrientation(),
|
||||
publicWebBuildId: build.id,
|
||||
thumbnailUrl: getWebBuildThumbnailUrl(project, build.id),
|
||||
discoverable: partialGameChange.discoverable,
|
||||
});
|
||||
// Then set authors and slug in parrallel.
|
||||
const [authorsUpdated, slugUpdated] = await Promise.all([
|
||||
tryUpdateAuthors(i18n),
|
||||
tryUpdateSlug(partialGameChange, i18n),
|
||||
]);
|
||||
// Update game again as cached values on the game entity might have changed.
|
||||
await loadGame();
|
||||
// If one of the update failed, return false so that the dialog is not closed.
|
||||
if (!authorsUpdated || !slugUpdated) {
|
||||
return false;
|
||||
}
|
||||
@@ -260,6 +246,7 @@ const OnlineGameLink = ({
|
||||
project,
|
||||
tryUpdateAuthors,
|
||||
tryUpdateSlug,
|
||||
loadGame,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -276,7 +263,7 @@ const OnlineGameLink = ({
|
||||
game && buildUrl && !isBuildPublished && (
|
||||
<DialogPrimaryButton
|
||||
key="publish"
|
||||
label={<Trans>Verify and Publish to Liluo.io</Trans>}
|
||||
label={<Trans>Verify and Publish to gd.games</Trans>}
|
||||
primary
|
||||
onClick={() => setIsOnlineGamePropertiesDialogOpen(true)}
|
||||
/>
|
||||
@@ -316,17 +303,28 @@ const OnlineGameLink = ({
|
||||
<ShareButton url={buildUrl} />
|
||||
)}
|
||||
{isBuildPublished && !navigator.share && (
|
||||
<Line justifyContent="space-between">
|
||||
<Column justifyContent="center">
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
Your game is published! Share it with the community!
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</Column>
|
||||
<Column justifyContent="flex-end">
|
||||
<SocialShareButtons url={buildUrl} />
|
||||
</Column>
|
||||
<Line expand>
|
||||
<ResponsiveLineStackLayout
|
||||
expand
|
||||
justifyContent="space-between"
|
||||
noMargin
|
||||
>
|
||||
<Column justifyContent="center" noMargin>
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
Your game is published! Share it with the
|
||||
community!
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</Column>
|
||||
<Column
|
||||
justifyContent="flex-end"
|
||||
noMargin
|
||||
alignItems="center"
|
||||
>
|
||||
<SocialShareButtons url={buildUrl} />
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Line>
|
||||
)}
|
||||
{!isBuildPublished && game && (
|
||||
@@ -335,7 +333,7 @@ const OnlineGameLink = ({
|
||||
<Trans>
|
||||
This link is private so you can share it with friends
|
||||
and testers. When you're ready you can update your
|
||||
Liluo.io game page.
|
||||
gd.games game page.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</Line>
|
||||
@@ -374,7 +372,6 @@ const OnlineGameLink = ({
|
||||
}
|
||||
}}
|
||||
game={game}
|
||||
slug={slug}
|
||||
isLoading={isGameLoading}
|
||||
/>
|
||||
)}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import { type Game, type GameSlug } from '../../../Utils/GDevelopServices/Game';
|
||||
import { type Game } from '../../../Utils/GDevelopServices/Game';
|
||||
import FlatButton from '../../../UI/FlatButton';
|
||||
import Dialog from '../../../UI/Dialog';
|
||||
import {
|
||||
@@ -22,7 +22,6 @@ type Props = {|
|
||||
onSaveProject: () => Promise<void>,
|
||||
buildId: string,
|
||||
game: Game,
|
||||
slug: ?GameSlug,
|
||||
onClose: () => void,
|
||||
onApply: PartialGameChange => Promise<void>,
|
||||
isLoading: boolean,
|
||||
@@ -33,7 +32,6 @@ export const OnlineGamePropertiesDialog = ({
|
||||
onSaveProject,
|
||||
buildId,
|
||||
game,
|
||||
slug,
|
||||
onClose,
|
||||
onApply,
|
||||
isLoading,
|
||||
@@ -63,10 +61,13 @@ export const OnlineGamePropertiesDialog = ({
|
||||
project.isPlayableWithMobile()
|
||||
);
|
||||
const [userSlug, setUserSlug] = React.useState<string>(
|
||||
(slug && slug.username) || (profile && profile.username) || ''
|
||||
(game.cachedCurrentSlug && game.cachedCurrentSlug.username) ||
|
||||
(profile && profile.username) ||
|
||||
''
|
||||
);
|
||||
const [gameSlug, setGameSlug] = React.useState<string>(
|
||||
(slug && slug.gameSlug) || cleanUpGameSlug(project.getName())
|
||||
(game.cachedCurrentSlug && game.cachedCurrentSlug.gameSlug) ||
|
||||
cleanUpGameSlug(project.getName())
|
||||
);
|
||||
const [orientation, setOrientation] = React.useState<string>(
|
||||
project.getOrientation()
|
||||
|
@@ -22,10 +22,6 @@ import {
|
||||
OnlineGameLink,
|
||||
} from '../GenericExporters/OnlineWebExport';
|
||||
import { downloadUrlsToLocalFiles } from '../../Utils/LocalFileDownloader';
|
||||
import {
|
||||
hasValidSubscriptionPlan,
|
||||
onlineWebExportSizeOptions,
|
||||
} from '../../Utils/GDevelopServices/Usage';
|
||||
const path = optionalRequire('path');
|
||||
const os = optionalRequire('os');
|
||||
const gd: libGDevelop = global.gd;
|
||||
@@ -160,15 +156,11 @@ export const localOnlineWebExportPipeline: ExportPipeline<
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ temporaryOutputDir }: ResourcesDownloadOutput
|
||||
): Promise<CompressionOutput> => {
|
||||
const hasValidSubscription = hasValidSubscriptionPlan(context.subscription);
|
||||
const archiveOutputDir = os.tmpdir();
|
||||
return archiveLocalFolder({
|
||||
path: temporaryOutputDir,
|
||||
outputFilename: path.join(archiveOutputDir, 'game-archive.zip'),
|
||||
// Higher limit for users with a subscription.
|
||||
sizeOptions: hasValidSubscription
|
||||
? onlineWebExportSizeOptions.subscribed
|
||||
: onlineWebExportSizeOptions.guest,
|
||||
sizeLimit: 250 * 1000 * 1000,
|
||||
});
|
||||
},
|
||||
|
||||
|
@@ -21,6 +21,7 @@ const removeServerListeners = () => {
|
||||
ipcRenderer.removeAllListeners('debugger-error-received');
|
||||
ipcRenderer.removeAllListeners('debugger-connection-closed');
|
||||
ipcRenderer.removeAllListeners('debugger-connection-opened');
|
||||
ipcRenderer.removeAllListeners('debugger-connection-errored');
|
||||
ipcRenderer.removeAllListeners('debugger-start-server-done');
|
||||
ipcRenderer.removeAllListeners('debugger-message-received');
|
||||
};
|
||||
@@ -71,6 +72,18 @@ export const localPreviewDebuggerServer: PreviewDebuggerServer = {
|
||||
);
|
||||
});
|
||||
|
||||
ipcRenderer.on(
|
||||
'debugger-connection-errored',
|
||||
(event, { id, errorMessage }) => {
|
||||
callbacksList.forEach(({ onConnectionErrored }) =>
|
||||
onConnectionErrored({
|
||||
id,
|
||||
errorMessage,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ipcRenderer.on('debugger-start-server-done', (event, { address }) => {
|
||||
console.info('Local preview debugger started');
|
||||
debuggerServerState = 'started';
|
||||
|
@@ -35,6 +35,10 @@ export type PreviewDebuggerServerCallbacks = {|
|
||||
id: DebuggerId,
|
||||
debuggerIds: Array<DebuggerId>,
|
||||
|}) => void,
|
||||
onConnectionErrored: ({|
|
||||
id: DebuggerId,
|
||||
errorMessage: string,
|
||||
|}) => void,
|
||||
onHandleParsedMessage: ({| id: DebuggerId, parsedMessage: Object |}) => void,
|
||||
|};
|
||||
|
||||
|
@@ -330,7 +330,7 @@ const GameFeedback = ({ i18n, authenticatedUser, game }: Props) => {
|
||||
<BackgroundText>
|
||||
<Trans>
|
||||
This is all the feedback received on {game.gameName}{' '}
|
||||
coming from Liluo.
|
||||
coming from gd.games.
|
||||
</Trans>
|
||||
</BackgroundText>
|
||||
</LineStackLayout>
|
||||
|
@@ -49,33 +49,33 @@ type TogglableProperties =
|
||||
const confirmationMessage = {
|
||||
discoverable: {
|
||||
true: t`
|
||||
You are about to make this game discoverable on Liluo.io categories pages.
|
||||
You are about to make this game discoverable on gd.games categories pages.
|
||||
Do you want to continue?
|
||||
`,
|
||||
false: t`
|
||||
You are about to hide this game from Liluo.io categories pages.
|
||||
You are about to hide this game from gd.games categories pages.
|
||||
Do you want to continue?
|
||||
`,
|
||||
},
|
||||
acceptsBuildComments: {
|
||||
true: t`
|
||||
You are about to activate a feedback banner on all builds of this game.
|
||||
By doing this you're allowing feedback from any player who has access to your Liluo.io build URLs.
|
||||
By doing this you're allowing feedback from any player who has access to your gd.games build URLs.
|
||||
Do you want to continue?
|
||||
`,
|
||||
false: t`
|
||||
You are about to de-activate the feedback banner on all your Liluo.io build pages.
|
||||
You are about to de-activate the feedback banner on all your gd.games build pages.
|
||||
Do you want to continue ?
|
||||
`,
|
||||
},
|
||||
acceptsGameComments: {
|
||||
true: t`
|
||||
You are about to activate a feedback banner on your Liluo.io game page.
|
||||
By doing this you will receive feedback from any Liluo.io visitor.
|
||||
You are about to activate a feedback banner on your gd.games game page.
|
||||
By doing this you will receive feedback from any gd.games visitor.
|
||||
Do you want to continue?
|
||||
`,
|
||||
false: t`
|
||||
You are about to de-activate the feedback banner on your Liluo.io game page.
|
||||
You are about to de-activate the feedback banner on your gd.games game page.
|
||||
Do you want to continue ?
|
||||
`,
|
||||
},
|
||||
@@ -230,9 +230,9 @@ export const GameCard = ({
|
||||
<>
|
||||
<Text size="body2" noMargin displayInlineAsSpan>
|
||||
{game.discoverable ? (
|
||||
<Trans>Public on Liluo.io</Trans>
|
||||
<Trans>Public on gd.games</Trans>
|
||||
) : (
|
||||
<Trans>Not visible on Liluo.io</Trans>
|
||||
<Trans>Not visible on gd.games</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Spacer />
|
||||
@@ -305,7 +305,7 @@ export const GameCard = ({
|
||||
onToggle(i18n, 'discoverable', !game.discoverable);
|
||||
}}
|
||||
toggled={!!game.discoverable}
|
||||
label={<Trans>Make discoverable on Liluo.io</Trans>}
|
||||
label={<Trans>Make discoverable on gd.games</Trans>}
|
||||
disabled={
|
||||
editedProperty === 'discoverable' || isDeletingGame
|
||||
}
|
||||
@@ -321,7 +321,7 @@ export const GameCard = ({
|
||||
}}
|
||||
toggled={!!game.acceptsGameComments}
|
||||
label={
|
||||
<Trans>Show feedback banner on Liluo.io game page</Trans>
|
||||
<Trans>Show feedback banner on gd.games game page</Trans>
|
||||
}
|
||||
disabled={
|
||||
editedProperty === 'acceptsGameComments' || isDeletingGame
|
||||
|
@@ -553,14 +553,14 @@ export const GameDetailsDialog = ({
|
||||
<RaisedButton
|
||||
onClick={() => {
|
||||
const answer = Window.showConfirmDialog(
|
||||
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo.io unique game URL not accessible anymore. \n\nYou can decide at any time to publish it again.'
|
||||
'Are you sure you want to unpublish this game? \n\nThis will make your gd.games unique game URL not accessible anymore. \n\nYou can decide at any time to publish it again.'
|
||||
);
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
unpublishGame();
|
||||
}}
|
||||
label={<Trans>Unpublish from Liluo.io</Trans>}
|
||||
label={<Trans>Unpublish from gd.games</Trans>}
|
||||
disabled={isGameUpdating}
|
||||
/>
|
||||
<Spacer />
|
||||
|
@@ -220,7 +220,7 @@ export const GameRegistration = ({
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Trans>Get stats about your game every week!</Trans>
|
||||
<Trans>Receive weekly stats about your game by email!</Trans>
|
||||
</AlertMessage>
|
||||
);
|
||||
}
|
||||
|
@@ -336,10 +336,10 @@ function LeaderboardAppearanceDialog({
|
||||
</Trans>
|
||||
</Text>
|
||||
<Link
|
||||
href="https://liluo.io/playground/test-leaderboard"
|
||||
href="https://gd.games/playground/test-leaderboard"
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
'https://liluo.io/playground/test-leaderboard'
|
||||
'https://gd.games/playground/test-leaderboard'
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@@ -25,10 +25,10 @@ const LeaderboardPlaygroundCard = () => {
|
||||
</Column>
|
||||
<Column>
|
||||
<Link
|
||||
href="https://liluo.io/playground/test-leaderboard"
|
||||
href="https://gd.games/playground/test-leaderboard"
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
'https://liluo.io/playground/test-leaderboard'
|
||||
'https://gd.games/playground/test-leaderboard'
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@@ -68,13 +68,13 @@ export const GameMonetization = ({ game, onGameUpdated }: Props) => {
|
||||
}}
|
||||
label={
|
||||
<Trans>
|
||||
Allow to display advertisments on the game page on Liluo.io.
|
||||
Allow to display advertisments on the game page on gd.games.
|
||||
</Trans>
|
||||
}
|
||||
tooltipOrHelperText={
|
||||
<Trans>
|
||||
This is recommended as this allows to maintain free publishing
|
||||
on Liluo.io and allow to analyze if you could benefit from
|
||||
on gd.games and allow to analyze if you could benefit from
|
||||
revenue sharing.
|
||||
</Trans>
|
||||
}
|
||||
|
@@ -168,8 +168,8 @@ export function PublicGameProperties({
|
||||
floatingLabelText={<Trans>Genres</Trans>}
|
||||
helperText={
|
||||
<Trans>
|
||||
Select up to 4 genres for the game to be visible on
|
||||
Liluo.io's categories pages!
|
||||
Select up to 3 genres for the game to be visible on
|
||||
gd.games's categories pages!
|
||||
</Trans>
|
||||
}
|
||||
value={
|
||||
@@ -201,14 +201,14 @@ export function PublicGameProperties({
|
||||
disabled: category.type === 'admin-only',
|
||||
}))}
|
||||
fullWidth
|
||||
optionsLimit={4}
|
||||
optionsLimit={3}
|
||||
disabled={disabled}
|
||||
loading={allGameCategories.length === 0}
|
||||
/>
|
||||
)}
|
||||
{setDiscoverable && (
|
||||
<Checkbox
|
||||
label={<Trans>Make your game discoverable on Liluo.io</Trans>}
|
||||
label={<Trans>Make your game discoverable on gd.games</Trans>}
|
||||
checked={!!discoverable}
|
||||
onCheck={(e, checked) => setDiscoverable(checked)}
|
||||
disabled={disabled}
|
||||
|
@@ -4,61 +4,21 @@ import { Trans } from '@lingui/macro';
|
||||
|
||||
import Dialog from '../UI/Dialog';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import { Column } from '../UI/Grid';
|
||||
import InfoBar from '../UI/Messages/InfoBar';
|
||||
import SocialShareButtons from '../UI/ShareDialog/SocialShareButtons';
|
||||
import ShareLink from '../UI/ShareDialog/ShareLink';
|
||||
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
type GameSlug,
|
||||
type Game,
|
||||
getGameSlugs,
|
||||
getGameUrl,
|
||||
} from '../Utils/GDevelopServices/Game';
|
||||
import { type Game, getGameUrl } from '../Utils/GDevelopServices/Game';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import ShareButton from '../UI/ShareDialog/ShareButton';
|
||||
import CircularProgress from '../UI/CircularProgress';
|
||||
import Text from '../UI/Text';
|
||||
|
||||
type Props = {| game: Game, onClose: () => void |};
|
||||
|
||||
const ShareDialog = ({ game, onClose }: Props) => {
|
||||
const [isFetchingGameSlug, setIsFetchingGameSlug] = React.useState(true);
|
||||
const [gameSlug, setGameSlug] = React.useState<?GameSlug>(null);
|
||||
const [showCopiedInfoBar, setShowCopiedInfoBar] = React.useState(false);
|
||||
const [showAlertMessage, setShowAlertMessage] = React.useState(false);
|
||||
const { getAuthorizationHeader, profile } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const fetchGameSlug = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
try {
|
||||
const gameSlugs = await getGameSlugs(
|
||||
getAuthorizationHeader,
|
||||
profile.id,
|
||||
game.id
|
||||
);
|
||||
setGameSlug(gameSlugs[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setShowAlertMessage(true);
|
||||
} finally {
|
||||
setIsFetchingGameSlug(false);
|
||||
}
|
||||
},
|
||||
[getAuthorizationHeader, profile, game]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
fetchGameSlug();
|
||||
},
|
||||
[fetchGameSlug]
|
||||
);
|
||||
|
||||
const gameUrl = getGameUrl(game, gameSlug);
|
||||
const gameUrl = getGameUrl(game);
|
||||
|
||||
if (!gameUrl) return null;
|
||||
return (
|
||||
@@ -75,26 +35,14 @@ const ShareDialog = ({ game, onClose }: Props) => {
|
||||
]}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
{!isFetchingGameSlug ? (
|
||||
<Column>
|
||||
<ShareLink url={gameUrl} />
|
||||
{navigator.share ? (
|
||||
<ShareButton url={gameUrl} />
|
||||
) : (
|
||||
<SocialShareButtons url={gameUrl} />
|
||||
)}
|
||||
</Column>
|
||||
) : (
|
||||
<Column alignItems="center">
|
||||
<Line>
|
||||
<CircularProgress />
|
||||
</Line>
|
||||
<Text>
|
||||
<Trans>Just a few seconds while we generate the link...</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
)}
|
||||
|
||||
<Column>
|
||||
<ShareLink url={gameUrl} />
|
||||
{navigator.share ? (
|
||||
<ShareButton url={gameUrl} />
|
||||
) : (
|
||||
<SocialShareButtons url={gameUrl} />
|
||||
)}
|
||||
</Column>
|
||||
<InfoBar
|
||||
message={<Trans>Copied to clipboard!</Trans>}
|
||||
visible={showCopiedInfoBar}
|
||||
|
@@ -41,6 +41,13 @@ export const styles = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
drawerTopButtonsContainer: {
|
||||
flex: 1,
|
||||
marginTop: 'env(safe-area-inset-top)',
|
||||
},
|
||||
bottomButtonsContainer: {
|
||||
marginBottom: 'env(safe-area-inset-bottom)',
|
||||
},
|
||||
};
|
||||
|
||||
export type HomeTab =
|
||||
@@ -165,19 +172,22 @@ export const HomePageMenu = ({
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
<Column>
|
||||
{buttons.map(({ label, getIcon, onClick, id }) => (
|
||||
<VerticalTabButton
|
||||
key={id}
|
||||
label={label}
|
||||
onClick={onClick}
|
||||
getIcon={getIcon}
|
||||
isActive={false}
|
||||
hideLabel={windowWidth !== 'large'}
|
||||
id={id}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
|
||||
<div style={styles.bottomButtonsContainer}>
|
||||
<Column>
|
||||
{buttons.map(({ label, getIcon, onClick, id }) => (
|
||||
<VerticalTabButton
|
||||
key={id}
|
||||
label={label}
|
||||
onClick={onClick}
|
||||
getIcon={getIcon}
|
||||
isActive={false}
|
||||
hideLabel={windowWidth !== 'large'}
|
||||
id={id}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
</div>
|
||||
</Paper>
|
||||
<Drawer
|
||||
open={isHomePageMenuDrawerOpen}
|
||||
@@ -197,41 +207,45 @@ export const HomePageMenu = ({
|
||||
>
|
||||
<Line expand>
|
||||
<Column expand>
|
||||
<Column noMargin expand>
|
||||
<Line noMargin justifyContent="flex-end">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setIsHomePageMenuDrawerOpen(false);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<DoubleChevronArrowLeft />
|
||||
</IconButton>
|
||||
</Line>
|
||||
{tabs.map(({ label, tab, getIcon }, index) => (
|
||||
<VerticalTabButton
|
||||
key={index}
|
||||
label={label}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setIsHomePageMenuDrawerOpen(false);
|
||||
}}
|
||||
getIcon={getIcon}
|
||||
isActive={activeTab === tab}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
<Column noMargin>
|
||||
{buttons.map(({ label, getIcon, onClick, id }) => (
|
||||
<VerticalTabButton
|
||||
key={id}
|
||||
label={label}
|
||||
onClick={onClick}
|
||||
getIcon={getIcon}
|
||||
isActive={false}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
<div style={styles.drawerTopButtonsContainer}>
|
||||
<Column noMargin expand>
|
||||
<Line noMargin justifyContent="flex-end">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setIsHomePageMenuDrawerOpen(false);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<DoubleChevronArrowLeft />
|
||||
</IconButton>
|
||||
</Line>
|
||||
{tabs.map(({ label, tab, getIcon }, index) => (
|
||||
<VerticalTabButton
|
||||
key={index}
|
||||
label={label}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setIsHomePageMenuDrawerOpen(false);
|
||||
}}
|
||||
getIcon={getIcon}
|
||||
isActive={activeTab === tab}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
</div>
|
||||
<div style={styles.bottomButtonsContainer}>
|
||||
<Column noMargin>
|
||||
{buttons.map(({ label, getIcon, onClick, id }) => (
|
||||
<VerticalTabButton
|
||||
key={id}
|
||||
label={label}
|
||||
onClick={onClick}
|
||||
getIcon={getIcon}
|
||||
isActive={false}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
</div>
|
||||
</Column>
|
||||
</Line>
|
||||
</Drawer>
|
||||
|
@@ -18,7 +18,7 @@ const PlaySection = () => {
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if (
|
||||
event.origin === 'https://liluo.io' &&
|
||||
event.origin === 'https://gd.games' &&
|
||||
event.data.id === 'set-embedded-height'
|
||||
) {
|
||||
setIframeHeight(event.data.height);
|
||||
@@ -33,8 +33,8 @@ const PlaySection = () => {
|
||||
>
|
||||
<SectionRow expand>
|
||||
<iframe
|
||||
src={`https://liluo.io/embedded/${paletteType}`}
|
||||
title="Liluo"
|
||||
src={`https://gd.games/embedded/${paletteType}`}
|
||||
title="gdgames"
|
||||
style={{ ...styles.iframe, height: iframeHeight }}
|
||||
scrolling="no" // This is deprecated, but this is the only way to disable the scrollbar.
|
||||
/>
|
||||
|
@@ -271,8 +271,8 @@ export const buildMainMenuDeclarativeTemplate = ({
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`GDevelop games on Liluo.io`),
|
||||
onClickOpenLink: 'https://liluo.io',
|
||||
label: i18n._(t`GDevelop games on gd.games`),
|
||||
onClickOpenLink: 'https://gd.games',
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Community Forums`),
|
||||
|
@@ -55,6 +55,9 @@ export const usePreviewDebuggerServerWatcher = (
|
||||
onConnectionOpened: ({ id, debuggerIds }) => {
|
||||
setDebuggerIds([...debuggerIds]);
|
||||
},
|
||||
onConnectionErrored: ({ id }) => {
|
||||
// Nothing to do (onConnectionClosed is called if necessary).
|
||||
},
|
||||
onServerStateChanged: () => {
|
||||
// Nothing to do.
|
||||
},
|
||||
|
@@ -875,22 +875,27 @@ const MainFrame = (props: Props) => {
|
||||
// Try to find an autosave (and ask user if found)
|
||||
try {
|
||||
await delay(150);
|
||||
const autoSavefileMetadata = await checkForAutosave();
|
||||
const autoSaveFileMetadata = await checkForAutosave();
|
||||
let content;
|
||||
try {
|
||||
const result = await onOpen(
|
||||
autoSavefileMetadata,
|
||||
autoSaveFileMetadata,
|
||||
setLoaderModalProgress
|
||||
);
|
||||
content = result.content;
|
||||
} catch (error) {
|
||||
// onOpen failed, tried to find again an autosave
|
||||
const autoSaveAfterFailurefileMetadata = await checkForAutosaveAfterFailure();
|
||||
if (autoSaveAfterFailurefileMetadata) {
|
||||
const result = await onOpen(autoSaveAfterFailurefileMetadata);
|
||||
// onOpen failed, try to find again an autosave.
|
||||
const autoSaveAfterFailureFileMetadata = await checkForAutosaveAfterFailure();
|
||||
if (autoSaveAfterFailureFileMetadata) {
|
||||
const result = await onOpen(autoSaveAfterFailureFileMetadata);
|
||||
content = result.content;
|
||||
}
|
||||
}
|
||||
if (!content) {
|
||||
throw new Error(
|
||||
'The project file content could not be read. It might be corrupted/malformed.'
|
||||
);
|
||||
}
|
||||
if (!verifyProjectContent(i18n, content)) {
|
||||
// The content is not recognized and the user was warned. Abort the opening.
|
||||
setIsLoadingProject(false);
|
||||
|
@@ -186,12 +186,12 @@ export default class RenderedCustomObjectInstance extends RenderedInstance
|
||||
update() {
|
||||
applyChildLayouts(this);
|
||||
|
||||
const defaultWidth = this.getDefaultWidth();
|
||||
const defaultHeight = this.getDefaultHeight();
|
||||
const originX = this._proportionalOriginX * defaultWidth;
|
||||
const originY = this._proportionalOriginY * defaultHeight;
|
||||
const centerX = defaultWidth / 2;
|
||||
const centerY = defaultHeight / 2;
|
||||
const width = this.getWidth();
|
||||
const height = this.getHeight();
|
||||
const originX = this._proportionalOriginX * width;
|
||||
const originY = this._proportionalOriginY * height;
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
this._pixiObject.pivot.x = centerX;
|
||||
this._pixiObject.pivot.y = centerY;
|
||||
@@ -200,12 +200,8 @@ export default class RenderedCustomObjectInstance extends RenderedInstance
|
||||
);
|
||||
this._pixiObject.scale.x = 1;
|
||||
this._pixiObject.scale.y = 1;
|
||||
this._pixiObject.position.x =
|
||||
this._instance.getX() +
|
||||
(centerX - originX) * Math.abs(this._pixiObject.scale.x);
|
||||
this._pixiObject.position.y =
|
||||
this._instance.getY() +
|
||||
(centerY - originY) * Math.abs(this._pixiObject.scale.y);
|
||||
this._pixiObject.position.x = this._instance.getX() + centerX - originX;
|
||||
this._pixiObject.position.y = this._instance.getY() + centerY - originY;
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
@@ -233,18 +229,10 @@ export default class RenderedCustomObjectInstance extends RenderedInstance
|
||||
}
|
||||
|
||||
getOriginX(): number {
|
||||
return (
|
||||
this._proportionalOriginX *
|
||||
this.getDefaultWidth() *
|
||||
this._pixiObject.scale.x
|
||||
);
|
||||
return this._proportionalOriginX * this.getWidth();
|
||||
}
|
||||
|
||||
getOriginY(): number {
|
||||
return (
|
||||
this._proportionalOriginY *
|
||||
this.getDefaultHeight() *
|
||||
this._pixiObject.scale.y
|
||||
);
|
||||
return this._proportionalOriginY * this.getHeight();
|
||||
}
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ type State = {|
|
||||
androidIconResourceNames: Array<string>,
|
||||
androidWindowSplashScreenAnimatedIconResourceName: string,
|
||||
iosIconResourceNames: Array<string>,
|
||||
displayLiluoThumbnailWarning: boolean,
|
||||
displayGamesPlatformThumbnailWarning: boolean,
|
||||
|};
|
||||
|
||||
const desktopSizes = [512];
|
||||
@@ -80,7 +80,7 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
_loadFrom(project: gdProject): State {
|
||||
const platformSpecificAssets = project.getPlatformSpecificAssets();
|
||||
return {
|
||||
thumbnailResourceName: platformSpecificAssets.get('liluo', `thumbnail`),
|
||||
thumbnailResourceName: platformSpecificAssets.get('liluo', 'thumbnail'),
|
||||
desktopIconResourceNames: desktopSizes.map(size =>
|
||||
platformSpecificAssets.get('desktop', `icon-${size}`)
|
||||
),
|
||||
@@ -93,7 +93,7 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
iosIconResourceNames: iosSizes.map(size =>
|
||||
platformSpecificAssets.get('ios', `icon-${size}`)
|
||||
),
|
||||
displayLiluoThumbnailWarning: false,
|
||||
displayGamesPlatformThumbnailWarning: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
androidIconResourceNames,
|
||||
androidWindowSplashScreenAnimatedIconResourceName,
|
||||
iosIconResourceNames,
|
||||
displayLiluoThumbnailWarning,
|
||||
displayGamesPlatformThumbnailWarning,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
@@ -336,10 +336,10 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
)}
|
||||
</Line>
|
||||
<Text size="sub-title">
|
||||
<Trans>Liluo.io thumbnail</Trans>
|
||||
<Trans>gd.games thumbnail</Trans>
|
||||
</Text>
|
||||
<ResourceSelectorWithThumbnail
|
||||
floatingLabelText={`Liluo.io thumbnail (1920x1080 px)`}
|
||||
floatingLabelText={`gd.games thumbnail (1920x1080 px)`}
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
resourceKind="image"
|
||||
@@ -347,18 +347,18 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
|
||||
onChange={resourceName => {
|
||||
this.setState({
|
||||
thumbnailResourceName: resourceName,
|
||||
displayLiluoThumbnailWarning:
|
||||
displayGamesPlatformThumbnailWarning:
|
||||
resourceName !== this.state.thumbnailResourceName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{displayLiluoThumbnailWarning ? (
|
||||
{displayGamesPlatformThumbnailWarning ? (
|
||||
<Line>
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
You're about to change the thumbnail displayed on Liluo.io for
|
||||
You're about to change the thumbnail displayed on gd.games for
|
||||
your game. Once you have applied changes here, you will then
|
||||
need to publish a new version of your game on Liluo.io so that
|
||||
need to publish a new version of your game on gd.games so that
|
||||
this new thumbnail is used.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
|
@@ -171,7 +171,7 @@ const EditProfileDialog = ({
|
||||
disabled={updateProfileInProgress}
|
||||
floatingLabelFixed
|
||||
helperMarkdownText={i18n._(
|
||||
t`Add a link to your donation page. It will be displayed on your Liluo.io profile and game pages.`
|
||||
t`Add a link to your donation page. It will be displayed on your gd.games profile and game pages.`
|
||||
)}
|
||||
errorText={donateLinkFormattingError}
|
||||
/>
|
||||
|
@@ -7,7 +7,7 @@ import WarningIcon from '@material-ui/icons/Warning';
|
||||
|
||||
import { type MenuItemTemplate } from '../UI/Menu/Menu.flow';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
import { IconContainer } from '../UI/IconContainer';
|
||||
import { IconContainer, iconWithBackgroundStyle } from '../UI/IconContainer';
|
||||
import { ListItem } from '../UI/List';
|
||||
import TextField, {
|
||||
type TextFieldInterface,
|
||||
@@ -24,6 +24,7 @@ import { type ExtensionShortHeader } from '../Utils/GDevelopServices/Extension';
|
||||
import GDevelopThemeContext from '../UI/Theme/ThemeContext';
|
||||
import Text from '../UI/Text';
|
||||
import DropIndicator from '../UI/SortableVirtualizedItemList/DropIndicator';
|
||||
import Lightning from '../UI/CustomSvgIcons/Lightning';
|
||||
|
||||
const styles = {
|
||||
noIndentNestedList: {
|
||||
@@ -33,6 +34,12 @@ const styles = {
|
||||
top: noMarginTextFieldInListItemTopOffset,
|
||||
},
|
||||
dragAndDropItemContainer: { display: 'flex', flexDirection: 'column' },
|
||||
draggableIcon: { display: 'flex' },
|
||||
extensionPlaceholderIconContainer: {
|
||||
...iconWithBackgroundStyle,
|
||||
display: 'flex',
|
||||
color: '#1D1D26',
|
||||
},
|
||||
};
|
||||
|
||||
type ProjectStructureItemProps = {|
|
||||
@@ -84,7 +91,7 @@ type ItemProps = {|
|
||||
primaryText: string,
|
||||
textEndAdornment?: React.Node,
|
||||
editingName: boolean,
|
||||
leftIcon?: ?React.Node,
|
||||
leftIcon: React.Element<any>,
|
||||
onEdit: () => void,
|
||||
onDelete: () => void,
|
||||
addLabel: string,
|
||||
@@ -228,86 +235,84 @@ export const Item = ({
|
||||
connectDropTarget(
|
||||
<div style={styles.dragAndDropItemContainer}>
|
||||
{isOver && <DropIndicator canDrop={canDrop} zIndex={1} />}
|
||||
{connectDragSource(
|
||||
<div style={styles.dragAndDropItemContainer}>
|
||||
<ListItem
|
||||
id={id}
|
||||
data={data}
|
||||
style={
|
||||
isLastItem
|
||||
? undefined
|
||||
: {
|
||||
borderBottom: `1px solid ${
|
||||
gdevelopTheme.listItem.separatorColor
|
||||
}`,
|
||||
}
|
||||
}
|
||||
noPadding
|
||||
primaryText={label}
|
||||
leftIcon={leftIcon}
|
||||
displayMenuButton
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Edit`),
|
||||
click: onEdit,
|
||||
},
|
||||
...(buildExtraMenuTemplate
|
||||
? buildExtraMenuTemplate(i18n)
|
||||
: []),
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Rename`),
|
||||
click: onEditName,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
click: onDelete,
|
||||
},
|
||||
{
|
||||
label: i18n._(addLabel),
|
||||
visible: !!onAdd,
|
||||
click: onAdd,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Copy`),
|
||||
click: onCopy,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Cut`),
|
||||
click: onCut,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Paste`),
|
||||
enabled: canPaste(),
|
||||
click: onPaste,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Duplicate`),
|
||||
click: onDuplicate,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Move up`),
|
||||
enabled: canMoveUp,
|
||||
click: onMoveUp,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Move down`),
|
||||
enabled: canMoveDown,
|
||||
click: onMoveDown,
|
||||
},
|
||||
]}
|
||||
onClick={() => {
|
||||
// It's essential to discard clicks when editing the name,
|
||||
// to avoid weird opening of an editor (accompanied with a
|
||||
// closing of the project manager) when clicking on the text
|
||||
// field.
|
||||
if (!editingName) onEdit();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ListItem
|
||||
id={id}
|
||||
data={data}
|
||||
style={
|
||||
isLastItem
|
||||
? undefined
|
||||
: {
|
||||
borderBottom: `1px solid ${
|
||||
gdevelopTheme.listItem.separatorColor
|
||||
}`,
|
||||
}
|
||||
}
|
||||
noPadding
|
||||
primaryText={label}
|
||||
leftIcon={connectDragSource(
|
||||
<div style={styles.draggableIcon}>{leftIcon}</div>
|
||||
)}
|
||||
displayMenuButton
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Edit`),
|
||||
click: onEdit,
|
||||
},
|
||||
...(buildExtraMenuTemplate
|
||||
? buildExtraMenuTemplate(i18n)
|
||||
: []),
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Rename`),
|
||||
click: onEditName,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
click: onDelete,
|
||||
},
|
||||
{
|
||||
label: i18n._(addLabel),
|
||||
visible: !!onAdd,
|
||||
click: onAdd,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Copy`),
|
||||
click: onCopy,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Cut`),
|
||||
click: onCut,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Paste`),
|
||||
enabled: canPaste(),
|
||||
click: onPaste,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Duplicate`),
|
||||
click: onDuplicate,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Move up`),
|
||||
enabled: canMoveUp,
|
||||
click: onMoveUp,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Move down`),
|
||||
enabled: canMoveDown,
|
||||
click: onMoveDown,
|
||||
},
|
||||
]}
|
||||
onClick={() => {
|
||||
// It's essential to discard clicks when editing the name,
|
||||
// to avoid weird opening of an editor (accompanied with a
|
||||
// closing of the project manager) when clicking on the text
|
||||
// field.
|
||||
if (!editingName) onEdit();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -378,7 +383,13 @@ export const EventFunctionExtensionItem = ({
|
||||
alt={eventsFunctionsExtension.getFullName()}
|
||||
src={iconUrl}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
// Use icon placeholder so that the user can drag and drop the
|
||||
// item in the project manager.
|
||||
<div style={styles.extensionPlaceholderIconContainer}>
|
||||
<Lightning />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
primaryText={name}
|
||||
editingName={isEditingName}
|
||||
|
@@ -97,12 +97,12 @@ const initialMosaicEditorNodes = {
|
||||
},
|
||||
};
|
||||
|
||||
const initialMosaicEditorNodesSmallWindow = {
|
||||
direction: 'row',
|
||||
const getInitialMosaicEditorNodesSmallWindow = () => ({
|
||||
direction: Window.getOrientation() === 'portrait' ? 'column' : 'row',
|
||||
first: 'instances-editor',
|
||||
second: 'objects-list',
|
||||
splitPercentage: 70,
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {|
|
||||
initialInstances: gdInitialInstancesContainer,
|
||||
@@ -500,6 +500,7 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
});
|
||||
|
||||
if (this._objectsList) this._objectsList.openNewObjectDialog();
|
||||
else this.openObjectsList();
|
||||
};
|
||||
|
||||
_onAddInstanceUnderCursor = () => {
|
||||
@@ -1589,7 +1590,7 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
initialNodes={
|
||||
windowWidth === 'small'
|
||||
? getDefaultEditorMosaicNode('scene-editor-small') ||
|
||||
initialMosaicEditorNodesSmallWindow
|
||||
getInitialMosaicEditorNodesSmallWindow()
|
||||
: getDefaultEditorMosaicNode('scene-editor') ||
|
||||
initialMosaicEditorNodes
|
||||
}
|
||||
|
13
newIDE/app/src/UI/CustomSvgIcons/Lightning.js
Normal file
13
newIDE/app/src/UI/CustomSvgIcons/Lightning.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default React.memo(props => (
|
||||
<SvgIcon {...props} width="17" height="16" viewBox="0 0 17 16" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.38082 2.20631C9.58606 2.29331 9.70892 2.50568 9.68203 2.72698L9.2316 6.43332H13.3338C13.5266 6.43332 13.7022 6.54413 13.7851 6.71812C13.8681 6.89212 13.8437 7.0983 13.7223 7.24808L8.53714 13.6481C8.3968 13.8213 8.15875 13.8807 7.95351 13.7937C7.74826 13.7067 7.6254 13.4943 7.65229 13.273L8.10272 9.56666H4.00049C3.80773 9.56666 3.63214 9.45585 3.54917 9.28186C3.46621 9.10786 3.49065 8.90168 3.612 8.7519L8.79718 2.3519C8.93752 2.17869 9.17557 2.1193 9.38082 2.20631ZM5.04909 8.56666H8.66716C8.81028 8.56666 8.94653 8.62799 9.04143 8.73512C9.13633 8.84224 9.18077 8.9849 9.16351 9.12698L8.85476 11.6675L12.2852 7.43332H8.66716C8.52404 7.43332 8.38779 7.37199 8.29289 7.26486C8.19799 7.15773 8.15355 7.01507 8.17081 6.873L8.47956 4.33248L5.04909 8.56666Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
));
|
8
newIDE/app/src/UI/CustomSvgIcons/Stairs.js
Normal file
8
newIDE/app/src/UI/CustomSvgIcons/Stairs.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default React.memo(props => (
|
||||
<SvgIcon {...props} width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 22v-2h3.5v-4.5H11V11h4.5V6.5H20V3h2v5.5h-4.5V13H13v4.5H8.5V22Z" />
|
||||
</SvgIcon>
|
||||
));
|
41
newIDE/app/src/UI/ExampleDifficultyChip.js
Normal file
41
newIDE/app/src/UI/ExampleDifficultyChip.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import Chip from './Chip';
|
||||
import StairesIcon from './CustomSvgIcons/Stairs';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
const styles = {
|
||||
chip: {
|
||||
marginRight: 2,
|
||||
marginBottom: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const makeFirstLetterUppercase = (str: string) =>
|
||||
str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
type Props = {|
|
||||
codeSizeLevel: string,
|
||||
|};
|
||||
|
||||
export const ExampleDifficultyChip = ({ codeSizeLevel }: Props) => {
|
||||
return (
|
||||
<Chip
|
||||
icon={<StairesIcon />}
|
||||
size="small"
|
||||
style={styles.chip}
|
||||
label={
|
||||
codeSizeLevel === 'simple' ? (
|
||||
<Trans>Simple</Trans>
|
||||
) : codeSizeLevel === 'advanced' ? (
|
||||
<Trans>Advanced</Trans>
|
||||
) : codeSizeLevel === 'expert' ? (
|
||||
<Trans>Expert</Trans>
|
||||
) : (
|
||||
makeFirstLetterUppercase(codeSizeLevel)
|
||||
)
|
||||
}
|
||||
key="example-size-level"
|
||||
/>
|
||||
);
|
||||
};
|
45
newIDE/app/src/UI/ExampleSizeChip.js
Normal file
45
newIDE/app/src/UI/ExampleSizeChip.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import Chip from './Chip';
|
||||
import SizeIcon from '@material-ui/icons/PhotoSizeSelectSmall';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
const styles = {
|
||||
chip: {
|
||||
marginRight: 2,
|
||||
marginBottom: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const makeFirstLetterUppercase = (str: string) =>
|
||||
str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
type Props = {|
|
||||
codeSizeLevel: string,
|
||||
|};
|
||||
|
||||
export const ExampleSizeChip = ({ codeSizeLevel }: Props) => {
|
||||
return (
|
||||
<Chip
|
||||
icon={<SizeIcon />}
|
||||
size="small"
|
||||
style={styles.chip}
|
||||
label={
|
||||
codeSizeLevel === 'tiny' ? (
|
||||
<Trans>Tiny</Trans>
|
||||
) : codeSizeLevel === 'small' ? (
|
||||
<Trans>Small</Trans>
|
||||
) : codeSizeLevel === 'medium' ? (
|
||||
<Trans>Medium</Trans>
|
||||
) : codeSizeLevel === 'big' ? (
|
||||
<Trans>Big</Trans>
|
||||
) : codeSizeLevel === 'huge' ? (
|
||||
<Trans>Huge</Trans>
|
||||
) : (
|
||||
makeFirstLetterUppercase(codeSizeLevel)
|
||||
)
|
||||
}
|
||||
key="example-size-level"
|
||||
/>
|
||||
);
|
||||
};
|
@@ -2,16 +2,18 @@
|
||||
import * as React from 'react';
|
||||
import { CorsAwareImage } from './CorsAwareImage';
|
||||
|
||||
export const iconWithBackgroundStyle = {
|
||||
background: 'linear-gradient(45deg, #FFFFFF33, #FFFFFF)',
|
||||
borderRadius: 4,
|
||||
};
|
||||
|
||||
const styles = {
|
||||
iconBackground: {
|
||||
flex: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
background: 'linear-gradient(45deg, #FFFFFF33, #FFFFFF)',
|
||||
borderRadius: 4,
|
||||
},
|
||||
icon: iconWithBackgroundStyle,
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
|
@@ -14,7 +14,15 @@ const LeftLoader = ({
|
||||
isLoading: ?boolean,
|
||||
}) => (
|
||||
<span>
|
||||
{isLoading && <CircularProgress size={20} style={styles.progress} />}
|
||||
{isLoading && (
|
||||
<CircularProgress
|
||||
// From size 20, this component applied to a Dialog button triggers glitches
|
||||
// when rotating: the scrollbar appears and disappears each time the diagonal
|
||||
// of the square box containing the round SVG is vertical.
|
||||
size={18}
|
||||
style={styles.progress}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
12
newIDE/app/src/UI/Theme/Global/Snackbar.css
Normal file
12
newIDE/app/src/UI/Theme/Global/Snackbar.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@media (orientation: portrait) {
|
||||
.MuiSnackbar-root.MuiSnackbar-anchorOriginBottomCenter,
|
||||
.MuiSnackbar-root.MuiSnackbar-anchorOriginBottomLeft,
|
||||
.MuiSnackbar-root.MuiSnackbar-anchorOriginBottomRight {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.MuiSnackbar-root.MuiSnackbar-anchorOriginTopCenter,
|
||||
.MuiSnackbar-root.MuiSnackbar-anchorOriginTopLeft,
|
||||
.MuiSnackbar-root.MuiSnackbar-anchorOriginTopRight {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@ import 'react-virtualized/styles.css';
|
||||
// Styles
|
||||
import './Global/Animation.css';
|
||||
import './Global/EventsSheet.css';
|
||||
import './Global/Snackbar.css';
|
||||
import './Global/Markdown.css';
|
||||
import './Global/Scrollbar.css';
|
||||
import './Global/Mosaic.css';
|
||||
|
@@ -8,6 +8,7 @@ import PublicProfileContext from '../../Profile/PublicProfileContext';
|
||||
const styles = {
|
||||
chip: {
|
||||
marginRight: 2,
|
||||
marginBottom: 2,
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -3,7 +3,6 @@ import { initializeZipJs } from './Zip.js';
|
||||
import { downloadUrlsToBlobs, type ItemResult } from './BlobDownloader';
|
||||
import path from 'path';
|
||||
import { shortenString } from './StringHelpers.js';
|
||||
import { type ExportSizeOptions } from './GDevelopServices/Usage';
|
||||
|
||||
export type BlobFileDescriptor = {|
|
||||
filePath: string,
|
||||
@@ -96,13 +95,13 @@ export const archiveFiles = async ({
|
||||
blobFiles,
|
||||
basePath,
|
||||
onProgress,
|
||||
sizeOptions,
|
||||
sizeLimit,
|
||||
}: {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
basePath: string,
|
||||
onProgress: (count: number, total: number) => void,
|
||||
sizeOptions?: ExportSizeOptions,
|
||||
sizeLimit?: number,
|
||||
|}): Promise<Blob> => {
|
||||
const zipJs: ZipJs = await initializeZipJs();
|
||||
|
||||
@@ -155,12 +154,15 @@ export const archiveFiles = async ({
|
||||
() => {
|
||||
zipWriter.close((blob: Blob) => {
|
||||
const fileSize = blob.size;
|
||||
if (sizeOptions && fileSize > sizeOptions.limit) {
|
||||
if (sizeLimit && fileSize > sizeLimit) {
|
||||
const roundFileSizeInMb = Math.round(
|
||||
fileSize / (1000 * 1000)
|
||||
);
|
||||
reject(
|
||||
new Error(sizeOptions.getErrorMessage(roundFileSizeInMb))
|
||||
new Error(
|
||||
`Archive is of size ${roundFileSizeInMb} MB, which is above the limit allowed of ${sizeLimit /
|
||||
(1000 * 1000)} MB.`
|
||||
)
|
||||
);
|
||||
}
|
||||
resolve(blob);
|
||||
|
@@ -10,20 +10,20 @@ export const GDevelopGamePreviews = {
|
||||
export const GDevelopGamesPlatform = {
|
||||
getInstantBuildUrl: (buildId: string) =>
|
||||
isDev
|
||||
? `https://liluo.io/instant-builds/${buildId}?dev=true`
|
||||
: `https://liluo.io/instant-builds/${buildId}`,
|
||||
? `https://gd.games/instant-builds/${buildId}?dev=true`
|
||||
: `https://gd.games/instant-builds/${buildId}`,
|
||||
getGameUrl: (gameId: string) =>
|
||||
isDev
|
||||
? `https://liluo.io/games/${gameId}?dev=true`
|
||||
: `https://liluo.io/games/${gameId}`,
|
||||
? `https://gd.games/games/${gameId}?dev=true`
|
||||
: `https://gd.games/games/${gameId}`,
|
||||
getGameUrlWithSlug: (userSlug: string, gameSlug: string) =>
|
||||
isDev
|
||||
? `https://liluo.io/${userSlug.toLowerCase()}/${gameSlug.toLowerCase()}?dev=true`
|
||||
: `https://liluo.io/${userSlug.toLowerCase()}/${gameSlug.toLowerCase()}`,
|
||||
? `https://gd.games/${userSlug.toLowerCase()}/${gameSlug.toLowerCase()}?dev=true`
|
||||
: `https://gd.games/${userSlug.toLowerCase()}/${gameSlug.toLowerCase()}`,
|
||||
getUserPublicProfileUrl: (userId: string, username: ?string) =>
|
||||
username
|
||||
? `https://liluo.io/${username}${isDev ? '?dev=true' : ''}`
|
||||
: `https://liluo.io/user/${userId}${isDev ? '?dev=true' : ''}`,
|
||||
? `https://gd.games/${username}${isDev ? '?dev=true' : ''}`
|
||||
: `https://gd.games/user/${userId}${isDev ? '?dev=true' : ''}`,
|
||||
};
|
||||
|
||||
export const GDevelopFirebaseConfig = {
|
||||
|
@@ -15,6 +15,8 @@ export type ExampleShortHeader = {|
|
||||
authorIds?: Array<string>,
|
||||
previewImageUrls: Array<string>,
|
||||
gdevelopVersion: string,
|
||||
codeSizeLevel: string,
|
||||
difficultyLevel?: string,
|
||||
|};
|
||||
|
||||
export type Example = {|
|
||||
|
@@ -6,6 +6,17 @@ import { type Filters } from './Filters';
|
||||
import { type UserPublicProfile } from './User';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
export type CachedGameSlug = {
|
||||
username: string,
|
||||
gameSlug: string,
|
||||
};
|
||||
|
||||
export type GameSlug = {
|
||||
username: string,
|
||||
gameSlug: string,
|
||||
createdAt: number,
|
||||
};
|
||||
|
||||
export type PublicGame = {
|
||||
id: string,
|
||||
gameName: string,
|
||||
@@ -40,6 +51,7 @@ export type Game = {
|
||||
acceptsBuildComments?: boolean,
|
||||
acceptsGameComments?: boolean,
|
||||
displayAdsOnGamePage?: boolean,
|
||||
cachedCurrentSlug?: CachedGameSlug,
|
||||
};
|
||||
|
||||
export type GameCategory = {
|
||||
@@ -47,12 +59,6 @@ export type GameCategory = {
|
||||
type: 'user-defined' | 'admin-only',
|
||||
};
|
||||
|
||||
export type GameSlug = {
|
||||
username: string,
|
||||
gameSlug: string,
|
||||
createdAt: number,
|
||||
};
|
||||
|
||||
export type ShowcasedGameLink = {
|
||||
url: string,
|
||||
type:
|
||||
@@ -135,8 +141,9 @@ export const getCategoryName = (category: string, i18n: I18nType) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getGameUrl = (game: ?Game, slug: ?GameSlug) => {
|
||||
export const getGameUrl = (game: ?Game) => {
|
||||
if (!game) return null;
|
||||
const slug = game.cachedCurrentSlug;
|
||||
return slug
|
||||
? GDevelopGamesPlatform.getGameUrlWithSlug(slug.username, slug.gameSlug)
|
||||
: GDevelopGamesPlatform.getGameUrl(game.id);
|
||||
@@ -395,26 +402,6 @@ export const getPublicGame = (gameId: string): Promise<PublicGame> => {
|
||||
.then(response => response.data);
|
||||
};
|
||||
|
||||
export const getGameSlugs = (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string,
|
||||
gameId: string
|
||||
): Promise<Array<GameSlug>> => {
|
||||
return getAuthorizationHeader()
|
||||
.then(authorizationHeader =>
|
||||
axios.get(`${GDevelopGameApi.baseUrl}/game-slug`, {
|
||||
params: {
|
||||
userId,
|
||||
gameId,
|
||||
},
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
})
|
||||
)
|
||||
.then(response => response.data);
|
||||
};
|
||||
|
||||
export const getGameCategories = (): Promise<GameCategory[]> => {
|
||||
return axios
|
||||
.get(`${GDevelopGameApi.baseUrl}/game-category`)
|
||||
|
@@ -9,30 +9,30 @@ describe('Play service', () => {
|
||||
});
|
||||
test('it returns null if input is not compliant', () => {
|
||||
expect(
|
||||
extractNextPageUriFromLinkHeader('https://www.liluo.io/ rel="next"')
|
||||
extractNextPageUriFromLinkHeader('https://www.gd.games/ rel="next"')
|
||||
).toBeNull();
|
||||
});
|
||||
test('it returns null if next link is not present', () => {
|
||||
expect(
|
||||
extractNextPageUriFromLinkHeader(
|
||||
'<https://www.liluo.io/>; rel="prev", <https://www.liluo.io/>; rel="home"'
|
||||
'<https://www.gd.games/>; rel="prev", <https://www.gd.games/>; rel="home"'
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
test('it returns URI if next link is present', () => {
|
||||
expect(
|
||||
extractNextPageUriFromLinkHeader(
|
||||
'<https://www.liluo.io/>; rel="next", <https://www.liluo.io/>; rel="home"'
|
||||
'<https://www.gd.games/>; rel="next", <https://www.gd.games/>; rel="home"'
|
||||
)
|
||||
).toEqual('https://www.liluo.io/');
|
||||
).toEqual('https://www.gd.games/');
|
||||
});
|
||||
test('it returns URI with encoded query parameters', () => {
|
||||
expect(
|
||||
extractNextPageUriFromLinkHeader(
|
||||
'<https://www.liluo.io/game/3723963b-4f27-4896-9d62-32b1b0adddd5/leaderboard/aa7a8a96-dcf5-405c-8844-f133bcf223c7/entries?after=%7B%22GSIRawSK%22%3A1.89%2C%22parentId%22%3A%22LID%23aa7a8a96-dcf5-405c-8844-f133bcf223c7%22%2C%22childId%22%3A%22LE%23497f82ec-3aba-4ff0-a001-07d7c128f890%22%7D&perPage=10&onlyBestEntry=false>; rel="next", <https://www.liluo.io/>; rel="home"'
|
||||
'<https://www.gd.games/game/3723963b-4f27-4896-9d62-32b1b0adddd5/leaderboard/aa7a8a96-dcf5-405c-8844-f133bcf223c7/entries?after=%7B%22GSIRawSK%22%3A1.89%2C%22parentId%22%3A%22LID%23aa7a8a96-dcf5-405c-8844-f133bcf223c7%22%2C%22childId%22%3A%22LE%23497f82ec-3aba-4ff0-a001-07d7c128f890%22%7D&perPage=10&onlyBestEntry=false>; rel="next", <https://www.gd.games/>; rel="home"'
|
||||
)
|
||||
).toEqual(
|
||||
'https://www.liluo.io/game/3723963b-4f27-4896-9d62-32b1b0adddd5/leaderboard/aa7a8a96-dcf5-405c-8844-f133bcf223c7/entries?after=%7B%22GSIRawSK%22%3A1.89%2C%22parentId%22%3A%22LID%23aa7a8a96-dcf5-405c-8844-f133bcf223c7%22%2C%22childId%22%3A%22LE%23497f82ec-3aba-4ff0-a001-07d7c128f890%22%7D&perPage=10&onlyBestEntry=false'
|
||||
'https://www.gd.games/game/3723963b-4f27-4896-9d62-32b1b0adddd5/leaderboard/aa7a8a96-dcf5-405c-8844-f133bcf223c7/entries?after=%7B%22GSIRawSK%22%3A1.89%2C%22parentId%22%3A%22LID%23aa7a8a96-dcf5-405c-8844-f133bcf223c7%22%2C%22childId%22%3A%22LE%23497f82ec-3aba-4ff0-a001-07d7c128f890%22%7D&perPage=10&onlyBestEntry=false'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -118,6 +118,9 @@ export const getSubscriptionPlans = (): Array<PlanDetails> => [
|
||||
{
|
||||
message: t`Unlimited leaderboards and unlimited player feedback responses.`,
|
||||
},
|
||||
{
|
||||
message: t`Immerse your players by removing the GDevelop watermark or the GDevelop logo when the game loads.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -136,7 +139,7 @@ export const getSubscriptionPlans = (): Array<PlanDetails> => [
|
||||
message: t`Unlimited leaderboards and unlimited player feedback responses.`,
|
||||
},
|
||||
{
|
||||
message: t`Immerse your players by removing GDevelop logo when the game loads.`,
|
||||
message: t`Immerse your players by removing the GDevelop watermark or the GDevelop logo when the game loads.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -190,29 +193,6 @@ export const businessPlan: PlanDetails = {
|
||||
descriptionBullets: [],
|
||||
};
|
||||
|
||||
const SUBSCRIBED_SIZE_LIMIT_IN_MB = 250;
|
||||
const GUEST_SIZE_LIMIT_IN_MB = 50;
|
||||
|
||||
export type ExportSizeOptions = {|
|
||||
limit: number,
|
||||
getErrorMessage: (fileSizeInMb: number) => string,
|
||||
|};
|
||||
|
||||
export const onlineWebExportSizeOptions: {
|
||||
[key: 'guest' | 'subscribed']: ExportSizeOptions,
|
||||
} = {
|
||||
guest: {
|
||||
limit: SUBSCRIBED_SIZE_LIMIT_IN_MB * 1000 * 1000,
|
||||
getErrorMessage: (fileSizeInMb: number) =>
|
||||
`Archive is of size ${fileSizeInMb} MB, which is above the limit allowed of ${SUBSCRIBED_SIZE_LIMIT_IN_MB} MB`,
|
||||
},
|
||||
subscribed: {
|
||||
limit: GUEST_SIZE_LIMIT_IN_MB * 1000 * 1000,
|
||||
getErrorMessage: (fileSizeInMb: number) =>
|
||||
`Archive is of size ${fileSizeInMb} MB, which is above the limit allowed of ${GUEST_SIZE_LIMIT_IN_MB} MB. You can subscribe to GDevelop to increase the limit to ${SUBSCRIBED_SIZE_LIMIT_IN_MB} MB.`,
|
||||
},
|
||||
};
|
||||
|
||||
export const getUserUsages = (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
import optionalRequire from './OptionalRequire';
|
||||
import optionalLazyRequire from '../Utils/OptionalLazyRequire';
|
||||
import { type ExportSizeOptions } from './GDevelopServices/Usage';
|
||||
const fs = optionalRequire('fs');
|
||||
const lazyRequireArchiver = optionalLazyRequire('archiver');
|
||||
|
||||
@@ -13,11 +12,11 @@ const lazyRequireArchiver = optionalLazyRequire('archiver');
|
||||
export const archiveLocalFolder = ({
|
||||
path,
|
||||
outputFilename,
|
||||
sizeOptions,
|
||||
sizeLimit,
|
||||
}: {|
|
||||
path: string,
|
||||
outputFilename: string,
|
||||
sizeOptions?: ExportSizeOptions,
|
||||
sizeLimit?: number,
|
||||
|}): Promise<string> => {
|
||||
const archiver = lazyRequireArchiver();
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -33,9 +32,14 @@ export const archiveLocalFolder = ({
|
||||
console.log(
|
||||
`Archive written at ${outputFilename}, ${fileSize} total bytes.`
|
||||
);
|
||||
if (sizeOptions && fileSize > sizeOptions.limit) {
|
||||
if (sizeLimit && fileSize > sizeLimit) {
|
||||
const roundFileSizeInMb = Math.round(fileSize / (1000 * 1000));
|
||||
reject(new Error(sizeOptions.getErrorMessage(roundFileSizeInMb)));
|
||||
reject(
|
||||
new Error(
|
||||
`Archive is of size ${roundFileSizeInMb} MB, which is above the limit allowed of ${sizeLimit /
|
||||
(1000 * 1000)} MB.`
|
||||
)
|
||||
);
|
||||
}
|
||||
resolve(outputFilename);
|
||||
});
|
||||
|
@@ -70,6 +70,7 @@ export const GenericRetryableProcessWithProgressDialog = ({
|
||||
) : null,
|
||||
]}
|
||||
cannotBeDismissed={!hasErrors}
|
||||
noMobileFullScreen={!hasErrors}
|
||||
open
|
||||
maxWidth="sm"
|
||||
>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user