mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
70 Commits
experiment
...
codex/upda
Author | SHA1 | Date | |
---|---|---|---|
![]() |
08f00bb893 | ||
![]() |
279d41cdb7 | ||
![]() |
5cf65a9f62 | ||
![]() |
08b05c13b6 | ||
![]() |
eb55c85f4e | ||
![]() |
8a243440db | ||
![]() |
b3e4e6b89c | ||
![]() |
a1a25f6df4 | ||
![]() |
6114a6cec1 | ||
![]() |
5058964937 | ||
![]() |
4488675540 | ||
![]() |
6a2d2c9e67 | ||
![]() |
b43c42d763 | ||
![]() |
69112183d4 | ||
![]() |
a4c2778b8d | ||
![]() |
f26e56c3bf | ||
![]() |
f5f9944fc4 | ||
![]() |
9467caf1e9 | ||
![]() |
00376f39d5 | ||
![]() |
40b6a34dc5 | ||
![]() |
17d2b8c2c2 | ||
![]() |
935af42d23 | ||
![]() |
d4a8d468cb | ||
![]() |
b16099aee0 | ||
![]() |
c17b918a43 | ||
![]() |
d58e8c7ef9 | ||
![]() |
ddd6b6e3a8 | ||
![]() |
e629c132ea | ||
![]() |
b80e03f153 | ||
![]() |
11e36ff3f1 | ||
![]() |
22de356413 | ||
![]() |
caefa04fbe | ||
![]() |
cf2e7d67d7 | ||
![]() |
685e444b2d | ||
![]() |
a9c1045afd | ||
![]() |
24e0d37583 | ||
![]() |
d44997d372 | ||
![]() |
062aa888f8 | ||
![]() |
de4c2ae4ad | ||
![]() |
29ad7308c3 | ||
![]() |
19b21c280e | ||
![]() |
fbfe8b246a | ||
![]() |
73f66eb51f | ||
![]() |
d62ba2b9a0 | ||
![]() |
323a2b6c2f | ||
![]() |
8e4cccd562 | ||
![]() |
795795ba40 | ||
![]() |
4af86b36e5 | ||
![]() |
b00632a625 | ||
![]() |
6f23f76441 | ||
![]() |
a6cd4b3c5d | ||
![]() |
81d63c41b6 | ||
![]() |
a924840228 | ||
![]() |
b013297c8e | ||
![]() |
ca77a31037 | ||
![]() |
5adb2240d5 | ||
![]() |
9d42be3362 | ||
![]() |
21201dec29 | ||
![]() |
08229cbe1d | ||
![]() |
96e9dd7c4b | ||
![]() |
7dbc687200 | ||
![]() |
7e1f2c6c97 | ||
![]() |
37cba12e4a | ||
![]() |
cdd80bca9e | ||
![]() |
3293d24c36 | ||
![]() |
bf31781d7a | ||
![]() |
f6c43b2db3 | ||
![]() |
f00156a654 | ||
![]() |
41fd1cbcee | ||
![]() |
52c807d74a |
@@ -13,17 +13,18 @@ orbs:
|
||||
aws-cli: circleci/aws-cli@2.0.6
|
||||
macos: circleci/macos@2.5.1 # For Rosetta (see below)
|
||||
node: circleci/node@5.2.0 # For a recent npm version (see below)
|
||||
win: circleci/windows@5.1.0
|
||||
jobs:
|
||||
# Build the **entire** app for macOS.
|
||||
# Build the **entire** app for macOS (including the GDevelop.js library).
|
||||
build-macos:
|
||||
macos:
|
||||
xcode: 14.2.0
|
||||
resource_class: macos.m1.large.gen1
|
||||
xcode: 16.4.0
|
||||
resource_class: m4pro.medium
|
||||
steps:
|
||||
- checkout
|
||||
# Install Rosetta for AWS CLI and disable TSO to speed up S3 uploads (https://support.circleci.com/hc/en-us/articles/19334402064027-Troubleshooting-slow-uploads-to-S3-for-jobs-using-an-m1-macOS-resource-class)
|
||||
- macos/install-rosetta
|
||||
- run: sudo sysctl net.inet.tcp.tso=0
|
||||
# - run: sudo sysctl net.inet.tcp.tso=0
|
||||
|
||||
# Install a recent version of npm to workaround a notarization issue because of a symlink made by npm: https://github.com/electron-userland/electron-builder/issues/7755
|
||||
# Node.js v20.14.0 comes with npm v10.7.0.
|
||||
@@ -46,9 +47,9 @@ jobs:
|
||||
# GDevelop.js dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- gd-macos-nodejs-dependencies-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}
|
||||
- gd-macos-nodejs-dependencies-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}-{{ checksum "GDJS/package-lock.json" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- gd-macos-nodejs-dependencies---
|
||||
- gd-macos-nodejs-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install GDevelop.js dependencies
|
||||
@@ -69,7 +70,8 @@ jobs:
|
||||
- newIDE/electron-app/node_modules
|
||||
- newIDE/app/node_modules
|
||||
- GDevelop.js/node_modules
|
||||
key: gd-macos-nodejs-dependencies-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}
|
||||
- GDJS/node_modules
|
||||
key: gd-macos-nodejs-dependencies-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}-{{ checksum "GDJS/package-lock.json" }}
|
||||
|
||||
# Build GDevelop IDE (seems like we need to allow Node.js to use more space than usual)
|
||||
# Note: Code signing is done using CSC_LINK (see https://www.electron.build/code-signing).
|
||||
@@ -86,15 +88,37 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: newIDE/electron-app/dist
|
||||
|
||||
|
||||
# Upload artifacts (AWS)
|
||||
- run:
|
||||
name: Deploy to S3 (specific commit)
|
||||
command: export PATH=~/.local/bin:$PATH && aws s3 sync newIDE/electron-app/dist s3://gdevelop-releases/$(git rev-parse --abbrev-ref HEAD)/commit/$(git rev-parse HEAD)/
|
||||
command: |
|
||||
export PATH=~/.local/bin:$PATH
|
||||
for i in 1 2 3 4 5 6 7; do
|
||||
aws s3 sync newIDE/electron-app/dist s3://gdevelop-releases/$(git rev-parse --abbrev-ref HEAD)/commit/$(git rev-parse HEAD)/ && break
|
||||
echo "Retry $i failed... retrying in 10 seconds"
|
||||
sleep 10
|
||||
done
|
||||
if [ $i -eq 7 ]; then
|
||||
echo "All retries for deployment failed!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- run:
|
||||
name: Deploy to S3 (latest)
|
||||
command: export PATH=~/.local/bin:$PATH && aws s3 sync newIDE/electron-app/dist s3://gdevelop-releases/$(git rev-parse --abbrev-ref HEAD)/latest/
|
||||
command: |
|
||||
export PATH=~/.local/bin:$PATH
|
||||
for i in 1 2 3 4 5 6 7; do
|
||||
aws s3 sync newIDE/electron-app/dist s3://gdevelop-releases/$(git rev-parse --abbrev-ref HEAD)/latest/ && break
|
||||
echo "Retry $i failed... retrying in 10 seconds"
|
||||
sleep 10
|
||||
done
|
||||
if [ $i -eq 7 ]; then
|
||||
echo "All retries for deployment failed!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the **entire** app for Linux.
|
||||
# Build the app for Linux (using a pre-built GDevelop.js library).
|
||||
build-linux:
|
||||
# CircleCI docker workers are failing if they don't have enough memory (no swap)
|
||||
resource_class: xlarge
|
||||
@@ -107,51 +131,33 @@ jobs:
|
||||
- checkout
|
||||
- aws-cli/setup
|
||||
|
||||
# System dependencies (for Electron Builder and Emscripten)
|
||||
# System dependencies (for Electron Builder)
|
||||
- run:
|
||||
name: Install dependencies for Emscripten
|
||||
command: sudo apt-get update && sudo apt install cmake
|
||||
|
||||
- run:
|
||||
name: Install Python3 dependencies for Emscripten
|
||||
command: sudo apt install python-is-python3 python3-distutils -y
|
||||
|
||||
- run:
|
||||
name: Install Emscripten (for GDevelop.js)
|
||||
command: git clone https://github.com/juj/emsdk.git && cd emsdk && ./emsdk install 3.1.21 && ./emsdk activate 3.1.21 && cd ..
|
||||
name: Update system dependencies
|
||||
command: sudo apt-get update
|
||||
|
||||
- run:
|
||||
name: Install system dependencies for Electron builder
|
||||
command: sudo apt install icnsutils && sudo apt install graphicsmagick && sudo apt install rsync
|
||||
|
||||
# GDevelop.js dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- gd-linux-nodejs-dependencies-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}
|
||||
- gd-linux-nodejs-dependencies-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}-{{ checksum "GDJS/package-lock.json" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- gd-linux-nodejs-dependencies---
|
||||
- gd-linux-nodejs-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install GDevelop.js dependencies and build it
|
||||
command: cd GDevelop.js && npm install && cd ..
|
||||
|
||||
# Build GDevelop.js (and run tests to ensure it works)
|
||||
- run:
|
||||
name: Build GDevelop.js
|
||||
# Use "--runInBand" as it's faster and avoid deadlocks on CircleCI Linux machines (probably because limited in processes number).
|
||||
command: cd GDevelop.js && source ../emsdk/emsdk_env.sh && npm run build && npm test -- --runInBand && cd ..
|
||||
|
||||
# GDevelop IDE dependencies (after building GDevelop.js to avoid downloading a pre-built version)
|
||||
# GDevelop IDE dependencies (using an exact version of GDevelop.js, built previously)
|
||||
- run:
|
||||
name: Install GDevelop IDE dependencies
|
||||
command: cd newIDE/app && npm install && cd ../electron-app && npm install
|
||||
command: export REQUIRES_EXACT_LIBGD_JS_VERSION=true && cd newIDE/app && npm install && cd ../electron-app && npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- newIDE/electron-app/node_modules
|
||||
- newIDE/app/node_modules
|
||||
- GDevelop.js/node_modules
|
||||
key: gd-linux-nodejs-dependencies-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}
|
||||
- GDJS/node_modules
|
||||
key: gd-linux-nodejs-dependencies-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}-{{ checksum "GDJS/package-lock.json" }}
|
||||
|
||||
# Build GDevelop IDE (seems like we need to allow Node.js to use more space than usual)
|
||||
- run:
|
||||
@@ -295,14 +301,203 @@ jobs:
|
||||
name: Deploy to S3 (specific commit)
|
||||
command: aws s3 sync Binaries/embuild/GDevelop.js s3://gdevelop-gdevelop.js/$(git rev-parse --abbrev-ref HEAD)/variant/debug-sanitizers/commit/$(git rev-parse HEAD)/
|
||||
|
||||
# Trigger AppVeyor build, which also does a Windows build (keep it for redundancy).
|
||||
trigger-appveyor-windows-build:
|
||||
docker:
|
||||
- image: cimg/node:16.13
|
||||
steps:
|
||||
- run:
|
||||
name: Trigger AppVeyor Windows build
|
||||
command: |
|
||||
curl -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${APPVEYOR_API_KEY}" \
|
||||
--data "{
|
||||
\"accountName\": \"4ian\",
|
||||
\"projectSlug\": \"gdevelop\",
|
||||
\"branch\": \"${CIRCLE_BRANCH}\"
|
||||
}" \
|
||||
-X POST https://ci.appveyor.com/api/builds
|
||||
|
||||
build-windows:
|
||||
executor:
|
||||
name: win/default
|
||||
size: medium
|
||||
working_directory: /home/circleci/project
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
# See https://www.ssl.com/how-to/how-to-integrate-esigner-cka-with-ci-cd-tools-for-automated-code-signing/
|
||||
#
|
||||
# This is necessary because of "signing to be FIPS-140 compliant". See
|
||||
# https://github.com/electron-userland/electron-builder/issues/6158
|
||||
#
|
||||
# Make sure to DISABLE "malware blocker" in SSL.com to avoid errors like:
|
||||
# Error information: "Error: SignerSign() failed." (-2146893821/0x80090003)
|
||||
name: Download and Unzip eSignerCKA Setup
|
||||
command: |
|
||||
|
||||
Invoke-WebRequest -OutFile eSigner_CKA_1.0.3.zip "https://www.ssl.com/download/ssl-com-esigner-cka-1-0-3"
|
||||
|
||||
Expand-Archive -Force eSigner_CKA_1.0.3.zip
|
||||
|
||||
Remove-Item eSigner_CKA_1.0.3.zip
|
||||
|
||||
Move-Item -Destination "eSigner_CKA_1.0.3.exe" -Path "eSigner_CKA_*\*.exe"
|
||||
- run:
|
||||
name: Setup eSignerCKA in Silent Mode
|
||||
command: |
|
||||
|
||||
mkdir -p "/home/circleci/project/eSignerCKA"
|
||||
|
||||
./eSigner_CKA_1.0.3.exe /CURRENTUSER /VERYSILENT /SUPPRESSMSGBOXES /DIR="/home/circleci/project/eSignerCKA" | Out-Null
|
||||
- run:
|
||||
name: Config Account Information on eSignerCKA
|
||||
command: |
|
||||
|
||||
/home/circleci/project/eSignerCKA/eSignerCKATool.exe config -mode product -user "$env:ESIGNER_USER_NAME" -pass "$env:ESIGNER_USER_PASSWORD" -totp "$env:ESIGNER_USER_TOTP" -key "/home/circleci/project/eSignerCKA/master.key" -r
|
||||
- run:
|
||||
name: Load Certificate into Windows Store
|
||||
command: |
|
||||
|
||||
/home/circleci/project/eSignerCKA/eSignerCKATool.exe unload
|
||||
|
||||
/home/circleci/project/eSignerCKA/eSignerCKATool.exe load
|
||||
- run:
|
||||
name: Select Certificate From Windows Store and Sign Sample File with SignTool
|
||||
command: |
|
||||
|
||||
$CodeSigningCert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
|
||||
|
||||
echo Certificate: $CodeSigningCert
|
||||
|
||||
- restore_cache:
|
||||
name: Restore node_modules cache
|
||||
keys:
|
||||
- v1-win-node-{{ checksum "newIDE/app/package-lock.json" }}-{{ checksum "newIDE/electron-app/package-lock.json" }}-{{ checksum "GDJS/package-lock.json" }}
|
||||
- v1-win-node-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
no_output_timeout: 25m
|
||||
# Remove package-lock.json because they seems to cause the npm install to be stuck. We should try again after re-generating them.
|
||||
# Also install setuptools as something requires distutils in electron-app, and it was removed in Python 3.12.
|
||||
# setuptools will make distutils available again (but we should migrate our packages probably).
|
||||
command: |
|
||||
pip install setuptools
|
||||
|
||||
cd newIDE\app
|
||||
|
||||
npm -v
|
||||
|
||||
Remove-Item package-lock.json
|
||||
|
||||
$Env:REQUIRES_EXACT_LIBGD_JS_VERSION = "true"
|
||||
|
||||
npm install
|
||||
|
||||
cd ..\electron-app
|
||||
|
||||
Remove-Item package-lock.json
|
||||
|
||||
npm install
|
||||
|
||||
cd ..\..
|
||||
|
||||
- save_cache:
|
||||
name: Save node_modules cache
|
||||
key: v1-win-node-{{ checksum "newIDE/app/package-lock.json" }}-{{ checksum "newIDE/electron-app/package-lock.json" }}-{{ checksum "GDJS/package-lock.json" }}
|
||||
paths:
|
||||
- newIDE/app/node_modules
|
||||
- newIDE/electron-app/node_modules
|
||||
- GDJS/node_modules
|
||||
|
||||
- run:
|
||||
name: Build NSIS executable (with code signing)
|
||||
command: |
|
||||
cd newIDE\electron-app
|
||||
|
||||
$CodeSigningCert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
|
||||
|
||||
echo Certificate: $CodeSigningCert
|
||||
|
||||
# Use a custom signtool path because of the signtool.exe bundled withy electron-builder not working for some reason.
|
||||
# Can also be found in versioned folders like "C:/Program Files (x86)/Windows Kits/10/bin/10.0.22000.0/x86/signtool.exe".
|
||||
# or "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\signtool.exe".
|
||||
|
||||
$Env:SIGNTOOL_PATH = "C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe"
|
||||
|
||||
# Extract thumbprint and subject name of the certificate (will be passed to electron-builder).
|
||||
|
||||
$Env:GD_SIGNTOOL_THUMBPRINT = $CodeSigningCert.Thumbprint
|
||||
|
||||
$Env:GD_SIGNTOOL_SUBJECT_NAME = ($CodeSigningCert.Subject -replace ", ?", "`n" | ConvertFrom-StringData).CN
|
||||
|
||||
# Build the nsis installer (signed: electron-builder will use SignTool.exe with the certificate)
|
||||
|
||||
node scripts/build.js --win nsis --publish=never
|
||||
|
||||
cd ..\..
|
||||
- run:
|
||||
name: Build AppX (without code signing)
|
||||
# Don't sign the appx (it will be signed by the Microsoft Store).
|
||||
command: |
|
||||
cd newIDE\electron-app
|
||||
|
||||
# Build the appx (not signed). Ensure all variables used for code signing are empty.
|
||||
|
||||
$Env:GD_SIGNTOOL_THUMBPRINT = ''
|
||||
|
||||
$Env:GD_SIGNTOOL_SUBJECT_NAME = ''
|
||||
|
||||
$Env:CSC_LINK = ''
|
||||
|
||||
$Env:CSC_KEY_PASSWORD = ''
|
||||
|
||||
node scripts/build.js --skip-app-build --win appx --publish=never
|
||||
|
||||
cd ..\..
|
||||
|
||||
- run:
|
||||
name: Clean binaries
|
||||
shell: cmd.exe
|
||||
command: |
|
||||
rmdir /s /q newIDE\electron-app\dist\win-unpacked
|
||||
|
||||
- run:
|
||||
name: Install AWS CLI
|
||||
command: |
|
||||
# Install the CLI for the current user
|
||||
|
||||
pip install --quiet --upgrade --user awscli
|
||||
|
||||
# Add the user-Scripts dir to PATH for this step and the next.
|
||||
|
||||
$binDir = (python -m site --user-base) + "\Scripts"
|
||||
$Env:Path += ";$binDir"
|
||||
|
||||
# Sanity check:
|
||||
aws --version
|
||||
|
||||
# Upload artifacts (S3)
|
||||
- run:
|
||||
name: Deploy to S3 (specific commit)
|
||||
command: |
|
||||
aws s3 sync newIDE\electron-app\dist "s3://gdevelop-releases/$Env:CIRCLE_BRANCH/commit/$Env:CIRCLE_SHA1/"
|
||||
|
||||
- run:
|
||||
name: Deploy to S3 (latest)
|
||||
command: |
|
||||
aws s3 sync newIDE\electron-app\dist "s3://gdevelop-releases/$Env:CIRCLE_BRANCH/latest/"
|
||||
|
||||
# Upload artifacts (CircleCI)
|
||||
- store_artifacts:
|
||||
path: newIDE/electron-app/dist
|
||||
|
||||
workflows:
|
||||
gdevelop_js-wasm:
|
||||
jobs:
|
||||
- build-gdevelop_js-wasm-only
|
||||
gdevelop_js-wasm-extra-checks:
|
||||
jobs:
|
||||
- build-gdevelop_js-debug-sanitizers-and-extra-checks:
|
||||
# Extra checks are resource intensive so don't all run them.
|
||||
# Extra checks are resource intensive so don't always run them.
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
@@ -310,13 +505,36 @@ workflows:
|
||||
- /experimental-build.*/
|
||||
builds:
|
||||
jobs:
|
||||
- build-gdevelop_js-wasm-only
|
||||
- build-macos:
|
||||
# The macOS version builds by itself GDevelop.js
|
||||
# (so we verify we can build it on macOS).
|
||||
# requires:
|
||||
# - build-gdevelop_js-wasm-only
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /experimental-build.*/
|
||||
- build-linux:
|
||||
requires:
|
||||
- build-gdevelop_js-wasm-only
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /experimental-build.*/
|
||||
- build-windows:
|
||||
requires:
|
||||
- build-gdevelop_js-wasm-only
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /experimental-build.*/
|
||||
- trigger-appveyor-windows-build:
|
||||
requires:
|
||||
- build-gdevelop_js-wasm-only
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
|
@@ -61,10 +61,12 @@ void GroupEvent::UnserializeFrom(gd::Project& project,
|
||||
project, events, element.GetChild("events"));
|
||||
|
||||
parameters.clear();
|
||||
gd::SerializerElement& parametersElement = element.GetChild("parameters");
|
||||
parametersElement.ConsiderAsArrayOf("parameters");
|
||||
for (std::size_t i = 0; i < parametersElement.GetChildrenCount(); ++i)
|
||||
parameters.push_back(parametersElement.GetChild(i).GetValue().GetString());
|
||||
if (element.HasChild("parameters")) {
|
||||
gd::SerializerElement& parametersElement = element.GetChild("parameters");
|
||||
parametersElement.ConsiderAsArrayOf("parameters");
|
||||
for (std::size_t i = 0; i < parametersElement.GetChildrenCount(); ++i)
|
||||
parameters.push_back(parametersElement.GetChild(i).GetValue().GetString());
|
||||
}
|
||||
}
|
||||
|
||||
void GroupEvent::SetBackgroundColor(unsigned int colorR_,
|
||||
|
@@ -163,6 +163,21 @@ void LinkEvent::UnserializeFrom(gd::Project& project,
|
||||
// end of compatibility code
|
||||
}
|
||||
|
||||
vector<gd::String> LinkEvent::GetAllSearchableStrings() const {
|
||||
vector<gd::String> allSearchableStrings;
|
||||
|
||||
allSearchableStrings.push_back(target);
|
||||
|
||||
return allSearchableStrings;
|
||||
}
|
||||
|
||||
bool LinkEvent::ReplaceAllSearchableStrings(
|
||||
std::vector<gd::String> newSearchableString) {
|
||||
if (newSearchableString[0] == target) return false;
|
||||
SetTarget(newSearchableString[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LinkEvent::AcceptVisitor(gd::EventVisitor &eventVisitor) {
|
||||
return BaseEvent::AcceptVisitor(eventVisitor) ||
|
||||
eventVisitor.VisitLinkEvent(*this);
|
||||
|
@@ -109,6 +109,10 @@ class GD_CORE_API LinkEvent : public gd::BaseEvent {
|
||||
|
||||
virtual bool IsExecutable() const override { return true; };
|
||||
|
||||
virtual std::vector<gd::String> GetAllSearchableStrings() const override;
|
||||
virtual bool ReplaceAllSearchableStrings(
|
||||
std::vector<gd::String> newSearchableString) override;
|
||||
|
||||
virtual void SerializeTo(SerializerElement& element) const override;
|
||||
virtual void UnserializeFrom(gd::Project& project,
|
||||
const SerializerElement& element) override;
|
||||
|
@@ -286,6 +286,20 @@ class GD_CORE_API BaseEvent {
|
||||
* \brief True if the event should be folded in the events editor.
|
||||
*/
|
||||
bool IsFolded() const { return folded; }
|
||||
|
||||
/**
|
||||
* \brief Set the AI generated event ID.
|
||||
*/
|
||||
void SetAiGeneratedEventId(const gd::String& aiGeneratedEventId_) {
|
||||
aiGeneratedEventId = aiGeneratedEventId_;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Get the AI generated event ID.
|
||||
*/
|
||||
const gd::String& GetAiGeneratedEventId() const {
|
||||
return aiGeneratedEventId;
|
||||
}
|
||||
///@}
|
||||
|
||||
std::weak_ptr<gd::BaseEvent>
|
||||
@@ -304,6 +318,7 @@ class GD_CORE_API BaseEvent {
|
||||
bool disabled; ///< True if the event is disabled and must not be executed
|
||||
gd::String type; ///< Type of the event. Must be assigned at the creation.
|
||||
///< Used for saving the event for instance.
|
||||
gd::String aiGeneratedEventId; ///< When generated by an AI/external tool.
|
||||
|
||||
static gd::EventsList badSubEvents;
|
||||
static gd::VariablesContainer badLocalVariables;
|
||||
|
@@ -221,6 +221,8 @@ void EventsListSerialization::UnserializeEventsFrom(
|
||||
|
||||
event->SetDisabled(eventElem.GetBoolAttribute("disabled", false));
|
||||
event->SetFolded(eventElem.GetBoolAttribute("folded", false));
|
||||
event->SetAiGeneratedEventId(
|
||||
eventElem.GetStringAttribute("aiGeneratedEventId", ""));
|
||||
|
||||
list.InsertEvent(event, list.GetEventsCount());
|
||||
}
|
||||
@@ -236,6 +238,8 @@ void EventsListSerialization::SerializeEventsTo(const EventsList& list,
|
||||
if (event.IsDisabled())
|
||||
eventElem.SetAttribute("disabled", event.IsDisabled());
|
||||
if (event.IsFolded()) eventElem.SetAttribute("folded", event.IsFolded());
|
||||
if (!event.GetAiGeneratedEventId().empty())
|
||||
eventElem.SetAttribute("aiGeneratedEventId", event.GetAiGeneratedEventId());
|
||||
eventElem.AddChild("type").SetValue(event.GetType());
|
||||
|
||||
event.SerializeTo(eventElem);
|
||||
|
@@ -37,8 +37,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
.SetIcon("res/actions/position24_black.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Angle"))
|
||||
.SetIcon("res/actions/direction24_black.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size"))
|
||||
.SetIcon("res/actions/scale24_black.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size")).SetIcon(
|
||||
"res/actions/scale24_black.png");
|
||||
|
||||
gd::ObjectMetadata& obj = extension.AddObject<gd::ObjectConfiguration>(
|
||||
"", _("Base object"), _("Base object"), "res/objeticon24.png");
|
||||
@@ -235,7 +235,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
|
||||
obj.AddAction("SetAngle",
|
||||
_("Angle"),
|
||||
_("Change the angle of rotation of an object (in degrees)."),
|
||||
_("Change the angle of rotation of an object (in degrees). For "
|
||||
"3D objects, this is the rotation around the Z axis."),
|
||||
_("the angle"),
|
||||
_("Angle"),
|
||||
"res/actions/direction24_black.png",
|
||||
@@ -250,7 +251,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
obj.AddAction("Rotate",
|
||||
_("Rotate"),
|
||||
_("Rotate an object, clockwise if the speed is positive, "
|
||||
"counterclockwise otherwise."),
|
||||
"counterclockwise otherwise. For 3D objects, this is the "
|
||||
"rotation around the Z axis."),
|
||||
_("Rotate _PARAM0_ at speed _PARAM1_ deg/second"),
|
||||
_("Angle"),
|
||||
"res/actions/rotate24_black.png",
|
||||
@@ -634,7 +636,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
|
||||
obj.AddCondition("Angle",
|
||||
_("Angle"),
|
||||
_("Compare the angle of the specified object."),
|
||||
_("Compare the angle, in degrees, of the specified object. "
|
||||
"For 3D objects, this is the angle around the Z axis."),
|
||||
_("the angle (in degrees)"),
|
||||
_("Angle"),
|
||||
"res/conditions/direction24_black.png",
|
||||
@@ -835,14 +838,13 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
.MarkAsAdvanced()
|
||||
.SetRelevantForLayoutEventsOnly();
|
||||
|
||||
obj.AddAction(
|
||||
"PushBooleanToObjectVariable",
|
||||
_("Add value to object array variable"),
|
||||
_("Adds a boolean to the end of an object array variable."),
|
||||
_("Add value _PARAM2_ to array variable _PARAM1_ of _PARAM0_"),
|
||||
_("Variables ❯ Arrays and structures"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
obj.AddAction("PushBooleanToObjectVariable",
|
||||
_("Add value to object array variable"),
|
||||
_("Adds a boolean to the end of an object array variable."),
|
||||
_("Add value _PARAM2_ to array variable _PARAM1_ of _PARAM0_"),
|
||||
_("Variables ❯ Arrays and structures"),
|
||||
"res/actions/var24.png",
|
||||
"res/actions/var.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("objectvar", _("Array variable"))
|
||||
.AddParameter("trueorfalse", _("Boolean to add"))
|
||||
@@ -1268,7 +1270,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
|
||||
obj.AddExpression("Angle",
|
||||
_("Angle"),
|
||||
_("Current angle, in degrees, of the object"),
|
||||
_("Current angle, in degrees, of the object. For 3D "
|
||||
"objects, this is the angle around the Z axis."),
|
||||
_("Angle"),
|
||||
"res/actions/direction_black.png")
|
||||
.AddParameter("object", _("Object"));
|
||||
@@ -1571,7 +1574,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
|
||||
extension
|
||||
.AddAction("Create",
|
||||
_("Create an object"),
|
||||
_("Create an object at specified position"),
|
||||
_("Create an instance of the object at the specified position."
|
||||
"The created object instance will be available for the next "
|
||||
"actions and sub-events."),
|
||||
_("Create object _PARAM1_ at position _PARAM2_;_PARAM3_ "
|
||||
"(layer: _PARAM4_)"),
|
||||
"",
|
||||
|
@@ -72,7 +72,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
|
||||
extension
|
||||
.AddExpression("normalize",
|
||||
_("Normalize a value between `min` and `max` to a value between 0 and 1."),
|
||||
_("Normalize a value between `min` and `max` to a value "
|
||||
"between 0 and 1."),
|
||||
_("Remap a value between 0 and 1."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
@@ -124,7 +125,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
extension
|
||||
.AddExpression("mod",
|
||||
_("Modulo"),
|
||||
_("x mod y"),
|
||||
_("Compute \"x mod y\". GDevelop does NOT support the \% "
|
||||
"operator. Use this mod(x, y) function instead."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("x (as in x mod y)"))
|
||||
@@ -184,11 +186,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("asinh",
|
||||
_("Arcsine"),
|
||||
_("Arcsine"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"asinh", _("Arcsine"), _("Arcsine"), "", "res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
@@ -218,11 +217,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("cbrt",
|
||||
_("Cube root"),
|
||||
_("Cube root"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"cbrt", _("Cube root"), _("Cube root"), "", "res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
@@ -260,12 +256,13 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("Expression"), "", true);
|
||||
|
||||
extension
|
||||
.AddExpression("cos",
|
||||
_("Cosine"),
|
||||
_("Cosine of an angle (in radian). "
|
||||
"If you want to use degrees, use`ToRad`: `sin(ToRad(45))`."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"cos",
|
||||
_("Cosine"),
|
||||
_("Cosine of an angle (in radian). "
|
||||
"If you want to use degrees, use`ToRad`: `sin(ToRad(45))`."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
@@ -293,29 +290,20 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("int",
|
||||
_("Round"),
|
||||
_("Round a number"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"int", _("Round"), _("Round a number"), "", "res/mathfunction.png")
|
||||
.SetHidden()
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("rint",
|
||||
_("Round"),
|
||||
_("Round a number"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"rint", _("Round"), _("Round a number"), "", "res/mathfunction.png")
|
||||
.SetHidden()
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("round",
|
||||
_("Round"),
|
||||
_("Round a number"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"round", _("Round"), _("Round a number"), "", "res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
@@ -324,8 +312,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
_("Round a number to the Nth decimal place"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"))
|
||||
.AddParameter("expression", _("Expression"), "", true);
|
||||
.AddParameter("expression", _("Number to Round"))
|
||||
.AddParameter("expression", _("Decimal Places"), "", true);
|
||||
|
||||
extension
|
||||
.AddExpression("exp",
|
||||
@@ -336,19 +324,13 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("log",
|
||||
_("Logarithm"),
|
||||
_("Logarithm"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"log", _("Logarithm"), _("Logarithm"), "", "res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("ln",
|
||||
_("Logarithm"),
|
||||
_("Logarithm"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"ln", _("Logarithm"), _("Logarithm"), "", "res/mathfunction.png")
|
||||
.SetHidden()
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
@@ -387,11 +369,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("The exponent (n in x^n)"));
|
||||
|
||||
extension
|
||||
.AddExpression("sec",
|
||||
_("Secant"),
|
||||
_("Secant"),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"sec", _("Secant"), _("Secant"), "", "res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
@@ -403,12 +382,13 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("sin",
|
||||
_("Sine"),
|
||||
_("Sine of an angle (in radian). "
|
||||
"If you want to use degrees, use`ToRad`: `sin(ToRad(45))`."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"sin",
|
||||
_("Sine"),
|
||||
_("Sine of an angle (in radian). "
|
||||
"If you want to use degrees, use`ToRad`: `sin(ToRad(45))`."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
@@ -428,12 +408,13 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
.AddExpression("tan",
|
||||
_("Tangent"),
|
||||
_("Tangent of an angle (in radian). "
|
||||
"If you want to use degrees, use`ToRad`: `tan(ToRad(45))`."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"tan",
|
||||
_("Tangent"),
|
||||
_("Tangent of an angle (in radian). "
|
||||
"If you want to use degrees, use`ToRad`: `tan(ToRad(45))`."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("Expression"));
|
||||
|
||||
extension
|
||||
@@ -463,26 +444,28 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
.AddParameter("expression", _("x (in a+(b-a)*x)"));
|
||||
|
||||
extension
|
||||
.AddExpression("XFromAngleAndDistance",
|
||||
_("X position from angle and distance"),
|
||||
_("Compute the X position when given an angle and distance "
|
||||
"relative to the origin (0;0). This is also known as "
|
||||
"getting the cartesian coordinates of a 2D vector, using "
|
||||
"its polar coordinates."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"XFromAngleAndDistance",
|
||||
_("X position from angle and distance"),
|
||||
_("Compute the X position when given an angle and distance "
|
||||
"relative to the origin (0;0). This is also known as "
|
||||
"getting the cartesian coordinates of a 2D vector, using "
|
||||
"its polar coordinates."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("Angle, in degrees"))
|
||||
.AddParameter("expression", _("Distance"));
|
||||
|
||||
extension
|
||||
.AddExpression("YFromAngleAndDistance",
|
||||
_("Y position from angle and distance"),
|
||||
_("Compute the Y position when given an angle and distance "
|
||||
"relative to the origin (0;0). This is also known as "
|
||||
"getting the cartesian coordinates of a 2D vector, using "
|
||||
"its polar coordinates."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddExpression(
|
||||
"YFromAngleAndDistance",
|
||||
_("Y position from angle and distance"),
|
||||
_("Compute the Y position when given an angle and distance "
|
||||
"relative to the origin (0;0). This is also known as "
|
||||
"getting the cartesian coordinates of a 2D vector, using "
|
||||
"its polar coordinates."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("Angle, in degrees"))
|
||||
.AddParameter("expression", _("Distance"));
|
||||
|
||||
@@ -497,7 +480,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
extension
|
||||
.AddExpression("lerpAngle",
|
||||
_("Lerp (Linear interpolation) between two angles"),
|
||||
_("Linearly interpolates between two angles (in degrees) by taking the shortest direction around the circle."),
|
||||
_("Linearly interpolates between two angles (in degrees) "
|
||||
"by taking the shortest direction around the circle."),
|
||||
"",
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("Starting angle, in degrees"))
|
||||
|
@@ -779,6 +779,26 @@ gd::String PlatformExtension::GetBehaviorFullType(
|
||||
return extensionName + separator + behaviorName;
|
||||
}
|
||||
|
||||
gd::String PlatformExtension::GetExtensionFromFullBehaviorType(
|
||||
const gd::String& type) {
|
||||
const auto separatorIndex =
|
||||
type.find(PlatformExtension::GetNamespaceSeparator());
|
||||
if (separatorIndex == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
return type.substr(0, separatorIndex);
|
||||
}
|
||||
|
||||
gd::String PlatformExtension::GetBehaviorNameFromFullBehaviorType(
|
||||
const gd::String& type) {
|
||||
const auto separatorIndex =
|
||||
type.find(PlatformExtension::GetNamespaceSeparator());
|
||||
if (separatorIndex == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
return type.substr(separatorIndex + 2);
|
||||
}
|
||||
|
||||
gd::String PlatformExtension::GetObjectEventsFunctionFullType(
|
||||
const gd::String& extensionName,
|
||||
const gd::String& objectName,
|
||||
|
@@ -651,6 +651,10 @@ class GD_CORE_API PlatformExtension {
|
||||
static gd::String GetBehaviorFullType(const gd::String& extensionName,
|
||||
const gd::String& behaviorName);
|
||||
|
||||
static gd::String GetExtensionFromFullBehaviorType(const gd::String& type);
|
||||
|
||||
static gd::String GetBehaviorNameFromFullBehaviorType(const gd::String& type);
|
||||
|
||||
static gd::String GetObjectEventsFunctionFullType(
|
||||
const gd::String& extensionName,
|
||||
const gd::String& objectName,
|
||||
|
@@ -1781,6 +1781,14 @@ void WholeProjectRefactorer::DoRenameBehavior(
|
||||
projectBrowser.ExposeFunctions(project, behaviorParameterRenamer);
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::UpdateBehaviorsSharedData(gd::Project &project) {
|
||||
for (std::size_t i = 0; i < project.GetLayoutsCount(); ++i) {
|
||||
gd::Layout &layout = project.GetLayout(i);
|
||||
|
||||
layout.UpdateBehaviorsSharedData(project);
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::DoRenameObject(
|
||||
gd::Project &project, const gd::String &oldObjectType,
|
||||
const gd::String &newObjectType, const gd::ProjectBrowser &projectBrowser) {
|
||||
|
@@ -704,6 +704,16 @@ class GD_CORE_API WholeProjectRefactorer {
|
||||
static size_t GetLayoutAndExternalLayoutLayerInstancesCount(
|
||||
gd::Project &project, gd::Layout &layout, const gd::String &layerName);
|
||||
|
||||
/**
|
||||
* This ensures that the scenes had an instance of shared data for
|
||||
* every behavior of every object that can be used on the scene
|
||||
* (i.e. the objects of the scene and the global objects)
|
||||
*
|
||||
* Must be called when a behavior have been added/deleted
|
||||
* from a global object or an object has been made global.
|
||||
*/
|
||||
static void UpdateBehaviorsSharedData(gd::Project &project);
|
||||
|
||||
virtual ~WholeProjectRefactorer(){};
|
||||
|
||||
private:
|
||||
|
@@ -250,25 +250,28 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor
|
||||
}
|
||||
const auto &eventsBasedObject = project->GetEventsBasedObject(GetType());
|
||||
|
||||
if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) {
|
||||
if (IsForcedToOverrideEventsBasedObjectChildrenConfiguration()) {
|
||||
for (auto &childObject : eventsBasedObject.GetObjects().GetObjects()) {
|
||||
auto &configuration = GetChildObjectConfiguration(childObject->GetName());
|
||||
configuration.ExposeResources(worker);
|
||||
}
|
||||
}
|
||||
else if (eventsBasedObject.GetVariants().HasVariantNamed(variantName)) {
|
||||
for (auto &childObject : eventsBasedObject.GetVariants()
|
||||
.GetVariant(variantName)
|
||||
.GetObjects()
|
||||
.GetObjects()) {
|
||||
childObject->GetConfiguration().ExposeResources(worker);
|
||||
}
|
||||
} else if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) {
|
||||
for (auto &childObject : eventsBasedObject.GetObjects().GetObjects()) {
|
||||
auto &configuration = GetChildObjectConfiguration(childObject->GetName());
|
||||
configuration.ExposeResources(worker);
|
||||
}
|
||||
} else {
|
||||
if (variantName.empty() ||
|
||||
!eventsBasedObject.GetVariants().HasVariantNamed(variantName)) {
|
||||
for (auto &childObject :
|
||||
eventsBasedObject.GetDefaultVariant().GetObjects().GetObjects()) {
|
||||
childObject->GetConfiguration().ExposeResources(worker);
|
||||
}
|
||||
} else {
|
||||
for (auto &childObject : eventsBasedObject.GetVariants()
|
||||
.GetVariant(variantName)
|
||||
.GetObjects()
|
||||
.GetObjects()) {
|
||||
childObject->GetConfiguration().ExposeResources(worker);
|
||||
}
|
||||
for (auto &childObject :
|
||||
eventsBasedObject.GetDefaultVariant().GetObjects().GetObjects()) {
|
||||
childObject->GetConfiguration().ExposeResources(worker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -78,6 +78,15 @@ public:
|
||||
variantName = variantName_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy events-based objects don't have any instance in their default
|
||||
* variant since there wasn't a graphical editor at the time. In this case,
|
||||
* the editor doesn't allow to choose a variant, but a variant may have stayed
|
||||
* after a user rolled back the extension. This variant must be ignored.
|
||||
*
|
||||
* @return true when its events-based object doesn't have any initial
|
||||
* instance.
|
||||
*/
|
||||
bool IsForcedToOverrideEventsBasedObjectChildrenConfiguration() const;
|
||||
|
||||
bool IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration() const {
|
||||
|
@@ -83,6 +83,9 @@ void EventsFunctionsExtension::SerializeTo(SerializerElement& element, bool isEx
|
||||
element.SetAttribute("iconUrl", iconUrl);
|
||||
element.SetAttribute("helpPath", helpPath);
|
||||
element.SetAttribute("gdevelopVersion", gdevelopVersion);
|
||||
if (changelog.GetChangesCount() > 0) {
|
||||
changelog.SerializeTo(element.AddChild("changelog"));
|
||||
}
|
||||
auto& dependenciesElement = element.AddChild("dependencies");
|
||||
dependenciesElement.ConsiderAsArray();
|
||||
for (auto& dependency : dependencies)
|
||||
@@ -139,6 +142,9 @@ void EventsFunctionsExtension::UnserializeExtensionDeclarationFrom(
|
||||
iconUrl = element.GetStringAttribute("iconUrl");
|
||||
helpPath = element.GetStringAttribute("helpPath");
|
||||
gdevelopVersion = element.GetStringAttribute("gdevelopVersion");
|
||||
if (element.HasChild("changelog")) {
|
||||
changelog.UnserializeFrom(element.GetChild("changelog"));
|
||||
}
|
||||
|
||||
if (element.HasChild("origin")) {
|
||||
gd::String originName =
|
||||
|
@@ -12,9 +12,11 @@
|
||||
#include "GDCore/Project/EventsBasedBehavior.h"
|
||||
#include "GDCore/Project/EventsBasedObject.h"
|
||||
#include "GDCore/Project/EventsFunctionsContainer.h"
|
||||
#include "GDCore/Project/EventsFunctionsExtensionChangelog.h"
|
||||
#include "GDCore/Project/VariablesContainer.h"
|
||||
#include "GDCore/String.h"
|
||||
#include "GDCore/Tools/SerializableWithNameList.h"
|
||||
|
||||
namespace gd {
|
||||
class SerializerElement;
|
||||
class Project;
|
||||
@@ -406,6 +408,7 @@ class GD_CORE_API EventsFunctionsExtension {
|
||||
gd::String helpPath; ///< The relative path to the help for this extension in
|
||||
///< the documentation (or an absolute URL).
|
||||
gd::String gdevelopVersion;
|
||||
gd::EventsFunctionsExtensionChangelog changelog;
|
||||
gd::SerializableWithNameList<EventsBasedBehavior> eventsBasedBehaviors;
|
||||
gd::SerializableWithNameList<EventsBasedObject> eventsBasedObjects;
|
||||
std::vector<gd::DependencyMetadata> dependencies;
|
||||
|
105
Core/GDCore/Project/EventsFunctionsExtensionChangelog.h
Normal file
105
Core/GDCore/Project/EventsFunctionsExtensionChangelog.h
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "GDCore/Serialization/SerializerElement.h"
|
||||
#include "GDCore/String.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
/**
|
||||
* @brief The change of a specific extension version (only the breaking
|
||||
* changes).
|
||||
*/
|
||||
class GD_CORE_API EventsFunctionsExtensionVersionChange {
|
||||
public:
|
||||
EventsFunctionsExtensionVersionChange(){};
|
||||
virtual ~EventsFunctionsExtensionVersionChange(){};
|
||||
|
||||
const gd::String &GetVersion() const { return version; };
|
||||
gd::EventsFunctionsExtensionVersionChange &
|
||||
SetVersion(const gd::String &version_) {
|
||||
version = version_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
const gd::String &GetBreakingChangesDescription() const { return version; };
|
||||
gd::EventsFunctionsExtensionVersionChange &
|
||||
GetBreakingChangesDescription(const gd::String &breakingChangesDescription_) {
|
||||
breakingChangesDescription = breakingChangesDescription_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Serialize the EventsFunctionsExtensionVersionChange to the specified
|
||||
* element
|
||||
*/
|
||||
void SerializeTo(gd::SerializerElement &element) const {
|
||||
element.SetAttribute("version", version);
|
||||
element.AddChild("breaking")
|
||||
.SetMultilineStringValue(breakingChangesDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Load the EventsFunctionsExtensionVersionChange from the specified
|
||||
* element.
|
||||
*/
|
||||
void UnserializeFrom(const gd::SerializerElement &element) {
|
||||
version = element.GetStringAttribute("version");
|
||||
breakingChangesDescription =
|
||||
element.GetChild("breaking").GetMultilineStringValue();
|
||||
}
|
||||
|
||||
private:
|
||||
gd::String version;
|
||||
gd::String breakingChangesDescription;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The changelog of an extension (only the breaking changes).
|
||||
*/
|
||||
class GD_CORE_API EventsFunctionsExtensionChangelog {
|
||||
public:
|
||||
EventsFunctionsExtensionChangelog(){};
|
||||
virtual ~EventsFunctionsExtensionChangelog(){};
|
||||
|
||||
/**
|
||||
* \brief Return the number of variants.
|
||||
*/
|
||||
std::size_t GetChangesCount() const { return versionChanges.size(); }
|
||||
|
||||
/**
|
||||
* \brief Serialize the EventsFunctionsExtensionChangelog to the specified
|
||||
* element
|
||||
*/
|
||||
void SerializeTo(gd::SerializerElement &element) const {
|
||||
element.ConsiderAsArray();
|
||||
for (const auto &versionChange : versionChanges) {
|
||||
versionChange.SerializeTo(element.AddChild(""));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Load the EventsFunctionsExtensionChangelog from the specified
|
||||
* element.
|
||||
*/
|
||||
void UnserializeFrom(const gd::SerializerElement &element) {
|
||||
versionChanges.clear();
|
||||
element.ConsiderAsArray();
|
||||
for (std::size_t i = 0; i < element.GetChildrenCount(); ++i) {
|
||||
gd::EventsFunctionsExtensionVersionChange versionChange;
|
||||
versionChange.UnserializeFrom(element.GetChild(i));
|
||||
versionChanges.push_back(versionChange);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<gd::EventsFunctionsExtensionVersionChange> versionChanges;
|
||||
};
|
||||
|
||||
} // namespace gd
|
@@ -365,6 +365,8 @@ class GD_CORE_API InitialInstance {
|
||||
* the same initial instance between serialization.
|
||||
*/
|
||||
InitialInstance& ResetPersistentUuid();
|
||||
|
||||
const gd::String& GetPersistentUuid() const { return persistentUuid; }
|
||||
///@}
|
||||
|
||||
private:
|
||||
|
@@ -81,6 +81,11 @@ class GD_CORE_API ObjectsContainersList {
|
||||
/**
|
||||
* \brief Return the container of the variables for the specified object or
|
||||
* group of objects.
|
||||
*
|
||||
* \warning In most cases, prefer to use other methods to access variables or use
|
||||
* ObjectVariableHelper::MergeVariableContainers if you know you're dealing with a group.
|
||||
* This is because the variables container of an object group does not exist and the one from
|
||||
* first object of the group will be returned.
|
||||
*/
|
||||
const gd::VariablesContainer* GetObjectOrGroupVariablesContainer(
|
||||
const gd::String& objectOrGroupName) const;
|
||||
|
@@ -21,14 +21,19 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
|
||||
element.AddChild("unit").SetStringValue(measurementUnit.GetName());
|
||||
}
|
||||
element.AddChild("label").SetStringValue(label);
|
||||
element.AddChild("description").SetStringValue(description);
|
||||
element.AddChild("group").SetStringValue(group);
|
||||
SerializerElement& extraInformationElement =
|
||||
element.AddChild("extraInformation");
|
||||
extraInformationElement.ConsiderAsArray();
|
||||
for (const gd::String& information : extraInformation) {
|
||||
extraInformationElement.AddChild("").SetStringValue(information);
|
||||
if (!description.empty())
|
||||
element.AddChild("description").SetStringValue(description);
|
||||
if (!group.empty()) element.AddChild("group").SetStringValue(group);
|
||||
|
||||
if (!extraInformation.empty()) {
|
||||
SerializerElement& extraInformationElement =
|
||||
element.AddChild("extraInformation");
|
||||
extraInformationElement.ConsiderAsArray();
|
||||
for (const gd::String& information : extraInformation) {
|
||||
extraInformationElement.AddChild("").SetStringValue(information);
|
||||
}
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
element.AddChild("hidden").SetBoolValue(hidden);
|
||||
}
|
||||
@@ -59,16 +64,21 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
: gd::MeasurementUnit::GetUndefined();
|
||||
}
|
||||
label = element.GetChild("label").GetStringValue();
|
||||
description = element.GetChild("description").GetStringValue();
|
||||
group = element.GetChild("group").GetStringValue();
|
||||
description = element.HasChild("description")
|
||||
? element.GetChild("description").GetStringValue()
|
||||
: "";
|
||||
group = element.HasChild("group") ? element.GetChild("group").GetStringValue()
|
||||
: "";
|
||||
|
||||
extraInformation.clear();
|
||||
const SerializerElement& extraInformationElement =
|
||||
element.GetChild("extraInformation");
|
||||
extraInformationElement.ConsiderAsArray();
|
||||
for (std::size_t i = 0; i < extraInformationElement.GetChildrenCount(); ++i)
|
||||
extraInformation.push_back(
|
||||
extraInformationElement.GetChild(i).GetStringValue());
|
||||
if (element.HasChild("extraInformation")) {
|
||||
const SerializerElement& extraInformationElement =
|
||||
element.GetChild("extraInformation");
|
||||
extraInformationElement.ConsiderAsArray();
|
||||
for (std::size_t i = 0; i < extraInformationElement.GetChildrenCount(); ++i)
|
||||
extraInformation.push_back(
|
||||
extraInformationElement.GetChild(i).GetStringValue());
|
||||
}
|
||||
|
||||
hidden = element.HasChild("hidden")
|
||||
? element.GetChild("hidden").GetBoolValue()
|
||||
|
@@ -34,7 +34,6 @@ namespace gdjs {
|
||||
objectData: gdjs.Object3DData & gdjs.CustomObjectConfiguration
|
||||
) {
|
||||
super(parent, objectData);
|
||||
this._renderer.reinitialize(this, parent);
|
||||
}
|
||||
|
||||
protected override _createRender() {
|
||||
|
@@ -44,10 +44,7 @@ namespace gdjs {
|
||||
) {
|
||||
this._object = object;
|
||||
this._isContainerDirty = true;
|
||||
const layer = parent.getLayer('');
|
||||
if (layer) {
|
||||
layer.getRenderer().add3DRendererObject(this._threeGroup);
|
||||
}
|
||||
this._threeGroup.clear();
|
||||
}
|
||||
|
||||
_updateThreeGroup() {
|
||||
|
@@ -14,6 +14,7 @@ describe('gdjs.AnchorRuntimeBehavior', () => {
|
||||
effects: [],
|
||||
content: {},
|
||||
childrenContent: {},
|
||||
isInnerAreaFollowingParentSize: false,
|
||||
});
|
||||
runtimeScene.addObject(customObject);
|
||||
customObject.setPosition(500, 250);
|
||||
|
@@ -12,7 +12,7 @@ This project is released under the MIT License.
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
|
||||
void DestroyOutsideBehavior::InitializeContent(gd::SerializerElement& content) {
|
||||
content.SetAttribute("extraBorder", 0);
|
||||
content.SetAttribute("extraBorder", 300);
|
||||
}
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
|
@@ -12,35 +12,42 @@ This project is released under the MIT License.
|
||||
|
||||
void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("DestroyOutsideBehavior",
|
||||
_("Destroy Outside Screen Behavior"),
|
||||
_("This behavior can be used to destroy "
|
||||
"objects when they go outside of "
|
||||
"the bounds of the camera. Useful for bullets "
|
||||
"or other short-lived objects."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionInformation(
|
||||
"DestroyOutsideBehavior",
|
||||
_("Destroy Outside Screen Behavior"),
|
||||
_("This behavior can be used to destroy objects when they go "
|
||||
"outside of the bounds of the 2D camera. Useful for 2D bullets or "
|
||||
"other short-lived objects. Don't use it for 3D objects in a "
|
||||
"FPS/TPS game or any game with a camera not being a top view "
|
||||
"(for 3D objects, prefer comparing "
|
||||
"the position, for example Z position to see if an object goes "
|
||||
"outside of the bound of the map). Be careful when using this "
|
||||
"behavior because if the object appears outside of the screen, it "
|
||||
"will be immediately removed."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Game mechanic")
|
||||
.SetTags("screen")
|
||||
.SetExtensionHelpPath("/behaviors/destroyoutside");
|
||||
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension.AddBehavior("DestroyOutside",
|
||||
_("Destroy when outside of the screen"),
|
||||
_("DestroyOutside"),
|
||||
_("Destroy objects automatically when they go "
|
||||
"outside of the screen's borders."),
|
||||
"",
|
||||
"CppPlatform/Extensions/destroyoutsideicon.png",
|
||||
"DestroyOutsideBehavior",
|
||||
std::make_shared<DestroyOutsideBehavior>(),
|
||||
std::shared_ptr<gd::BehaviorsSharedData>())
|
||||
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
|
||||
extension
|
||||
.AddBehavior("DestroyOutside",
|
||||
_("Destroy when outside of the screen"),
|
||||
_("DestroyOutside"),
|
||||
_("Destroy objects automatically when they go "
|
||||
"outside of the 2D camera borders."),
|
||||
"",
|
||||
"CppPlatform/Extensions/destroyoutsideicon.png",
|
||||
"DestroyOutsideBehavior",
|
||||
std::make_shared<DestroyOutsideBehavior>(),
|
||||
std::shared_ptr<gd::BehaviorsSharedData>())
|
||||
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
|
||||
|
||||
aut.AddCondition("ExtraBorder",
|
||||
_("Additional border"),
|
||||
_("Compare the additional border that the object must cross "
|
||||
"before being deleted."),
|
||||
_("Additional border (extra distance before deletion)"),
|
||||
_("Compare the extra distance (in pixels) the object must "
|
||||
"travel beyond the screen before it gets deleted."),
|
||||
_("the additional border"),
|
||||
_("Destroy outside configuration"),
|
||||
"CppPlatform/Extensions/destroyoutsideicon24.png",
|
||||
@@ -53,9 +60,9 @@ void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("GetExtraBorder");
|
||||
|
||||
aut.AddAction("ExtraBorder",
|
||||
_("Additional border"),
|
||||
_("Change the additional border that the object must cross "
|
||||
"before being deleted."),
|
||||
_("Additional border (extra distance before deletion)"),
|
||||
_("Change the extra distance (in pixels) the object must "
|
||||
"travel beyond the screen before it gets deleted."),
|
||||
_("the additional border"),
|
||||
_("Destroy outside configuration"),
|
||||
"CppPlatform/Extensions/destroyoutsideicon24.png",
|
||||
|
@@ -35,25 +35,32 @@ void DeclareDraggableBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
std::make_shared<DraggableBehavior>(),
|
||||
std::shared_ptr<gd::BehaviorsSharedData>());
|
||||
|
||||
aut.AddCondition("Dragged",
|
||||
_("Being dragged"),
|
||||
_("Check if the object is being dragged."),
|
||||
_("_PARAM0_ is being dragged"),
|
||||
_("Draggable"),
|
||||
"CppPlatform/Extensions/draggableicon24.png",
|
||||
"CppPlatform/Extensions/draggableicon16.png")
|
||||
aut.AddCondition(
|
||||
"Dragged",
|
||||
_("Being dragged"),
|
||||
_("Check if the object is being dragged. This means the mouse button "
|
||||
"or touch is pressed on it. When the mouse button or touch is "
|
||||
"released, the object is no longer being considered dragged (use "
|
||||
"the condition \"Was just dropped\" to check when the dragging is "
|
||||
"ending)."),
|
||||
_("_PARAM0_ is being dragged"),
|
||||
_("Draggable"),
|
||||
"CppPlatform/Extensions/draggableicon24.png",
|
||||
"CppPlatform/Extensions/draggableicon16.png")
|
||||
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "Draggable")
|
||||
.SetFunctionName("IsDragged");
|
||||
|
||||
aut.AddCondition("Dropped",
|
||||
_("Was just dropped"),
|
||||
_("Check if the object was just dropped after being dragged."),
|
||||
_("_PARAM0_ was just dropped"),
|
||||
_("Draggable"),
|
||||
"CppPlatform/Extensions/draggableicon24.png",
|
||||
"CppPlatform/Extensions/draggableicon16.png")
|
||||
aut.AddCondition(
|
||||
"Dropped",
|
||||
_("Was just dropped"),
|
||||
_("Check if the object was just dropped after being dragged (the "
|
||||
"mouse button or touch was just released this frame)."),
|
||||
_("_PARAM0_ was just dropped"),
|
||||
_("Draggable"),
|
||||
"CppPlatform/Extensions/draggableicon24.png",
|
||||
"CppPlatform/Extensions/draggableicon16.png")
|
||||
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "Draggable")
|
||||
|
@@ -116,48 +116,7 @@ namespace gdjs {
|
||||
|
||||
_updateLocalPositions() {
|
||||
const obj = this._object;
|
||||
this._centerSprite.position.x = obj._lBorder;
|
||||
this._centerSprite.position.y = obj._tBorder;
|
||||
|
||||
//Right
|
||||
this._borderSprites[0].position.x = obj._width - obj._rBorder;
|
||||
this._borderSprites[0].position.y = obj._tBorder;
|
||||
|
||||
//Top-right
|
||||
this._borderSprites[1].position.x =
|
||||
obj._width - this._borderSprites[1].width;
|
||||
this._borderSprites[1].position.y = 0;
|
||||
|
||||
//Top
|
||||
this._borderSprites[2].position.x = obj._lBorder;
|
||||
this._borderSprites[2].position.y = 0;
|
||||
|
||||
//Top-Left
|
||||
this._borderSprites[3].position.x = 0;
|
||||
this._borderSprites[3].position.y = 0;
|
||||
|
||||
//Left
|
||||
this._borderSprites[4].position.x = 0;
|
||||
this._borderSprites[4].position.y = obj._tBorder;
|
||||
|
||||
//Bottom-Left
|
||||
this._borderSprites[5].position.x = 0;
|
||||
this._borderSprites[5].position.y =
|
||||
obj._height - this._borderSprites[5].height;
|
||||
|
||||
//Bottom
|
||||
this._borderSprites[6].position.x = obj._lBorder;
|
||||
this._borderSprites[6].position.y = obj._height - obj._bBorder;
|
||||
|
||||
//Bottom-Right
|
||||
this._borderSprites[7].position.x =
|
||||
obj._width - this._borderSprites[7].width;
|
||||
this._borderSprites[7].position.y =
|
||||
obj._height - this._borderSprites[7].height;
|
||||
}
|
||||
|
||||
_updateSpritesAndTexturesSize() {
|
||||
const obj = this._object;
|
||||
this._centerSprite.width = Math.max(
|
||||
obj._width - obj._rBorder - obj._lBorder,
|
||||
0
|
||||
@@ -167,35 +126,107 @@ namespace gdjs {
|
||||
0
|
||||
);
|
||||
|
||||
let leftMargin = obj._lBorder;
|
||||
let rightMargin = obj._rBorder;
|
||||
if (this._centerSprite.width === 0 && obj._lBorder + obj._rBorder > 0) {
|
||||
leftMargin =
|
||||
(obj._width * obj._lBorder) / (obj._lBorder + obj._rBorder);
|
||||
rightMargin = obj._width - leftMargin;
|
||||
}
|
||||
let topMargin = obj._tBorder;
|
||||
let bottomMargin = obj._bBorder;
|
||||
if (this._centerSprite.height === 0 && obj._tBorder + obj._bBorder > 0) {
|
||||
topMargin =
|
||||
(obj._height * obj._tBorder) / (obj._tBorder + obj._bBorder);
|
||||
bottomMargin = obj._height - topMargin;
|
||||
}
|
||||
|
||||
//Right
|
||||
this._borderSprites[0].width = obj._rBorder;
|
||||
this._borderSprites[0].width = rightMargin;
|
||||
this._borderSprites[0].height = Math.max(
|
||||
obj._height - obj._tBorder - obj._bBorder,
|
||||
obj._height - topMargin - bottomMargin,
|
||||
0
|
||||
);
|
||||
|
||||
//Top
|
||||
this._borderSprites[2].height = obj._tBorder;
|
||||
this._borderSprites[2].height = topMargin;
|
||||
this._borderSprites[2].width = Math.max(
|
||||
obj._width - obj._rBorder - obj._lBorder,
|
||||
obj._width - rightMargin - leftMargin,
|
||||
0
|
||||
);
|
||||
|
||||
//Left
|
||||
this._borderSprites[4].width = obj._lBorder;
|
||||
this._borderSprites[4].width = leftMargin;
|
||||
this._borderSprites[4].height = Math.max(
|
||||
obj._height - obj._tBorder - obj._bBorder,
|
||||
obj._height - topMargin - bottomMargin,
|
||||
0
|
||||
);
|
||||
|
||||
//Bottom
|
||||
this._borderSprites[6].height = obj._bBorder;
|
||||
this._borderSprites[6].height = bottomMargin;
|
||||
this._borderSprites[6].width = Math.max(
|
||||
obj._width - obj._rBorder - obj._lBorder,
|
||||
obj._width - rightMargin - leftMargin,
|
||||
0
|
||||
);
|
||||
|
||||
//Top-right
|
||||
this._borderSprites[1].width = rightMargin;
|
||||
this._borderSprites[1].height = topMargin;
|
||||
|
||||
//Top-Left
|
||||
this._borderSprites[3].width = leftMargin;
|
||||
this._borderSprites[3].height = topMargin;
|
||||
|
||||
//Bottom-Left
|
||||
this._borderSprites[5].width = leftMargin;
|
||||
this._borderSprites[5].height = bottomMargin;
|
||||
|
||||
//Bottom-Right
|
||||
this._borderSprites[7].width = rightMargin;
|
||||
this._borderSprites[7].height = bottomMargin;
|
||||
|
||||
this._wasRendered = true;
|
||||
this._spritesContainer.cacheAsBitmap = false;
|
||||
|
||||
const leftBorder = leftMargin;
|
||||
const topBorder = topMargin;
|
||||
const rightBorder = obj._width - rightMargin;
|
||||
const bottomBorder = obj._height - bottomMargin;
|
||||
|
||||
this._centerSprite.position.x = leftBorder;
|
||||
this._centerSprite.position.y = topBorder;
|
||||
|
||||
//Right
|
||||
this._borderSprites[0].position.x = rightBorder;
|
||||
this._borderSprites[0].position.y = topBorder;
|
||||
|
||||
//Top-right
|
||||
this._borderSprites[1].position.x = rightBorder;
|
||||
this._borderSprites[1].position.y = 0;
|
||||
|
||||
//Top
|
||||
this._borderSprites[2].position.x = leftBorder;
|
||||
this._borderSprites[2].position.y = 0;
|
||||
|
||||
//Top-Left
|
||||
this._borderSprites[3].position.x = 0;
|
||||
this._borderSprites[3].position.y = 0;
|
||||
|
||||
//Left
|
||||
this._borderSprites[4].position.x = 0;
|
||||
this._borderSprites[4].position.y = topBorder;
|
||||
|
||||
//Bottom-Left
|
||||
this._borderSprites[5].position.x = 0;
|
||||
this._borderSprites[5].position.y = bottomBorder;
|
||||
|
||||
//Bottom
|
||||
this._borderSprites[6].position.x = leftBorder;
|
||||
this._borderSprites[6].position.y = bottomBorder;
|
||||
|
||||
//Bottom-Right
|
||||
this._borderSprites[7].position.x = rightBorder;
|
||||
this._borderSprites[7].position.y = bottomBorder;
|
||||
}
|
||||
|
||||
setTexture(
|
||||
@@ -340,7 +371,6 @@ namespace gdjs {
|
||||
)
|
||||
)
|
||||
);
|
||||
this._updateSpritesAndTexturesSize();
|
||||
this._updateLocalPositions();
|
||||
this.updatePosition();
|
||||
this._wrapperContainer.pivot.x = this._object._width / 2;
|
||||
@@ -349,14 +379,12 @@ namespace gdjs {
|
||||
|
||||
updateWidth(): void {
|
||||
this._wrapperContainer.pivot.x = this._object._width / 2;
|
||||
this._updateSpritesAndTexturesSize();
|
||||
this._updateLocalPositions();
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
updateHeight(): void {
|
||||
this._wrapperContainer.pivot.y = this._object._height / 2;
|
||||
this._updateSpritesAndTexturesSize();
|
||||
this._updateLocalPositions();
|
||||
this.updatePosition();
|
||||
}
|
||||
|
@@ -37,9 +37,9 @@ void DeclareParticleSystemExtension(gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.AddObject<ParticleEmitterObject>(
|
||||
"ParticleEmitter",
|
||||
_("Particles emitter"),
|
||||
_("2D particles emitter"),
|
||||
_("Displays a large number of small 2D particles to create "
|
||||
"visual effects."),
|
||||
"visual effects in a 2D game or user interface."),
|
||||
"CppPlatform/Extensions/particleSystemicon.png")
|
||||
.SetCategoryFullName(_("Visual effect"))
|
||||
.AddDefaultBehavior("EffectCapability::EffectBehavior");
|
||||
|
@@ -818,7 +818,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('yesorno', _('Treat as bullet?'), '', false)
|
||||
.addParameter('yesorno', _('Treat as bullet'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('setBullet');
|
||||
@@ -852,7 +852,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('yesorno', _('Fixed rotation?'), '', false)
|
||||
.addParameter('yesorno', _('Fixed rotation'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('setFixedRotation');
|
||||
@@ -886,7 +886,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('yesorno', _('Can sleep?'), '', false)
|
||||
.addParameter('yesorno', _('Can sleep'), '', false)
|
||||
.setDefaultValue('true')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('setSleepingAllowed');
|
||||
@@ -1296,7 +1296,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Layer (1 - 16)'))
|
||||
.addParameter('yesorno', _('Enable?'), '', false)
|
||||
.addParameter('yesorno', _('Enable'), '', false)
|
||||
.setDefaultValue('true')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enableLayer');
|
||||
@@ -1332,7 +1332,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Mask (1 - 16)'))
|
||||
.addParameter('yesorno', _('Enable?'), '', false)
|
||||
.addParameter('yesorno', _('Enable'), '', false)
|
||||
.setDefaultValue('true')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enableMask');
|
||||
@@ -2409,7 +2409,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Joint ID'))
|
||||
.addParameter('yesorno', _('Enable?'))
|
||||
.addParameter('yesorno', _('Enable'))
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enableRevoluteJointLimits');
|
||||
|
||||
@@ -2488,7 +2488,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Joint ID'))
|
||||
.addParameter('yesorno', _('Enable?'))
|
||||
.addParameter('yesorno', _('Enable'))
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enableRevoluteJointMotor');
|
||||
|
||||
@@ -2727,7 +2727,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Joint ID'))
|
||||
.addParameter('yesorno', _('Enable?'))
|
||||
.addParameter('yesorno', _('Enable'))
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enablePrismaticJointLimits');
|
||||
|
||||
@@ -2806,7 +2806,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Joint ID'))
|
||||
.addParameter('yesorno', _('Enable?'))
|
||||
.addParameter('yesorno', _('Enable'))
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enablePrismaticJointMotor');
|
||||
|
||||
@@ -3486,7 +3486,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Joint ID'))
|
||||
.addParameter('yesorno', _('Enable?'))
|
||||
.addParameter('yesorno', _('Enable'))
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enableWheelJointMotor');
|
||||
|
||||
|
@@ -274,7 +274,7 @@ module.exports = {
|
||||
.setLabel('Fixed Rotation')
|
||||
.setDescription(
|
||||
_(
|
||||
"If enabled, the object won't rotate and will stay at the same angle. Useful for characters for example."
|
||||
"If enabled, the object won't rotate and will stay at the same angle."
|
||||
)
|
||||
)
|
||||
.setGroup(_('Movement'));
|
||||
@@ -845,7 +845,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.addParameter('yesorno', _('Treat as bullet?'), '', false)
|
||||
.addParameter('yesorno', _('Treat as bullet'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('setBullet');
|
||||
@@ -870,7 +870,7 @@ module.exports = {
|
||||
'SetFixedRotation',
|
||||
_('Fixed rotation'),
|
||||
_(
|
||||
"Enable or disable an object fixed rotation. If enabled the object won't be able to rotate."
|
||||
"Enable or disable an object fixed rotation. If enabled the object won't be able to rotate. This action has no effect on characters."
|
||||
),
|
||||
_('Set _PARAM0_ fixed rotation: _PARAM2_'),
|
||||
_('Dynamics'),
|
||||
@@ -879,7 +879,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.addParameter('yesorno', _('Fixed rotation?'), '', false)
|
||||
.addParameter('yesorno', _('Fixed rotation'), '', false)
|
||||
.setDefaultValue('false')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('setFixedRotation');
|
||||
@@ -1054,7 +1054,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.addParameter('expression', _('Layer (1 - 8)'))
|
||||
.addParameter('yesorno', _('Enable?'), '', false)
|
||||
.addParameter('yesorno', _('Enable'), '', false)
|
||||
.setDefaultValue('true')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enableLayer');
|
||||
@@ -1090,7 +1090,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.addParameter('expression', _('Mask (1 - 8)'))
|
||||
.addParameter('yesorno', _('Enable?'), '', false)
|
||||
.addParameter('yesorno', _('Enable'), '', false)
|
||||
.setDefaultValue('true')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('enableMask');
|
||||
@@ -1270,7 +1270,7 @@ module.exports = {
|
||||
.addParameter('expression', _('Application point on Z axis'))
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'Use `MassCenterX` and `MassCenterY` expressions to avoid any rotation.'
|
||||
'Use `MassCenterX`, `MassCenterY` and `MassCenterZ` expressions to avoid any rotation.'
|
||||
)
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
@@ -1544,6 +1544,19 @@ module.exports = {
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('getMassCenterY');
|
||||
|
||||
aut
|
||||
.addExpression(
|
||||
'MassCenterZ',
|
||||
_('Mass center Z'),
|
||||
_('Mass center Z'),
|
||||
'',
|
||||
'JsPlatform/Extensions/physics3d.svg'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('getMassCenterZ');
|
||||
}
|
||||
// Collision
|
||||
extension
|
||||
|
@@ -927,9 +927,7 @@ namespace gdjs {
|
||||
const angularVelocityY = angularVelocity.GetY();
|
||||
const angularVelocityZ = angularVelocity.GetZ();
|
||||
|
||||
let bodyID = this._body.GetID();
|
||||
bodyInterface.RemoveBody(bodyID);
|
||||
bodyInterface.DestroyBody(bodyID);
|
||||
this.bodyUpdater.destroyBody();
|
||||
this._contactsEndedThisFrame.length = 0;
|
||||
this._contactsStartedThisFrame.length = 0;
|
||||
this._currentContacts.length = 0;
|
||||
@@ -938,7 +936,7 @@ namespace gdjs {
|
||||
if (!this._body) {
|
||||
return;
|
||||
}
|
||||
bodyID = this._body.GetID();
|
||||
const bodyID = this._body.GetID();
|
||||
bodyInterface.SetLinearVelocity(
|
||||
bodyID,
|
||||
this.getVec3(linearVelocityX, linearVelocityY, linearVelocityZ)
|
||||
|
@@ -733,7 +733,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
setWheelOffsetZ(wheelOffsetZ: float): void {
|
||||
this._wheelOffsetY = wheelOffsetZ;
|
||||
this._wheelOffsetZ = wheelOffsetZ;
|
||||
this._updateWheels();
|
||||
}
|
||||
|
||||
@@ -783,11 +783,11 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
hasFrontWheelDrive(): boolean {
|
||||
return this._hasBackWheelDrive;
|
||||
return this._hasFrontWheelDrive;
|
||||
}
|
||||
|
||||
setFrontWheelDrive(hasFrontWheelDrive: boolean): void {
|
||||
this._hasBackWheelDrive = hasFrontWheelDrive;
|
||||
this._hasFrontWheelDrive = hasFrontWheelDrive;
|
||||
this.invalidateShape();
|
||||
}
|
||||
|
||||
|
@@ -36,6 +36,7 @@ void PlatformerObjectBehavior::InitializeContent(
|
||||
behaviorContent.SetAttribute("yGrabOffset", 0);
|
||||
behaviorContent.SetAttribute("xGrabTolerance", 10);
|
||||
behaviorContent.SetAttribute("useLegacyTrajectory", false);
|
||||
behaviorContent.SetAttribute("useRepeatedJump", false);
|
||||
behaviorContent.SetAttribute("canGoDownFromJumpthru", true);
|
||||
}
|
||||
|
||||
@@ -108,11 +109,11 @@ PlatformerObjectBehavior::GetProperties(
|
||||
.SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("maxSpeed")));
|
||||
properties["IgnoreDefaultControls"]
|
||||
.SetLabel(_("Default controls"))
|
||||
.SetLabel(_("Disable default keyboard controls"))
|
||||
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden)
|
||||
.SetValue(behaviorContent.GetBoolAttribute("ignoreDefaultControls")
|
||||
? "false"
|
||||
: "true")
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties["SlopeMaxAngle"]
|
||||
.SetLabel(_("Slope max. angle"))
|
||||
@@ -156,14 +157,23 @@ PlatformerObjectBehavior::GetProperties(
|
||||
.SetValue(gd::String::From(
|
||||
behaviorContent.GetDoubleAttribute("xGrabTolerance", 10)));
|
||||
properties["UseLegacyTrajectory"]
|
||||
.SetLabel(_("Use frame rate dependent trajectories (deprecated, it's "
|
||||
"recommended to leave this unchecked)"))
|
||||
.SetLabel(_("Use frame rate dependent trajectories "
|
||||
"(deprecated — best left unchecked)"))
|
||||
.SetGroup(_("Deprecated options"))
|
||||
.SetDeprecated()
|
||||
.SetValue(behaviorContent.GetBoolAttribute("useLegacyTrajectory", true)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties["UseRepeatedJump"]
|
||||
.SetLabel(_("Allows repeated jumps while holding the jump key "
|
||||
"(deprecated — best left unchecked)"))
|
||||
.SetGroup(_("Deprecated options"))
|
||||
.SetDeprecated()
|
||||
.SetValue(behaviorContent.GetBoolAttribute("useRepeatedJump", true)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties["CanGoDownFromJumpthru"]
|
||||
.SetLabel(_("Can go down from jumpthru platforms"))
|
||||
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden)
|
||||
@@ -180,13 +190,15 @@ bool PlatformerObjectBehavior::UpdateProperty(
|
||||
const gd::String& name,
|
||||
const gd::String& value) {
|
||||
if (name == "IgnoreDefaultControls")
|
||||
behaviorContent.SetAttribute("ignoreDefaultControls", (value == "0"));
|
||||
behaviorContent.SetAttribute("ignoreDefaultControls", (value == "1"));
|
||||
else if (name == "CanGrabPlatforms")
|
||||
behaviorContent.SetAttribute("canGrabPlatforms", (value == "1"));
|
||||
else if (name == "CanGrabWithoutMoving")
|
||||
behaviorContent.SetAttribute("canGrabWithoutMoving", (value == "1"));
|
||||
else if (name == "UseLegacyTrajectory")
|
||||
behaviorContent.SetAttribute("useLegacyTrajectory", (value == "1"));
|
||||
else if (name == "UseRepeatedJump")
|
||||
behaviorContent.SetAttribute("useRepeatedJump", (value == "1"));
|
||||
else if (name == "CanGoDownFromJumpthru")
|
||||
behaviorContent.SetAttribute("canGoDownFromJumpthru", (value == "1"));
|
||||
else if (name == "YGrabOffset")
|
||||
|
@@ -23,7 +23,6 @@ namespace gdjs {
|
||||
interface JumpingStateNetworkSyncData {
|
||||
cjs: number;
|
||||
tscjs: number;
|
||||
jkhsjs: boolean;
|
||||
jfd: boolean;
|
||||
}
|
||||
|
||||
@@ -57,6 +56,7 @@ namespace gdjs {
|
||||
juk: boolean;
|
||||
rpk: boolean;
|
||||
rlk: boolean;
|
||||
jkhsjs: boolean;
|
||||
sn: string;
|
||||
ssd: StateNetworkSyncData;
|
||||
}
|
||||
@@ -119,6 +119,7 @@ namespace gdjs {
|
||||
private _xGrabTolerance: any;
|
||||
|
||||
_useLegacyTrajectory: boolean;
|
||||
_useRepeatedJump: boolean;
|
||||
|
||||
_canGoDownFromJumpthru: boolean = false;
|
||||
|
||||
@@ -139,6 +140,7 @@ namespace gdjs {
|
||||
_upKey: boolean = false;
|
||||
_downKey: boolean = false;
|
||||
_jumpKey: boolean = false;
|
||||
_jumpKeyHeldSinceJumpStart: boolean = false;
|
||||
_releasePlatformKey: boolean = false;
|
||||
_releaseLadderKey: boolean = false;
|
||||
|
||||
@@ -204,6 +206,10 @@ namespace gdjs {
|
||||
behaviorData.useLegacyTrajectory === undefined
|
||||
? true
|
||||
: behaviorData.useLegacyTrajectory;
|
||||
this._useRepeatedJump =
|
||||
behaviorData.useRepeatedJump === undefined
|
||||
? true
|
||||
: behaviorData.useRepeatedJump;
|
||||
this._canGoDownFromJumpthru = behaviorData.canGoDownFromJumpthru;
|
||||
this._slopeMaxAngle = 0;
|
||||
this.setSlopeMaxAngle(behaviorData.slopeMaxAngle);
|
||||
@@ -249,6 +255,7 @@ namespace gdjs {
|
||||
juk: this._wasJumpKeyPressed,
|
||||
rpk: this._wasReleasePlatformKeyPressed,
|
||||
rlk: this._wasReleaseLadderKeyPressed,
|
||||
jkhsjs: this._jumpKeyHeldSinceJumpStart,
|
||||
sn: this._state.toString(),
|
||||
ssd: this._state.getNetworkSyncData(),
|
||||
},
|
||||
@@ -306,6 +313,9 @@ namespace gdjs {
|
||||
if (behaviorSpecificProps.rlk !== this._releaseLadderKey) {
|
||||
this._releaseLadderKey = behaviorSpecificProps.rlk;
|
||||
}
|
||||
if (behaviorSpecificProps.jkhsjs !== this._jumpKeyHeldSinceJumpStart) {
|
||||
this._jumpKeyHeldSinceJumpStart = behaviorSpecificProps.jkhsjs;
|
||||
}
|
||||
|
||||
if (behaviorSpecificProps.sn !== this._state.toString()) {
|
||||
switch (behaviorSpecificProps.sn) {
|
||||
@@ -427,6 +437,11 @@ namespace gdjs {
|
||||
(inputManager.isKeyPressed(LSHIFTKEY) ||
|
||||
inputManager.isKeyPressed(RSHIFTKEY) ||
|
||||
inputManager.isKeyPressed(SPACEKEY)));
|
||||
// Check if the jump key is continuously held since
|
||||
// the beginning of the jump.
|
||||
if (!this._jumpKey) {
|
||||
this._jumpKeyHeldSinceJumpStart = false;
|
||||
}
|
||||
|
||||
this._ladderKey ||
|
||||
(this._ladderKey =
|
||||
@@ -750,7 +765,11 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
_checkTransitionJumping() {
|
||||
if (this._canJump && this._jumpKey) {
|
||||
if (
|
||||
this._canJump &&
|
||||
this._jumpKey &&
|
||||
(!this._jumpKeyHeldSinceJumpStart || this._useRepeatedJump)
|
||||
) {
|
||||
this._setJumping();
|
||||
}
|
||||
}
|
||||
@@ -2270,7 +2289,6 @@ namespace gdjs {
|
||||
private _behavior: PlatformerObjectRuntimeBehavior;
|
||||
private _currentJumpSpeed: number = 0;
|
||||
private _timeSinceCurrentJumpStart: number = 0;
|
||||
private _jumpKeyHeldSinceJumpStart: boolean = false;
|
||||
private _jumpingFirstDelta: boolean = false;
|
||||
|
||||
constructor(behavior: PlatformerObjectRuntimeBehavior) {
|
||||
@@ -2288,7 +2306,7 @@ namespace gdjs {
|
||||
enter(from: State) {
|
||||
const behavior = this._behavior;
|
||||
this._timeSinceCurrentJumpStart = 0;
|
||||
this._jumpKeyHeldSinceJumpStart = true;
|
||||
behavior._jumpKeyHeldSinceJumpStart = true;
|
||||
|
||||
if (from !== behavior._jumping && from !== behavior._falling) {
|
||||
this._jumpingFirstDelta = true;
|
||||
@@ -2329,17 +2347,12 @@ namespace gdjs {
|
||||
beforeMovingY(timeDelta: float, oldX: float) {
|
||||
const behavior = this._behavior;
|
||||
|
||||
// Check if the jump key is continuously held since
|
||||
// the beginning of the jump.
|
||||
if (!behavior._jumpKey) {
|
||||
this._jumpKeyHeldSinceJumpStart = false;
|
||||
}
|
||||
this._timeSinceCurrentJumpStart += timeDelta;
|
||||
|
||||
const previousJumpSpeed = this._currentJumpSpeed;
|
||||
// Decrease jump speed after the (optional) jump sustain time is over.
|
||||
const sustainJumpSpeed =
|
||||
this._jumpKeyHeldSinceJumpStart &&
|
||||
behavior._jumpKeyHeldSinceJumpStart &&
|
||||
this._timeSinceCurrentJumpStart < behavior._jumpSustainTime;
|
||||
if (!sustainJumpSpeed) {
|
||||
this._currentJumpSpeed -= behavior._gravity * timeDelta;
|
||||
@@ -2374,7 +2387,6 @@ namespace gdjs {
|
||||
return {
|
||||
cjs: this._currentJumpSpeed,
|
||||
tscjs: this._timeSinceCurrentJumpStart,
|
||||
jkhsjs: this._jumpKeyHeldSinceJumpStart,
|
||||
jfd: this._jumpingFirstDelta,
|
||||
};
|
||||
}
|
||||
@@ -2382,7 +2394,6 @@ namespace gdjs {
|
||||
updateFromNetworkSyncData(data: JumpingStateNetworkSyncData) {
|
||||
this._currentJumpSpeed = data.cjs;
|
||||
this._timeSinceCurrentJumpStart = data.tscjs;
|
||||
this._jumpKeyHeldSinceJumpStart = data.jkhsjs;
|
||||
this._jumpingFirstDelta = data.jfd;
|
||||
}
|
||||
|
||||
|
@@ -27,6 +27,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: slopeMaxAngle,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -331,6 +332,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -491,6 +493,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -624,6 +627,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 0,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -728,6 +732,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
|
@@ -26,6 +26,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -172,6 +173,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -287,6 +289,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: true,
|
||||
useRepeatedJump: true,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -383,6 +386,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: true,
|
||||
useRepeatedJump: true,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -481,6 +485,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: true,
|
||||
useRepeatedJump: true,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -555,6 +560,8 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
let runtimeScene;
|
||||
let object;
|
||||
let platform;
|
||||
/** @type {gdjs.PlatformerObjectRuntimeBehavior} */
|
||||
let characterBehavior;
|
||||
|
||||
beforeEach(function () {
|
||||
runtimeScene = makePlatformerTestRuntimeScene();
|
||||
@@ -578,6 +585,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -585,6 +593,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
object.setCustomWidthAndHeight(10, 20);
|
||||
runtimeScene.addObject(object);
|
||||
object.setPosition(0, -32);
|
||||
characterBehavior = object.getBehavior('auto1');
|
||||
|
||||
// Put a platform.
|
||||
platform = addPlatformObject(runtimeScene);
|
||||
@@ -644,6 +653,48 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
expect(object.getY()).to.be(-30);
|
||||
});
|
||||
|
||||
it('can only jump once while the jump key is held', function () {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
|
||||
//Check the object is on the platform
|
||||
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
|
||||
expect(characterBehavior.isFalling()).to.be(false);
|
||||
expect(characterBehavior.isFallingWithoutJumping()).to.be(false);
|
||||
expect(characterBehavior.isMoving()).to.be(false);
|
||||
|
||||
// The character jumps a first time.
|
||||
for (let i = 0; i < 80; ++i) {
|
||||
characterBehavior.simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
characterBehavior.isJumping(true);
|
||||
}
|
||||
// The character lands back on the floor
|
||||
// while the player holds the jump key.
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
characterBehavior.simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
characterBehavior.isOnFloor(true);
|
||||
expect(object.getY()).to.be(-30);
|
||||
|
||||
// The character doesn't jump a 2nd time.
|
||||
characterBehavior.simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
characterBehavior.isOnFloor(true);
|
||||
|
||||
// The player release the jump key.
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
characterBehavior.isOnFloor(true);
|
||||
|
||||
// The character can now jump again.
|
||||
characterBehavior.simulateJumpKey();
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
characterBehavior.isJumping(true);
|
||||
});
|
||||
|
||||
it('can jump, and only sustain the jump while key held', function () {
|
||||
// Ensure the object falls on the platform
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
@@ -1126,6 +1177,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
canGoDownFromJumpthru: true,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -1467,6 +1519,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -1497,6 +1550,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
{
|
||||
type: 'PlatformBehavior::PlatformBehavior',
|
||||
@@ -1637,6 +1691,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -1758,6 +1813,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -1843,6 +1899,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -1921,6 +1978,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
|
@@ -26,6 +26,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
canGrabWithoutMoving: canGrabWithoutMoving,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -233,6 +234,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
canGrabWithoutMoving: true,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -398,6 +400,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
|
@@ -32,6 +32,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -308,6 +309,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
|
@@ -27,6 +27,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -438,6 +439,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
@@ -546,6 +548,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
|
||||
slopeMaxAngle: slopeMaxAngle,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
|
@@ -36,6 +36,7 @@ describe(`gdjs.PlatformerObjectRuntimeBehavior.findHighestFloorAndMoveOnTop`, fu
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
],
|
||||
effects: [],
|
||||
|
@@ -311,7 +311,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
"such as \"Path line\" (in the Advanced category) can be "
|
||||
"used to draw. Be sure to use \"End fill path\" action when "
|
||||
"you're done drawing the shape."),
|
||||
_("Begins drawing filling of an advanced path "
|
||||
_("Begin drawing filling of an advanced path "
|
||||
"with _PARAM0_ (start: _PARAM1_;_PARAM2_)"),
|
||||
_("Advanced"),
|
||||
"res/actions/beginFillPath24.png",
|
||||
|
@@ -34,6 +34,7 @@ void TopDownMovementBehavior::InitializeContent(
|
||||
behaviorContent.SetAttribute("viewpoint", "TopDown");
|
||||
behaviorContent.SetAttribute("customIsometryAngle", 30);
|
||||
behaviorContent.SetAttribute("movementAngleOffset", 0);
|
||||
behaviorContent.SetAttribute("useLegacyTurnBack", false);
|
||||
}
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor>
|
||||
@@ -89,10 +90,20 @@ TopDownMovementBehavior::GetProperties(
|
||||
.SetValue(
|
||||
gd::String::From(behaviorContent.GetDoubleAttribute("angleOffset")));
|
||||
properties["IgnoreDefaultControls"]
|
||||
.SetLabel(_("Default controls"))
|
||||
.SetLabel(_("Disable default keyboard controls"))
|
||||
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden)
|
||||
.SetValue(behaviorContent.GetBoolAttribute("ignoreDefaultControls")
|
||||
? "false"
|
||||
: "true")
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
properties["UseLegacyTurnBack"]
|
||||
.SetLabel(_("Only use acceleration to turn back "
|
||||
"(deprecated — best left unchecked)"))
|
||||
.SetGroup(_("Deprecated options"))
|
||||
.SetDeprecated()
|
||||
.SetValue(behaviorContent.GetBoolAttribute("useLegacyTurnBack", true)
|
||||
? "true"
|
||||
: "false")
|
||||
.SetType("Boolean");
|
||||
|
||||
gd::String viewpoint = behaviorContent.GetStringAttribute("viewpoint");
|
||||
@@ -145,7 +156,7 @@ bool TopDownMovementBehavior::UpdateProperty(
|
||||
const gd::String& name,
|
||||
const gd::String& value) {
|
||||
if (name == "IgnoreDefaultControls") {
|
||||
behaviorContent.SetAttribute("ignoreDefaultControls", (value == "0"));
|
||||
behaviorContent.SetAttribute("ignoreDefaultControls", (value == "1"));
|
||||
return true;
|
||||
}
|
||||
if (name == "AllowDiagonals") {
|
||||
@@ -156,6 +167,9 @@ bool TopDownMovementBehavior::UpdateProperty(
|
||||
behaviorContent.SetAttribute("rotateObject", (value != "0"));
|
||||
return true;
|
||||
}
|
||||
if (name == "UseLegacyTurnBack") {
|
||||
behaviorContent.SetAttribute("useLegacyTurnBack", (value == "1"));
|
||||
}
|
||||
if (name == "Viewpoint") {
|
||||
// Fix the offset angle when switching between top-down and isometry
|
||||
const gd::String& oldValue =
|
||||
|
@@ -37,6 +37,7 @@ namespace gdjs {
|
||||
private _angleOffset: float;
|
||||
private _ignoreDefaultControls: boolean;
|
||||
private _movementAngleOffset: float;
|
||||
private _useLegacyTurnBack: boolean;
|
||||
|
||||
/** The latest angle of movement, in degrees. */
|
||||
private _angle: float = 0;
|
||||
@@ -102,6 +103,10 @@ namespace gdjs {
|
||||
behaviorData.customIsometryAngle
|
||||
);
|
||||
this._movementAngleOffset = behaviorData.movementAngleOffset || 0;
|
||||
this._useLegacyTurnBack =
|
||||
behaviorData.useLegacyTurnBack === undefined
|
||||
? true
|
||||
: behaviorData.useLegacyTurnBack;
|
||||
}
|
||||
|
||||
getNetworkSyncData(): TopDownMovementNetworkSyncData {
|
||||
@@ -303,9 +308,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
getSpeed(): float {
|
||||
return Math.sqrt(
|
||||
this._xVelocity * this._xVelocity + this._yVelocity * this._yVelocity
|
||||
);
|
||||
return Math.hypot(this._xVelocity, this._yVelocity);
|
||||
}
|
||||
|
||||
getXVelocity(): float {
|
||||
@@ -468,74 +471,71 @@ namespace gdjs {
|
||||
// variables without assigning them a value.
|
||||
let directionInRad = 0;
|
||||
let directionInDeg = 0;
|
||||
let cos = 1;
|
||||
let sin = 0;
|
||||
|
||||
let isMoving = false;
|
||||
let targetedSpeed = 0;
|
||||
// Update the speed of the object:
|
||||
if (direction !== -1) {
|
||||
isMoving = true;
|
||||
directionInRad =
|
||||
((direction + this._movementAngleOffset / 45) * Math.PI) / 4.0;
|
||||
directionInDeg = direction * 45 + this._movementAngleOffset;
|
||||
// This makes the trigo resilient to rounding errors on directionInRad.
|
||||
cos = Math.cos(directionInRad);
|
||||
sin = Math.sin(directionInRad);
|
||||
if (cos === -1 || cos === 1) {
|
||||
sin = 0;
|
||||
}
|
||||
if (sin === -1 || sin === 1) {
|
||||
cos = 0;
|
||||
}
|
||||
this._xVelocity += this._acceleration * timeDelta * cos;
|
||||
this._yVelocity += this._acceleration * timeDelta * sin;
|
||||
targetedSpeed = this._maxSpeed;
|
||||
} else if (this._stickForce !== 0) {
|
||||
isMoving = true;
|
||||
if (!this._allowDiagonals) {
|
||||
this._stickAngle = 90 * Math.floor((this._stickAngle + 45) / 90);
|
||||
}
|
||||
directionInDeg = this._stickAngle + this._movementAngleOffset;
|
||||
directionInRad = (directionInDeg * Math.PI) / 180;
|
||||
const norm = this._acceleration * timeDelta * this._stickForce;
|
||||
// This makes the trigo resilient to rounding errors on directionInRad.
|
||||
cos = Math.cos(directionInRad);
|
||||
sin = Math.sin(directionInRad);
|
||||
if (cos === -1 || cos === 1) {
|
||||
sin = 0;
|
||||
}
|
||||
if (sin === -1 || sin === 1) {
|
||||
cos = 0;
|
||||
}
|
||||
this._xVelocity += norm * cos;
|
||||
this._yVelocity += norm * sin;
|
||||
targetedSpeed = this._maxSpeed * this._stickForce;
|
||||
|
||||
this._wasStickUsed = true;
|
||||
this._stickForce = 0;
|
||||
} else if (this._yVelocity !== 0 || this._xVelocity !== 0) {
|
||||
isMoving = true;
|
||||
directionInRad = Math.atan2(this._yVelocity, this._xVelocity);
|
||||
directionInDeg = (directionInRad * 180.0) / Math.PI;
|
||||
const xVelocityWasPositive = this._xVelocity >= 0;
|
||||
const yVelocityWasPositive = this._yVelocity >= 0;
|
||||
}
|
||||
if (isMoving) {
|
||||
// This makes the trigo resilient to rounding errors on directionInRad.
|
||||
cos = Math.cos(directionInRad);
|
||||
sin = Math.sin(directionInRad);
|
||||
let cos = Math.cos(directionInRad);
|
||||
let sin = Math.sin(directionInRad);
|
||||
if (cos === -1 || cos === 1) {
|
||||
sin = 0;
|
||||
}
|
||||
if (sin === -1 || sin === 1) {
|
||||
cos = 0;
|
||||
}
|
||||
this._xVelocity -= this._deceleration * timeDelta * cos;
|
||||
this._yVelocity -= this._deceleration * timeDelta * sin;
|
||||
if (this._xVelocity > 0 !== xVelocityWasPositive) {
|
||||
this._xVelocity = 0;
|
||||
}
|
||||
if (this._yVelocity > 0 !== yVelocityWasPositive) {
|
||||
this._yVelocity = 0;
|
||||
|
||||
const getAcceleratedSpeed = this._useLegacyTurnBack
|
||||
? TopDownMovementRuntimeBehavior.getLegacyAcceleratedSpeed
|
||||
: TopDownMovementRuntimeBehavior.getAcceleratedSpeed;
|
||||
|
||||
let currentSpeed = Math.hypot(this._xVelocity, this._yVelocity);
|
||||
const dotProduct = this._xVelocity * cos + this._yVelocity * sin;
|
||||
if (dotProduct < 0) {
|
||||
// The object is turning back.
|
||||
// Keep the negative velocity projected on the new direction.
|
||||
currentSpeed = dotProduct;
|
||||
}
|
||||
const speed = getAcceleratedSpeed(
|
||||
currentSpeed,
|
||||
targetedSpeed,
|
||||
this._maxSpeed,
|
||||
this._acceleration,
|
||||
this._deceleration,
|
||||
timeDelta
|
||||
);
|
||||
this._xVelocity = speed * cos;
|
||||
this._yVelocity = speed * sin;
|
||||
}
|
||||
|
||||
const squaredSpeed =
|
||||
this._xVelocity * this._xVelocity + this._yVelocity * this._yVelocity;
|
||||
if (squaredSpeed > this._maxSpeed * this._maxSpeed) {
|
||||
this._xVelocity = this._maxSpeed * cos;
|
||||
this._yVelocity = this._maxSpeed * sin;
|
||||
const ratio = this._maxSpeed / Math.sqrt(squaredSpeed);
|
||||
this._xVelocity *= ratio;
|
||||
this._yVelocity *= ratio;
|
||||
}
|
||||
|
||||
// No acceleration for angular speed for now.
|
||||
@@ -589,9 +589,125 @@ namespace gdjs {
|
||||
this._rightKey = false;
|
||||
this._upKey = false;
|
||||
this._downKey = false;
|
||||
this._stickForce = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static getAcceleratedSpeed(
|
||||
currentSpeed: float,
|
||||
targetedSpeed: float,
|
||||
speedMax: float,
|
||||
acceleration: float,
|
||||
deceleration: float,
|
||||
timeDelta: float
|
||||
): float {
|
||||
let newSpeed = currentSpeed;
|
||||
const turningBackAcceleration = Math.max(acceleration, deceleration);
|
||||
if (targetedSpeed < 0) {
|
||||
if (currentSpeed <= targetedSpeed) {
|
||||
// Reduce the speed to match the stick force.
|
||||
newSpeed = Math.min(
|
||||
targetedSpeed,
|
||||
currentSpeed + turningBackAcceleration * timeDelta
|
||||
);
|
||||
} else if (currentSpeed <= 0) {
|
||||
// Accelerate
|
||||
newSpeed -= Math.max(-speedMax, acceleration * timeDelta);
|
||||
} else {
|
||||
// Turn back at least as fast as it would stop.
|
||||
newSpeed = Math.max(
|
||||
targetedSpeed,
|
||||
currentSpeed - turningBackAcceleration * timeDelta
|
||||
);
|
||||
}
|
||||
} else if (targetedSpeed > 0) {
|
||||
if (currentSpeed >= targetedSpeed) {
|
||||
// Reduce the speed to match the stick force.
|
||||
newSpeed = Math.max(
|
||||
targetedSpeed,
|
||||
currentSpeed - turningBackAcceleration * timeDelta
|
||||
);
|
||||
} else if (currentSpeed >= 0) {
|
||||
// Accelerate
|
||||
newSpeed = Math.min(
|
||||
speedMax,
|
||||
currentSpeed + acceleration * timeDelta
|
||||
);
|
||||
} else {
|
||||
// Turn back at least as fast as it would stop.
|
||||
newSpeed = Math.min(
|
||||
targetedSpeed,
|
||||
currentSpeed + turningBackAcceleration * timeDelta
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Decelerate and stop.
|
||||
if (currentSpeed < 0) {
|
||||
newSpeed = Math.min(currentSpeed + deceleration * timeDelta, 0);
|
||||
}
|
||||
if (currentSpeed > 0) {
|
||||
newSpeed = Math.max(currentSpeed - deceleration * timeDelta, 0);
|
||||
}
|
||||
}
|
||||
return newSpeed;
|
||||
}
|
||||
|
||||
private static getLegacyAcceleratedSpeed(
|
||||
currentSpeed: float,
|
||||
targetedSpeed: float,
|
||||
speedMax: float,
|
||||
acceleration: float,
|
||||
deceleration: float,
|
||||
timeDelta: float
|
||||
): float {
|
||||
let newSpeed = currentSpeed;
|
||||
if (targetedSpeed < 0) {
|
||||
if (currentSpeed <= targetedSpeed) {
|
||||
// Reduce the speed to match the stick force.
|
||||
newSpeed = Math.min(
|
||||
targetedSpeed,
|
||||
currentSpeed + deceleration * timeDelta
|
||||
);
|
||||
} else if (currentSpeed <= 0) {
|
||||
// Accelerate
|
||||
newSpeed -= Math.max(-speedMax, acceleration * timeDelta);
|
||||
} else {
|
||||
newSpeed = Math.max(
|
||||
targetedSpeed,
|
||||
currentSpeed - deceleration * timeDelta
|
||||
);
|
||||
}
|
||||
} else if (targetedSpeed > 0) {
|
||||
if (currentSpeed >= targetedSpeed) {
|
||||
// Reduce the speed to match the stick force.
|
||||
newSpeed = Math.max(
|
||||
targetedSpeed,
|
||||
currentSpeed - deceleration * timeDelta
|
||||
);
|
||||
} else if (currentSpeed >= 0) {
|
||||
// Accelerate
|
||||
newSpeed = Math.min(
|
||||
speedMax,
|
||||
currentSpeed + acceleration * timeDelta
|
||||
);
|
||||
} else {
|
||||
newSpeed = Math.min(
|
||||
targetedSpeed,
|
||||
currentSpeed + deceleration * timeDelta
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Decelerate and stop.
|
||||
if (currentSpeed < 0) {
|
||||
newSpeed = Math.min(currentSpeed + deceleration * timeDelta, 0);
|
||||
}
|
||||
if (currentSpeed > 0) {
|
||||
newSpeed = Math.max(currentSpeed - deceleration * timeDelta, 0);
|
||||
}
|
||||
}
|
||||
return newSpeed;
|
||||
}
|
||||
|
||||
simulateControl(input: string) {
|
||||
if (input === 'Left') {
|
||||
this._leftKey = true;
|
||||
|
@@ -60,7 +60,7 @@ module.exports = {
|
||||
'Tween',
|
||||
_('Tweening'),
|
||||
_(
|
||||
'Animate object properties over time. This allows smooth transitions, animations or movement of objects to specified positions.'
|
||||
'Smoothly animate object properties over time — such as position, rotation scale, opacity, and more — as well as variables. Ideal for creating fluid transitions and UI animations. While you can use tweens to move objects, other behaviors (like platform, physics, ellipse movement...) or forces are often better suited for dynamic movement. Tween is best used for animating UI elements, static objects that need to move from one point to another, or other values like variables.'
|
||||
),
|
||||
'Matthias Meike, Florian Rival',
|
||||
'Open source (MIT License)'
|
||||
|
@@ -14,6 +14,7 @@ namespace gdjs {
|
||||
animatable?: SpriteAnimationData[];
|
||||
variant: string;
|
||||
childrenContent: { [objectName: string]: ObjectConfiguration & any };
|
||||
isInnerAreaFollowingParentSize: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -111,9 +112,19 @@ namespace gdjs {
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
// Legacy events-based objects don't have any instance in their default
|
||||
// variant since there wasn't a graphical editor at the time.
|
||||
// In this case, the editor doesn't allow to choose a variant, but a
|
||||
// variant may have stayed after a user rolled back the extension.
|
||||
// This variant must be ignored to match what the editor shows.
|
||||
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
|
||||
eventsBasedObjectData.defaultVariant.instances.length == 0;
|
||||
let usedVariantData: EventsBasedObjectVariantData =
|
||||
eventsBasedObjectData.defaultVariant;
|
||||
if (customObjectData.variant) {
|
||||
if (
|
||||
customObjectData.variant &&
|
||||
!isForcedToOverrideEventsBasedObjectChildrenConfiguration
|
||||
) {
|
||||
for (
|
||||
let variantIndex = 0;
|
||||
variantIndex < eventsBasedObjectData.variants.length;
|
||||
@@ -154,10 +165,12 @@ namespace gdjs {
|
||||
override reinitialize(objectData: ObjectData & CustomObjectConfiguration) {
|
||||
super.reinitialize(objectData);
|
||||
|
||||
this._initializeFromObjectData(objectData);
|
||||
this._reinitializeRenderer();
|
||||
this._initializeFromObjectData(objectData);
|
||||
|
||||
// The generated code calls the onCreated super implementation at the end.
|
||||
// When changing the variant, the instance is like a new instance.
|
||||
// We call again `onCreated` at the end, like done by the constructor
|
||||
// the first time it's created.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
@@ -172,6 +185,34 @@ namespace gdjs {
|
||||
newObjectData.animatable || []
|
||||
);
|
||||
}
|
||||
if (oldObjectData.variant !== newObjectData.variant) {
|
||||
const width = this.getWidth();
|
||||
const height = this.getHeight();
|
||||
const hasInnerAreaChanged =
|
||||
oldObjectData.isInnerAreaFollowingParentSize &&
|
||||
this._instanceContainer._initialInnerArea &&
|
||||
this._innerArea &&
|
||||
(this._instanceContainer._initialInnerArea.min[0] !==
|
||||
this._innerArea.min[0] ||
|
||||
this._instanceContainer._initialInnerArea.min[1] !==
|
||||
this._innerArea.min[1] ||
|
||||
this._instanceContainer._initialInnerArea.max[0] !==
|
||||
this._innerArea.max[0] ||
|
||||
this._instanceContainer._initialInnerArea.max[1] !==
|
||||
this._innerArea.max[1]);
|
||||
|
||||
this._reinitializeRenderer();
|
||||
this._initializeFromObjectData(newObjectData);
|
||||
|
||||
// The generated code calls the onCreated super implementation at the end.
|
||||
this.onCreated();
|
||||
|
||||
// Keep the custom size
|
||||
if (hasInnerAreaChanged) {
|
||||
this.setWidth(width);
|
||||
this.setHeight(height);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,6 @@ namespace gdjs {
|
||||
objectData: ObjectData & CustomObjectConfiguration
|
||||
) {
|
||||
super(parent, objectData);
|
||||
this.getRenderer().reinitialize(this, parent);
|
||||
}
|
||||
|
||||
protected override _createRender(): gdjs.CustomRuntimeObject2DRenderer {
|
||||
|
@@ -24,7 +24,7 @@ namespace gdjs {
|
||||
*
|
||||
* @see gdjs.CustomRuntimeObject._innerArea
|
||||
**/
|
||||
private _initialInnerArea: {
|
||||
_initialInnerArea: {
|
||||
min: [float, float, float];
|
||||
max: [float, float, float];
|
||||
} | null = null;
|
||||
@@ -47,6 +47,9 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
addLayer(layerData: LayerData) {
|
||||
if (this._layers.containsKey(layerData.name)) {
|
||||
return;
|
||||
}
|
||||
const layer = new gdjs.RuntimeCustomObjectLayer(layerData, this);
|
||||
this._layers.put(layerData.name, layer);
|
||||
this._orderedLayers.push(layer);
|
||||
@@ -71,6 +74,10 @@ namespace gdjs {
|
||||
this.onDestroyFromScene(this._parent);
|
||||
}
|
||||
|
||||
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
|
||||
!eventsBasedObjectVariantData.name &&
|
||||
eventsBasedObjectVariantData.instances.length == 0;
|
||||
|
||||
this._setOriginalInnerArea(eventsBasedObjectVariantData);
|
||||
|
||||
// Registering objects
|
||||
@@ -83,7 +90,8 @@ namespace gdjs {
|
||||
// The children configuration override only applies to the default variant.
|
||||
if (
|
||||
customObjectData.childrenContent &&
|
||||
!eventsBasedObjectVariantData.name
|
||||
(!eventsBasedObjectVariantData.name ||
|
||||
isForcedToOverrideEventsBasedObjectChildrenConfiguration)
|
||||
) {
|
||||
this.registerObject({
|
||||
...childObjectData,
|
||||
|
@@ -811,6 +811,8 @@ namespace gdjs {
|
||||
this._objectsCtor = new Hashtable();
|
||||
this._allInstancesList = [];
|
||||
this._instancesRemoved = [];
|
||||
this._layersCameraCoordinates = {};
|
||||
this._initialBehaviorSharedData = new Hashtable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -222,7 +222,9 @@ namespace gdjs {
|
||||
kind: 'fatal',
|
||||
message:
|
||||
'Unexpected error happened while hot-reloading: ' +
|
||||
error.message,
|
||||
error.message +
|
||||
'\n' +
|
||||
error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -472,13 +474,24 @@ namespace gdjs {
|
||||
newExternalLayoutData.associatedLayout
|
||||
);
|
||||
|
||||
const oldObjectDataList =
|
||||
HotReloader.resolveCustomObjectConfigurations(
|
||||
oldProjectData,
|
||||
oldLayoutData ? oldLayoutData.objects : []
|
||||
);
|
||||
const newObjectDataList =
|
||||
HotReloader.resolveCustomObjectConfigurations(
|
||||
newProjectData,
|
||||
newLayoutData ? newLayoutData.objects : []
|
||||
);
|
||||
|
||||
sceneStack._stack.forEach((runtimeScene) => {
|
||||
this._hotReloadRuntimeSceneInstances(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
oldLayoutData ? oldLayoutData.objects : [],
|
||||
newLayoutData ? newLayoutData.objects : [],
|
||||
oldObjectDataList,
|
||||
newObjectDataList,
|
||||
oldExternalLayoutData.instances,
|
||||
newExternalLayoutData.instances,
|
||||
runtimeScene
|
||||
|
@@ -397,60 +397,68 @@ namespace gdjs {
|
||||
document.addEventListener(
|
||||
'pause',
|
||||
function () {
|
||||
const soundList = that._freeSounds.concat(that._freeMusics);
|
||||
for (let key in that._sounds) {
|
||||
if (that._sounds.hasOwnProperty(key)) {
|
||||
soundList.push(that._sounds[key]);
|
||||
}
|
||||
}
|
||||
for (let key in that._musics) {
|
||||
if (that._musics.hasOwnProperty(key)) {
|
||||
soundList.push(that._musics[key]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < soundList.length; i++) {
|
||||
const sound = soundList[i];
|
||||
if (!sound.paused() && !sound.stopped()) {
|
||||
sound.pause();
|
||||
that._pausedSounds.push(sound);
|
||||
}
|
||||
}
|
||||
that._paused = true;
|
||||
that.pauseAllActiveSounds();
|
||||
},
|
||||
false
|
||||
);
|
||||
document.addEventListener(
|
||||
'resume',
|
||||
function () {
|
||||
try {
|
||||
for (let i = 0; i < that._pausedSounds.length; i++) {
|
||||
const sound = that._pausedSounds[i];
|
||||
if (!sound.stopped()) {
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error.message &&
|
||||
typeof error.message === 'string' &&
|
||||
error.message.startsWith('Maximum call stack size exceeded')
|
||||
) {
|
||||
console.warn(
|
||||
'An error occurred when resuming paused sounds while the game was in background:',
|
||||
error
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
that._pausedSounds.length = 0;
|
||||
that._paused = false;
|
||||
that.resumeAllActiveSounds();
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pauseAllActiveSounds(): void {
|
||||
const soundList = this._freeSounds.concat(this._freeMusics);
|
||||
for (let key in this._sounds) {
|
||||
if (this._sounds.hasOwnProperty(key)) {
|
||||
soundList.push(this._sounds[key]);
|
||||
}
|
||||
}
|
||||
for (let key in this._musics) {
|
||||
if (this._musics.hasOwnProperty(key)) {
|
||||
soundList.push(this._musics[key]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < soundList.length; i++) {
|
||||
const sound = soundList[i];
|
||||
if (!sound.paused() && !sound.stopped()) {
|
||||
sound.pause();
|
||||
this._pausedSounds.push(sound);
|
||||
}
|
||||
}
|
||||
this._paused = true;
|
||||
}
|
||||
|
||||
resumeAllActiveSounds(): void {
|
||||
try {
|
||||
for (let i = 0; i < this._pausedSounds.length; i++) {
|
||||
const sound = this._pausedSounds[i];
|
||||
if (!sound.stopped()) {
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error.message &&
|
||||
typeof error.message === 'string' &&
|
||||
error.message.startsWith('Maximum call stack size exceeded')
|
||||
) {
|
||||
console.warn(
|
||||
'An error occurred when resuming paused sounds while the game was in background:',
|
||||
error
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
this._pausedSounds.length = 0;
|
||||
this._paused = false;
|
||||
}
|
||||
|
||||
getResourceKinds(): ResourceKind[] {
|
||||
return resourceKinds;
|
||||
}
|
||||
|
@@ -50,12 +50,7 @@ namespace gdjs {
|
||||
) {
|
||||
this._object = object;
|
||||
this._isContainerDirty = true;
|
||||
const layer = parent.getLayer('');
|
||||
if (layer) {
|
||||
layer
|
||||
.getRenderer()
|
||||
.addRendererObject(this._pixiContainer, object.getZOrder());
|
||||
}
|
||||
this._pixiContainer.removeChildren();
|
||||
}
|
||||
|
||||
getRendererObject() {
|
||||
|
2
GDJS/Runtime/types/project-data.d.ts
vendored
2
GDJS/Runtime/types/project-data.d.ts
vendored
@@ -229,7 +229,7 @@ declare interface EventsBasedObjectVariantData extends InstanceContainerData {
|
||||
/**
|
||||
* A value shared by every object instances.
|
||||
*
|
||||
* @see gdjs.CustomRuntimeObjectInstanceContainer._originalInnerArea
|
||||
* @see gdjs.CustomRuntimeObjectInstanceContainer._initialInnerArea
|
||||
**/
|
||||
_initialInnerArea: {
|
||||
min: [float, float, float];
|
||||
|
@@ -12,9 +12,8 @@ npm install
|
||||
Then launch tests:
|
||||
|
||||
```bash
|
||||
npm run test:watch # This will use Chrome Headless
|
||||
npm run test:watch # Runs tests in a headless browser using Vitest
|
||||
npm run test-benchmark:watch # This will also run benchmarks
|
||||
npm run test:firefox:watch # To run tests using Firefox
|
||||
```
|
||||
|
||||
> ⚠️ If you're working on GDJS or extensions, make sure to have the development version of GDevelop running so that changes in GDJS or extension files are rebuilt (or run `npm run build` in `GDJS/`, but better run GDevelop so that any changes are watched).
|
||||
@@ -23,7 +22,7 @@ npm run test:firefox:watch # To run tests using Firefox
|
||||
|
||||
### Unit tests
|
||||
|
||||
Tests are launched using Chrome. You need Chrome installed to run them. You can change the browser by modifying the package.json "test" command and install the appropriate karma package.
|
||||
Tests are launched using [Vitest](https://vitest.dev/) in a headless browser. You can change the browser by editing the `vitest.config.js` file.
|
||||
|
||||
Tests are located in the **tests** folder for the game engine, or directly in the folder of the tested extensions.
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
describe('gdjs.Force', function() {
|
||||
it('benchmark setting angle and length', function(){
|
||||
this.timeout(20000);
|
||||
it('benchmark setting angle and length', function(){
|
||||
vi.setTimeout(20000);
|
||||
var layer = new gdjs.Force();
|
||||
|
||||
const benchmarkSuite = makeBenchmarkSuite();
|
||||
|
@@ -5,7 +5,7 @@ describe('gdjs.Layer', function() {
|
||||
var runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
|
||||
it('benchmark convertCoords and convertInverseCoords', function() {
|
||||
this.timeout(30000);
|
||||
vi.setTimeout(30000);
|
||||
var layer = new gdjs.Layer(
|
||||
{ name: 'My layer',
|
||||
visibility: true,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
describe('gdjs.Polygon', function() {
|
||||
it('benchmark gdjs.Polygon.collisionTest between two polygons', function() {
|
||||
this.timeout(20000);
|
||||
vi.setTimeout(20000);
|
||||
var rect1 = gdjs.Polygon.createRectangle(32, 40);
|
||||
var rect2 = gdjs.Polygon.createRectangle(32, 40);
|
||||
var rect3 = gdjs.Polygon.createRectangle(32, 40);
|
||||
|
@@ -1,8 +1,8 @@
|
||||
describe('gdjs.RuntimeObject', function() {
|
||||
const runtimeScene = new gdjs.RuntimeScene(null);
|
||||
|
||||
it('benchmark getAABB of rotated vs non rotated objects', function(){
|
||||
this.timeout(20000);
|
||||
it('benchmark getAABB of rotated vs non rotated objects', function(){
|
||||
vi.setTimeout(20000);
|
||||
var object = new gdjs.RuntimeObject(runtimeScene, {name: "obj1", type: "", behaviors: [], effects: []});
|
||||
object.getWidth = function() { return 10; };
|
||||
object.getHeight = function() { return 20; };
|
||||
@@ -25,8 +25,8 @@ describe('gdjs.RuntimeObject', function() {
|
||||
console.log(benchmarkSuite.run());
|
||||
});
|
||||
|
||||
it('benchmark getAABB of rotated vs non rotated objects, with non default center', function(){
|
||||
this.timeout(20000);
|
||||
it('benchmark getAABB of rotated vs non rotated objects, with non default center', function(){
|
||||
vi.setTimeout(20000);
|
||||
var object = new gdjs.RuntimeObject(runtimeScene, {name: "obj1", type: "", behaviors: [], effects: []});
|
||||
object.getWidth = function() { return 10; };
|
||||
object.getHeight = function() { return 20; };
|
||||
|
@@ -65,7 +65,7 @@ describe('gdjs.SpriteRuntimeObject', function () {
|
||||
});
|
||||
|
||||
it('benchmark getAABB of rotated vs non rotated sprite, with custom hitboxes, origin and center', function () {
|
||||
this.timeout(20000);
|
||||
vi.setTimeout(20000);
|
||||
const object = makeSpriteRuntimeObjectWithCustomHitBox(runtimeScene);
|
||||
|
||||
const benchmarkSuite = makeBenchmarkSuite({
|
||||
|
@@ -1,6 +1,6 @@
|
||||
describe('gdjs.VariablesContainer', function() {
|
||||
it('benchmark get', function() {
|
||||
this.timeout(20000);
|
||||
vi.setTimeout(20000);
|
||||
var container = new gdjs.VariablesContainer();
|
||||
|
||||
const benchmarkSuite = makeBenchmarkSuite();
|
||||
|
@@ -5,16 +5,10 @@
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "karma start --browsers ChromeHeadless --single-run",
|
||||
"test:watch": "karma start --browsers ChromeHeadless",
|
||||
"test-benchmark": "karma start --browsers ChromeHeadless --single-run --enableBenchmarks",
|
||||
"test-benchmark:watch": "karma start --browsers ChromeHeadless --enableBenchmarks",
|
||||
"test:firefox": "karma start --browsers Firefox --single-run",
|
||||
"test:firefox:watch": "karma start --browsers Firefox",
|
||||
"test:chrome": "karma start --browsers Chrome --single-run",
|
||||
"test:chrome:watch": "karma start --browsers Chrome",
|
||||
"test:edge": "karma start --browsers EdgeHeadless --single-run",
|
||||
"test:edge:watch": "karma start --browsers EdgeHeadless"
|
||||
"test": "vitest run --browser",
|
||||
"test:watch": "vitest --browser",
|
||||
"test-benchmark": "vitest run --browser",
|
||||
"test-benchmark:watch": "vitest --browser"
|
||||
},
|
||||
"keywords": [
|
||||
"HTML5",
|
||||
@@ -30,12 +24,9 @@
|
||||
"mocha": "^1.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chiragrupani/karma-chromium-edge-launcher": "2.1.1",
|
||||
"karma": "^1.7.1",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-firefox-launcher": "^1.1.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-sinon": "^1.0.5",
|
||||
"vitest": "^0.34.0",
|
||||
"vite": "^4.4.9",
|
||||
"@vitest/browser": "^0.34.0",
|
||||
"sinon": "^15.0.1"
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ describe('gdjs.CustomRuntimeObject', function () {
|
||||
name: 'MyCustomObject',
|
||||
type: 'MyExtension::MyEventsBasedObject',
|
||||
variant: '',
|
||||
isInnerAreaFollowingParentSize: false,
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
|
@@ -27,7 +27,7 @@
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
const asyncTasksManager = runtimeScene.getAsyncTasksManager();
|
||||
|
||||
this.beforeEach(() => asyncTasksManager.clearTasks());
|
||||
beforeEach(() => asyncTasksManager.clearTasks());
|
||||
|
||||
it('should call a resolved callback', function () {
|
||||
const cb = createMockCallback();
|
||||
|
@@ -100,6 +100,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => {
|
||||
effects: [],
|
||||
content: {},
|
||||
childrenContent: {},
|
||||
isInnerAreaFollowingParentSize: false,
|
||||
};
|
||||
|
||||
/** @type {LayerData} */
|
||||
|
19
GDJS/tests/vitest.config.js
Normal file
19
GDJS/tests/vitest.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
name: 'chrome',
|
||||
headless: true,
|
||||
},
|
||||
setupFiles: [
|
||||
'./vitest.setup.js',
|
||||
'./tests-utils/init.js',
|
||||
'./tests-utils/init.pixiruntimegamewithassets.js',
|
||||
'./tests-utils/init.pixiruntimegame.js',
|
||||
'./tests-utils/MockedCustomObject.js',
|
||||
],
|
||||
include: ['tests/**/*.js', 'benchmarks/**/*.js', '../Extensions/**/tests/**/*.js'],
|
||||
},
|
||||
});
|
9
GDJS/tests/vitest.setup.js
Normal file
9
GDJS/tests/vitest.setup.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
// Expose expect.js and sinon globally like Karma used to do.
|
||||
// Vitest provides its own expect, but tests rely on expect.js syntax.
|
||||
// eslint-disable-next-line no-undef
|
||||
global.expect = expect;
|
||||
// eslint-disable-next-line no-undef
|
||||
global.sinon = sinon;
|
@@ -1405,6 +1405,7 @@ interface InitialInstance {
|
||||
double GetCustomDepth();
|
||||
|
||||
[Ref] InitialInstance ResetPersistentUuid();
|
||||
[Const, Ref] DOMString GetPersistentUuid();
|
||||
|
||||
void UpdateCustomProperty(
|
||||
[Const] DOMString name,
|
||||
@@ -2293,6 +2294,8 @@ interface PlatformExtension {
|
||||
[Const, Value] DOMString STATIC_GetNamespaceSeparator();
|
||||
[Const, Value] DOMString STATIC_GetBehaviorFullType(
|
||||
[Const] DOMString extensionName, [Const] DOMString behaviorName);
|
||||
[Const, Value] DOMString STATIC_GetExtensionFromFullBehaviorType([Const] DOMString type);
|
||||
[Const, Value] DOMString STATIC_GetBehaviorNameFromFullBehaviorType([Const] DOMString type);
|
||||
[Const, Value] DOMString STATIC_GetObjectFullType(
|
||||
[Const] DOMString extensionName, [Const] DOMString objectName);
|
||||
[Const, Value] DOMString STATIC_GetExtensionFromFullObjectType([Const] DOMString type);
|
||||
@@ -2342,6 +2345,9 @@ interface BaseEvent {
|
||||
|
||||
void SerializeTo([Ref] SerializerElement element);
|
||||
void UnserializeFrom([Ref] Project project, [Const, Ref] SerializerElement element);
|
||||
|
||||
[Const, Ref] DOMString GetAiGeneratedEventId();
|
||||
void SetAiGeneratedEventId([Const] DOMString aiGeneratedEventId);
|
||||
};
|
||||
|
||||
interface StandardEvent {
|
||||
@@ -2805,6 +2811,7 @@ interface WholeProjectRefactorer {
|
||||
unsigned long STATIC_GetLayoutAndExternalLayoutLayerInstancesCount([Ref] Project project, [Ref] Layout layout, [Const] DOMString layerName);
|
||||
void STATIC_RenameLeaderboards([Ref] Project project, [Const, Ref] MapStringString leaderboardIdMap);
|
||||
[Value] SetString STATIC_FindAllLeaderboardIds([Ref] Project project);
|
||||
void STATIC_UpdateBehaviorsSharedData([Ref] Project project);
|
||||
};
|
||||
|
||||
interface ObjectTools {
|
||||
|
@@ -674,6 +674,7 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
GetLayoutAndExternalLayoutLayerInstancesCount
|
||||
#define STATIC_RenameLeaderboards RenameLeaderboards
|
||||
#define STATIC_FindAllLeaderboardIds FindAllLeaderboardIds
|
||||
#define STATIC_UpdateBehaviorsSharedData UpdateBehaviorsSharedData
|
||||
|
||||
#define STATIC_GenerateBehaviorGetterAndSetter GenerateBehaviorGetterAndSetter
|
||||
#define STATIC_GenerateObjectGetterAndSetter GenerateObjectGetterAndSetter
|
||||
@@ -715,6 +716,8 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
|
||||
#define STATIC_GetNamespaceSeparator GetNamespaceSeparator
|
||||
#define STATIC_GetBehaviorFullType GetBehaviorFullType
|
||||
#define STATIC_GetExtensionFromFullBehaviorType GetExtensionFromFullBehaviorType
|
||||
#define STATIC_GetBehaviorNameFromFullBehaviorType GetBehaviorNameFromFullBehaviorType
|
||||
#define STATIC_GetObjectFullType GetObjectFullType
|
||||
#define STATIC_GetExtensionFromFullObjectType GetExtensionFromFullObjectType
|
||||
#define STATIC_GetObjectNameFromFullObjectType GetObjectNameFromFullObjectType
|
||||
|
@@ -140,6 +140,9 @@ describe('libGD.js', function () {
|
||||
expect(gd.Project.getSafeName('官话 name')).toBe('官话_name');
|
||||
expect(gd.Project.getSafeName('')).toBe('Unnamed');
|
||||
expect(gd.Project.getSafeName('9')).toBe('_9');
|
||||
expect(gd.Project.getSafeName('ExplosionParticles3D')).toBe(
|
||||
'ExplosionParticles3D'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have a list of extensions', function () {
|
||||
|
6
GDevelop.js/types.d.ts
vendored
6
GDevelop.js/types.d.ts
vendored
@@ -1167,6 +1167,7 @@ export class InitialInstance extends EmscriptenObject {
|
||||
setCustomDepth(depth: number): void;
|
||||
getCustomDepth(): number;
|
||||
resetPersistentUuid(): InitialInstance;
|
||||
getPersistentUuid(): string;
|
||||
updateCustomProperty(name: string, value: string, globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): void;
|
||||
getCustomProperties(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): MapStringPropertyDescriptor;
|
||||
getRawDoubleProperty(name: string): number;
|
||||
@@ -1740,6 +1741,8 @@ export class PlatformExtension extends EmscriptenObject {
|
||||
getAllSourceFiles(): VectorSourceFileMetadata;
|
||||
static getNamespaceSeparator(): string;
|
||||
static getBehaviorFullType(extensionName: string, behaviorName: string): string;
|
||||
static getExtensionFromFullBehaviorType(type: string): string;
|
||||
static getBehaviorNameFromFullBehaviorType(type: string): string;
|
||||
static getObjectFullType(extensionName: string, objectName: string): string;
|
||||
static getExtensionFromFullObjectType(type: string): string;
|
||||
static getObjectNameFromFullObjectType(type: string): string;
|
||||
@@ -1780,6 +1783,8 @@ export class BaseEvent extends EmscriptenObject {
|
||||
setFolded(folded: boolean): void;
|
||||
serializeTo(element: SerializerElement): void;
|
||||
unserializeFrom(project: Project, element: SerializerElement): void;
|
||||
getAiGeneratedEventId(): string;
|
||||
setAiGeneratedEventId(aiGeneratedEventId: string): void;
|
||||
}
|
||||
|
||||
export class StandardEvent extends BaseEvent {
|
||||
@@ -1994,6 +1999,7 @@ export class WholeProjectRefactorer extends EmscriptenObject {
|
||||
static getLayoutAndExternalLayoutLayerInstancesCount(project: Project, layout: Layout, layerName: string): number;
|
||||
static renameLeaderboards(project: Project, leaderboardIdMap: MapStringString): void;
|
||||
static findAllLeaderboardIds(project: Project): SetString;
|
||||
static updateBehaviorsSharedData(project: Project): void;
|
||||
}
|
||||
|
||||
export class ObjectTools extends EmscriptenObject {
|
||||
|
@@ -17,6 +17,8 @@ declare class gdBaseEvent extends gdBaseEvent {
|
||||
setFolded(folded: boolean): void;
|
||||
serializeTo(element: gdSerializerElement): void;
|
||||
unserializeFrom(project: gdProject, element: gdSerializerElement): void;
|
||||
getAiGeneratedEventId(): string;
|
||||
setAiGeneratedEventId(aiGeneratedEventId: string): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -44,6 +44,7 @@ declare class gdInitialInstance {
|
||||
setCustomDepth(depth: number): void;
|
||||
getCustomDepth(): number;
|
||||
resetPersistentUuid(): gdInitialInstance;
|
||||
getPersistentUuid(): string;
|
||||
updateCustomProperty(name: string, value: string, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): void;
|
||||
getCustomProperties(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): gdMapStringPropertyDescriptor;
|
||||
getRawDoubleProperty(name: string): number;
|
||||
|
@@ -57,6 +57,8 @@ declare class gdPlatformExtension {
|
||||
getAllSourceFiles(): gdVectorSourceFileMetadata;
|
||||
static getNamespaceSeparator(): string;
|
||||
static getBehaviorFullType(extensionName: string, behaviorName: string): string;
|
||||
static getExtensionFromFullBehaviorType(type: string): string;
|
||||
static getBehaviorNameFromFullBehaviorType(type: string): string;
|
||||
static getObjectFullType(extensionName: string, objectName: string): string;
|
||||
static getExtensionFromFullObjectType(type: string): string;
|
||||
static getObjectNameFromFullObjectType(type: string): string;
|
||||
|
@@ -63,6 +63,7 @@ declare class gdWholeProjectRefactorer {
|
||||
static getLayoutAndExternalLayoutLayerInstancesCount(project: gdProject, layout: gdLayout, layerName: string): number;
|
||||
static renameLeaderboards(project: gdProject, leaderboardIdMap: gdMapStringString): void;
|
||||
static findAllLeaderboardIds(project: gdProject): gdSetString;
|
||||
static updateBehaviorsSharedData(project: gdProject): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
60
appveyor.yml
60
appveyor.yml
@@ -1,17 +1,15 @@
|
||||
# AppVeyor configuration to build GDevelop app running
|
||||
# Deprecated AppVeyor configuration to build GDevelop app running
|
||||
# on the Electron runtime (newIDE/electron-app) for Windows.
|
||||
# For macOS and Linux, see the config.yml file.
|
||||
#
|
||||
# This was replaced by build on CircleCI - but kept for redundancy/tests.
|
||||
# For Windows, macOS and Linux builds, see the config.yml file.
|
||||
|
||||
version: 1.0.{build}
|
||||
pull_requests:
|
||||
do_not_increment_build_number: true
|
||||
image: Visual Studio 2019
|
||||
clone_depth: 5
|
||||
# Only build
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /experimental-build.*/
|
||||
# Build must be triggered by the API.
|
||||
skip_tags: true # Don't rebuild on tags.
|
||||
init:
|
||||
- ps: Install-Product node 16
|
||||
@@ -21,13 +19,14 @@ cache:
|
||||
- newIDE\app\node_modules -> newIDE\app\package-lock.json
|
||||
- newIDE\electron-app\node_modules -> newIDE\electron-app\package-lock.json
|
||||
- GDevelop.js\node_modules -> GDevelop.js\package-lock.json
|
||||
- GDJS\node_modules -> GDJS\package-lock.json
|
||||
install:
|
||||
# Download and install SSL.com eSigner CKA.
|
||||
# Download and install SSL.com eSigner CKA.
|
||||
# See https://www.ssl.com/how-to/how-to-integrate-esigner-cka-with-ci-cd-tools-for-automated-code-signing/.
|
||||
#
|
||||
# This is necessary because of "signing to be FIPS-140 compliant". See
|
||||
# This is necessary because of "signing to be FIPS-140 compliant". See
|
||||
# https://github.com/electron-userland/electron-builder/issues/6158
|
||||
#
|
||||
#
|
||||
# Make sure to DISABLE "malware blocker" in SSL.com to avoid errors like:
|
||||
# Error information: "Error: SignerSign() failed." (-2146893821/0x80090003)
|
||||
- ps: >-
|
||||
@@ -42,13 +41,13 @@ install:
|
||||
Remove-Item eSigner_CKA_Setup.zip
|
||||
|
||||
Move-Item -Destination "eSigner_CKA_Installer.exe" -Path "eSigner_CKA_*\*.exe"
|
||||
|
||||
|
||||
# Install it. See https://www.ssl.com/how-to/how-to-integrate-esigner-cka-with-ci-cd-tools-for-automated-code-signing/
|
||||
|
||||
New-Item -ItemType Directory -Force -Path "C:\projects\gdevelop\eSignerCKA"
|
||||
|
||||
./eSigner_CKA_Installer.exe /CURRENTUSER /VERYSILENT /SUPPRESSMSGBOXES /DIR="C:\projects\gdevelop\eSignerCKA" | Out-Null
|
||||
|
||||
|
||||
# Disable logger.
|
||||
|
||||
# $LogConfig = Get-Content -Path C:\projects\gdevelop\eSignerCKA/log4net.config
|
||||
@@ -57,23 +56,6 @@ install:
|
||||
|
||||
# $LogConfig | Set-Content -Path C:\projects\gdevelop\eSignerCKA/log4net.config
|
||||
|
||||
# Build GDevelop.js (and run tests to ensure it works).
|
||||
# (in a subshell to avoid Emscripten polluting the Node.js and npm version for the rest of the build)
|
||||
- cmd: >-
|
||||
cd GDevelop.js
|
||||
|
||||
npm -v && npm install
|
||||
|
||||
git clone https://github.com/juj/emsdk.git
|
||||
|
||||
cd emsdk
|
||||
|
||||
emsdk install 3.1.21
|
||||
|
||||
CMD /C "emsdk activate 3.1.21 && cd .. && npm run build"
|
||||
|
||||
cd ..\..
|
||||
|
||||
# Build GDevelop IDE.
|
||||
# Also install setuptools as something requires distutils in electron-app, and it was removed in Python 3.12.
|
||||
# setuptools will make distutils available again (but we should migrate our packages probably).
|
||||
@@ -111,9 +93,8 @@ build_script:
|
||||
echo Certificate: $CodeSigningCert
|
||||
|
||||
# Use a custom signtool path because of the signtool.exe bundled withy electron-builder not working for some reason.
|
||||
# Can also be found in versioned folders like "C:/Program Files (x86)/Windows Kits/10/bin/10.0.22000.0/x86/signtool.exe".
|
||||
|
||||
$Env:SIGNTOOL_PATH = "C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe"
|
||||
$Env:SIGNTOOL_PATH = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x86\signtool.exe"
|
||||
|
||||
# Extract thumbprint and subject name of the certificate (will be passed to electron-builder).
|
||||
|
||||
@@ -142,12 +123,6 @@ build_script:
|
||||
# Run a few tests on Windows.
|
||||
test_script:
|
||||
- cmd: >-
|
||||
cd GDevelop.js
|
||||
|
||||
npm test
|
||||
|
||||
cd ..
|
||||
|
||||
cd newIDE\app
|
||||
|
||||
npm test
|
||||
@@ -159,8 +134,9 @@ artifacts:
|
||||
name: GDevelopWindows
|
||||
|
||||
# Upload artifacts (AWS) - configuration is stored on AppVeyor itself.
|
||||
deploy:
|
||||
- provider: Environment
|
||||
name: Amazon S3 releases
|
||||
- provider: Environment
|
||||
name: Amazon S3 latest releases
|
||||
# Disabled because done by CircleCI "build-windows" job.
|
||||
# deploy:
|
||||
# - provider: Environment
|
||||
# name: Amazon S3 releases
|
||||
# - provider: Environment
|
||||
# name: Amazon S3 latest releases
|
||||
|
@@ -18,5 +18,6 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
'@storybook/preset-create-react-app',
|
||||
'storybook-addon-mock',
|
||||
],
|
||||
};
|
||||
|
3412
newIDE/app/package-lock.json
generated
3412
newIDE/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,13 +10,13 @@
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@lingui/cli": "^2.7.3",
|
||||
"@lingui/macro": "^2.7.3",
|
||||
"@storybook/addon-essentials": "^7.4.0",
|
||||
"@storybook/addons": "^7.4.0",
|
||||
"@storybook/components": "^7.4.0",
|
||||
"@storybook/preset-create-react-app": "^7.4.0",
|
||||
"@storybook/react": "^7.4.0",
|
||||
"@storybook/react-webpack5": "^7.4.0",
|
||||
"@storybook/theming": "^7.4.0",
|
||||
"@storybook/addon-essentials": "7.4.6",
|
||||
"@storybook/addons": "7.4.6",
|
||||
"@storybook/components": "7.4.6",
|
||||
"@storybook/preset-create-react-app": "7.4.6",
|
||||
"@storybook/react": "7.4.6",
|
||||
"@storybook/react-webpack5": "7.4.6",
|
||||
"@storybook/theming": "7.4.6",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
@@ -33,7 +33,8 @@
|
||||
"recursive-copy": "^2.0.14",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"shelljs": "0.8.4",
|
||||
"storybook": "^7.4.0",
|
||||
"storybook": "7.4.6",
|
||||
"storybook-addon-mock": "4.3.0",
|
||||
"style-dictionary": "^2.10.2",
|
||||
"typescript": "^4.1.3",
|
||||
"webpack": "5.88.2",
|
||||
|
@@ -158,13 +158,18 @@ ${extension.getDescription()} ${generateReadMoreLink(extension.getHelpPath())}
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {String} */
|
||||
const generateBuiltInExtensionNote = ({ extension }) => {
|
||||
return `The ${extension.getFullName()} extension is always installed in all GDevelop projects: there is no need to add it from the Project Manager.\n\n`;
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateExtensionFooterText = ({ extension }) => {
|
||||
return {
|
||||
text:
|
||||
`
|
||||
---
|
||||
*This page is an auto-generated reference page about the **${extension.getFullName()}** feature of [GDevelop, the open-source, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
|
||||
`\n\n---\n\n` +
|
||||
generateBuiltInExtensionNote({ extension }) +
|
||||
`*This page is an auto-generated reference page about the **${extension.getFullName()}** feature of [GDevelop, the open-source, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
|
||||
' ' +
|
||||
'Learn more about [all GDevelop features here](/gdevelop5/all-features).',
|
||||
};
|
||||
|
@@ -63,6 +63,19 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) {
|
||||
var hash = (hashShellString.stdout || 'unknown-hash').trim();
|
||||
var branch = (branchShellString.stdout || 'unknown-branch').trim();
|
||||
|
||||
if (branch === 'HEAD') {
|
||||
// We're in detached HEAD. Try to read the branch from the CI environment variables.
|
||||
if (process.env.APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH) {
|
||||
branch = process.env.APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH;
|
||||
} else if (process.env.APPVEYOR_REPO_BRANCH) {
|
||||
branch = process.env.APPVEYOR_REPO_BRANCH;
|
||||
} else {
|
||||
shell.echo(
|
||||
`⚠️ Can't find the branch of the associated commit - if you're in detached HEAD, you need to be on a branch instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(
|
||||
downloadLibGdJs(
|
||||
`https://s3.amazonaws.com/gdevelop-gdevelop.js/${branch}/commit/${hash}`
|
||||
@@ -129,7 +142,20 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) {
|
||||
|
||||
// Try to download the latest libGD.js, fallback to previous or master ones
|
||||
// if not found (including different parents, for handling of merge commits).
|
||||
downloadCommitLibGdJs('HEAD').then(onLibGdJsDownloaded, () =>
|
||||
downloadCommitLibGdJs('HEAD').then(onLibGdJsDownloaded, () => {
|
||||
// Force the exact version of GDevelop.js to be downloaded for AppVeyor - because
|
||||
// this means we build the app and we don't want to risk mismatch (Core C++ not up to date
|
||||
// with the IDE JavaScript).
|
||||
if (process.env.APPVEYOR || process.env.REQUIRES_EXACT_LIBGD_JS_VERSION) {
|
||||
shell.echo(
|
||||
`❌ Can't download the exact required version of libGD.js - check it was built by CircleCI before running this CI.`
|
||||
);
|
||||
shell.echo(
|
||||
`ℹ️ See the pipeline on https://app.circleci.com/pipelines/github/4ian/GDevelop.`
|
||||
);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
downloadCommitLibGdJs('HEAD~1').then(onLibGdJsDownloaded, () =>
|
||||
downloadCommitLibGdJs('HEAD~2').then(onLibGdJsDownloaded, () =>
|
||||
downloadCommitLibGdJs('HEAD~3').then(onLibGdJsDownloaded, () =>
|
||||
@@ -150,6 +176,6 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) {
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@@ -7,30 +7,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes chat-bubble-appear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes content-appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
/* Start after the bubble container. */
|
||||
37% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes thinking-appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -54,18 +30,21 @@
|
||||
justify-content: center;
|
||||
|
||||
animation: new-chat-appear 0.5s;
|
||||
|
||||
margin-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.chatBubbleContainer {
|
||||
.aiRequestChatContainer {
|
||||
display: flex;
|
||||
animation: chat-bubble-appear 0.3s;
|
||||
min-width: 0; /** Prevents horizontal overflow */
|
||||
}
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
flex: 1 1 0%;
|
||||
min-height: 0px;
|
||||
|
||||
.chatBubbleContent {
|
||||
display: flex;
|
||||
animation: content-appear 0.8s;
|
||||
min-width: 0; /** Prevents horizontal overflow */
|
||||
margin-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.thinkingText {
|
158
newIDE/app/src/AiGeneration/AiRequestChat/AiRequestUtils.js
Normal file
158
newIDE/app/src/AiGeneration/AiRequestChat/AiRequestUtils.js
Normal file
@@ -0,0 +1,158 @@
|
||||
// @flow
|
||||
import {
|
||||
type AiRequest,
|
||||
type AiRequestMessageAssistantFunctionCall,
|
||||
type AiRequestFunctionCallOutput,
|
||||
} from '../../Utils/GDevelopServices/Generation';
|
||||
import { type EditorFunctionCallResult } from '../../EditorFunctions/EditorFunctionCallRunner';
|
||||
|
||||
export const getFunctionCallToFunctionCallOutputMap = ({
|
||||
aiRequest,
|
||||
}: {|
|
||||
aiRequest: AiRequest,
|
||||
|}): Map<
|
||||
AiRequestMessageAssistantFunctionCall,
|
||||
AiRequestFunctionCallOutput | null
|
||||
> => {
|
||||
// Maps each function call to its corresponding output (or null if no output)
|
||||
const functionCallsToOutputs = new Map<
|
||||
AiRequestMessageAssistantFunctionCall,
|
||||
AiRequestFunctionCallOutput | null
|
||||
>();
|
||||
|
||||
// Track function calls by their call_id to match with outputs
|
||||
const functionCallsByCallId = new Map<
|
||||
string,
|
||||
AiRequestMessageAssistantFunctionCall
|
||||
>();
|
||||
|
||||
// Process messages in a single loop
|
||||
for (let i = 0; i < aiRequest.output.length; i++) {
|
||||
const message = aiRequest.output[i];
|
||||
|
||||
if (message.type === 'message' && message.role === 'assistant') {
|
||||
// Process function calls in this message
|
||||
message.content.forEach(content => {
|
||||
if (content.type === 'function_call') {
|
||||
// Initialize with null output - will be updated if we find a matching output
|
||||
functionCallsToOutputs.set(content, null);
|
||||
|
||||
// Store function call by call_id for later matching
|
||||
functionCallsByCallId.set(content.call_id, content);
|
||||
}
|
||||
});
|
||||
} else if (message.type === 'function_call_output') {
|
||||
// Find the corresponding function calls with this call_id
|
||||
const functionCall = functionCallsByCallId.get(message.call_id);
|
||||
functionCallsByCallId.delete(message.call_id);
|
||||
|
||||
// Match with the most recent function call with this call_id
|
||||
if (functionCall) {
|
||||
functionCallsToOutputs.set(functionCall, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return functionCallsToOutputs;
|
||||
};
|
||||
|
||||
export const getFunctionCallsToProcess = ({
|
||||
aiRequest,
|
||||
editorFunctionCallResults,
|
||||
}: {|
|
||||
aiRequest: AiRequest,
|
||||
editorFunctionCallResults: Array<EditorFunctionCallResult> | null,
|
||||
|}): Array<AiRequestMessageAssistantFunctionCall> => {
|
||||
const functionCallsToProcess: AiRequestMessageAssistantFunctionCall[] = [];
|
||||
const appliedFunctionCallIds = new Set<string>();
|
||||
const alreadyProcessedFunctionCallIds = new Set<string>();
|
||||
|
||||
// Track already applied function calls
|
||||
(editorFunctionCallResults || []).forEach(functionCallOutput => {
|
||||
appliedFunctionCallIds.add(functionCallOutput.call_id);
|
||||
});
|
||||
|
||||
// Process from the end and collect function calls until we hit a message with no function calls
|
||||
let foundFunctionCall = false;
|
||||
|
||||
for (let i = aiRequest.output.length - 1; i >= 0; i--) {
|
||||
const message = aiRequest.output[i];
|
||||
|
||||
// Track already processed function call outputs
|
||||
if (message.type === 'function_call_output') {
|
||||
alreadyProcessedFunctionCallIds.add(message.call_id);
|
||||
}
|
||||
|
||||
// Collect function calls that need processing
|
||||
if (message.type === 'message' && message.role === 'assistant') {
|
||||
const functionCalls = message.content.filter(
|
||||
content => content.type === 'function_call'
|
||||
);
|
||||
|
||||
if (functionCalls.length > 0) {
|
||||
foundFunctionCall = true;
|
||||
|
||||
// Add new unique function calls that haven't been processed or applied
|
||||
for (let j = functionCalls.length - 1; j >= 0; j--) {
|
||||
const functionCall = functionCalls[j];
|
||||
if (functionCall.type !== 'function_call') continue;
|
||||
|
||||
if (
|
||||
!alreadyProcessedFunctionCallIds.has(functionCall.call_id) &&
|
||||
!appliedFunctionCallIds.has(functionCall.call_id)
|
||||
) {
|
||||
functionCallsToProcess.unshift(functionCall); // Add to beginning to preserve original order
|
||||
}
|
||||
}
|
||||
} else if (foundFunctionCall) {
|
||||
// If we've found function calls and now hit a message with no function calls, stop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return functionCallsToProcess;
|
||||
};
|
||||
|
||||
export const getFunctionCallOutputsFromEditorFunctionCallResults = (
|
||||
editorFunctionCallResults: Array<EditorFunctionCallResult> | null
|
||||
): {|
|
||||
hasUnfinishedResult: boolean,
|
||||
functionCallOutputs: Array<AiRequestFunctionCallOutput>,
|
||||
|} => {
|
||||
if (!editorFunctionCallResults)
|
||||
return { hasUnfinishedResult: false, functionCallOutputs: [] };
|
||||
|
||||
let hasUnfinishedResult = false;
|
||||
const functionCallOutputs = editorFunctionCallResults
|
||||
.map(functionCallOutput => {
|
||||
if (functionCallOutput.status === 'finished') {
|
||||
return {
|
||||
type: 'function_call_output',
|
||||
call_id: functionCallOutput.call_id,
|
||||
output: JSON.stringify({
|
||||
success: functionCallOutput.success,
|
||||
...functionCallOutput.output,
|
||||
}),
|
||||
};
|
||||
} else if (functionCallOutput.status === 'ignored') {
|
||||
return {
|
||||
type: 'function_call_output',
|
||||
call_id: functionCallOutput.call_id,
|
||||
output: JSON.stringify({
|
||||
ignored: true,
|
||||
message: 'This was marked as ignored by the user.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
hasUnfinishedResult = true;
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
functionCallOutputs,
|
||||
hasUnfinishedResult,
|
||||
};
|
||||
};
|
37
newIDE/app/src/AiGeneration/AiRequestChat/ChatBubble.js
Normal file
37
newIDE/app/src/AiGeneration/AiRequestChat/ChatBubble.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import classes from './ChatBubble.module.css';
|
||||
import Paper from '../../UI/Paper';
|
||||
|
||||
const styles = {
|
||||
chatBubble: {
|
||||
paddingTop: 5,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
};
|
||||
|
||||
type ChatBubbleProps = {|
|
||||
children: React.Node,
|
||||
feedbackButtons?: React.Node,
|
||||
role: 'assistant' | 'user',
|
||||
|};
|
||||
|
||||
export const ChatBubble = ({
|
||||
children,
|
||||
feedbackButtons,
|
||||
role,
|
||||
}: ChatBubbleProps) => {
|
||||
return (
|
||||
<div className={classes.chatBubbleContainer}>
|
||||
<Paper
|
||||
background={role === 'user' ? 'light' : 'medium'}
|
||||
style={styles.chatBubble}
|
||||
>
|
||||
<div className={classes.chatBubbleContent}>{children}</div>
|
||||
{feedbackButtons}
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -0,0 +1,35 @@
|
||||
@keyframes chat-bubble-appear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes content-appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
/* Start after the bubble container. */
|
||||
37% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chatBubbleContainer {
|
||||
display: flex;
|
||||
animation: chat-bubble-appear 0.3s;
|
||||
min-width: 0; /** Prevents horizontal overflow */
|
||||
}
|
||||
|
||||
.chatBubbleContent {
|
||||
display: flex;
|
||||
animation: content-appear 0.8s;
|
||||
min-width: 0; /** Prevents horizontal overflow */
|
||||
}
|
@@ -4,8 +4,8 @@ import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { I18n } from '@lingui/react';
|
||||
import classNames from 'classnames';
|
||||
import Window from '../../../Utils/Window';
|
||||
import { getHelpLink } from '../../../Utils/HelpLink';
|
||||
import Window from '../../Utils/Window';
|
||||
import { getHelpLink } from '../../Utils/HelpLink';
|
||||
import classes from './ChatMarkdownText.module.css';
|
||||
import {
|
||||
type ConceptMetadata,
|
||||
@@ -127,7 +127,7 @@ type Props = {|
|
||||
/**
|
||||
* Display a markdown text for a AI chat bubble.
|
||||
*/
|
||||
export const ChatMarkdownText = (props: Props) => {
|
||||
export const ChatMarkdownText = React.memo<Props>((props: Props) => {
|
||||
const { getConceptMetadataFromHref } = useGetConceptMetadata();
|
||||
const renderChatLink = React.useMemo(
|
||||
() => makeRenderChatLink({ getConceptMetadataFromHref }),
|
||||
@@ -169,4 +169,4 @@ export const ChatMarkdownText = (props: Props) => {
|
||||
});
|
||||
|
||||
return <span className={className}>{markdownElement}</span>;
|
||||
};
|
||||
});
|
258
newIDE/app/src/AiGeneration/AiRequestChat/ChatMessages.js
Normal file
258
newIDE/app/src/AiGeneration/AiRequestChat/ChatMessages.js
Normal file
@@ -0,0 +1,258 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { ChatBubble } from './ChatBubble';
|
||||
import { Line } from '../../UI/Grid';
|
||||
import { ChatMarkdownText } from './ChatMarkdownText';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import { getFunctionCallToFunctionCallOutputMap } from './AiRequestUtils';
|
||||
import { FunctionCallRow } from './FunctionCallRow';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import Like from '../../UI/CustomSvgIcons/Like';
|
||||
import Dislike from '../../UI/CustomSvgIcons/Dislike';
|
||||
import Copy from '../../UI/CustomSvgIcons/Copy';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
type AiRequest,
|
||||
type AiRequestMessageAssistantFunctionCall,
|
||||
} from '../../Utils/GDevelopServices/Generation';
|
||||
import {
|
||||
type EditorFunctionCallResult,
|
||||
type EditorCallbacks,
|
||||
} from '../../EditorFunctions';
|
||||
import classes from './ChatMessages.module.css';
|
||||
import { DislikeFeedbackDialog } from './DislikeFeedbackDialog';
|
||||
import LeftLoader from '../../UI/LeftLoader';
|
||||
import Text from '../../UI/Text';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
|
||||
type Props = {|
|
||||
aiRequest: AiRequest,
|
||||
onSendFeedback: (
|
||||
aiRequestId: string,
|
||||
messageIndex: number,
|
||||
feedback: 'like' | 'dislike',
|
||||
reason?: string,
|
||||
freeFormDetails?: string
|
||||
) => Promise<void>,
|
||||
editorFunctionCallResults: Array<EditorFunctionCallResult> | null,
|
||||
onProcessFunctionCalls: (
|
||||
functionCalls: Array<AiRequestMessageAssistantFunctionCall>,
|
||||
options: ?{|
|
||||
ignore?: boolean,
|
||||
|}
|
||||
) => Promise<void>,
|
||||
editorCallbacks: EditorCallbacks,
|
||||
project: gdProject | null,
|
||||
|};
|
||||
|
||||
export const ChatMessages = React.memo<Props>(function ChatMessages({
|
||||
aiRequest,
|
||||
onSendFeedback,
|
||||
editorFunctionCallResults,
|
||||
onProcessFunctionCalls,
|
||||
editorCallbacks,
|
||||
project,
|
||||
}: Props) {
|
||||
const theme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
const [messageFeedbacks, setMessageFeedbacks] = React.useState({});
|
||||
const [
|
||||
dislikeFeedbackDialogOpenedFor,
|
||||
setDislikeFeedbackDialogOpenedFor,
|
||||
] = React.useState(null);
|
||||
|
||||
const functionCallToFunctionCallOutput = aiRequest
|
||||
? getFunctionCallToFunctionCallOutputMap({
|
||||
aiRequest,
|
||||
})
|
||||
: new Map();
|
||||
|
||||
return (
|
||||
<>
|
||||
{aiRequest.output.flatMap((message, messageIndex) => {
|
||||
if (message.type === 'message' && message.role === 'user') {
|
||||
return [
|
||||
<Line key={messageIndex} justifyContent="flex-end">
|
||||
<ChatBubble role="user">
|
||||
<ChatMarkdownText
|
||||
source={message.content
|
||||
.map(messageContent => messageContent.text)
|
||||
.join('\n')}
|
||||
/>
|
||||
</ChatBubble>
|
||||
</Line>,
|
||||
];
|
||||
}
|
||||
if (message.type === 'message' && message.role === 'assistant') {
|
||||
return [
|
||||
...message.content
|
||||
.map((messageContent, messageContentIndex) => {
|
||||
const key = `messageIndex${messageIndex}-${messageContentIndex}`;
|
||||
if (messageContent.type === 'output_text') {
|
||||
const feedbackKey = `${messageIndex}-${messageContentIndex}`;
|
||||
const currentFeedback = messageFeedbacks[feedbackKey];
|
||||
|
||||
return (
|
||||
<Line key={key} justifyContent="flex-start">
|
||||
<ChatBubble
|
||||
role="assistant"
|
||||
feedbackButtons={
|
||||
<div className={classes.feedbackButtonsContainer}>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Copy`}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
messageContent.text
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Copy fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`This was helpful`}
|
||||
onClick={() => {
|
||||
setMessageFeedbacks({
|
||||
...messageFeedbacks,
|
||||
[feedbackKey]: 'like',
|
||||
});
|
||||
onSendFeedback(
|
||||
aiRequest.id,
|
||||
messageIndex,
|
||||
'like'
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Like
|
||||
fontSize="small"
|
||||
htmlColor={
|
||||
currentFeedback === 'like'
|
||||
? theme.message.valid
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`This needs improvement`}
|
||||
onClick={() => {
|
||||
setMessageFeedbacks({
|
||||
...messageFeedbacks,
|
||||
[feedbackKey]: 'dislike',
|
||||
});
|
||||
setDislikeFeedbackDialogOpenedFor({
|
||||
aiRequestId: aiRequest.id,
|
||||
messageIndex,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Dislike
|
||||
fontSize="small"
|
||||
htmlColor={
|
||||
currentFeedback === 'dislike'
|
||||
? theme.message.warning
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ChatMarkdownText source={messageContent.text} />
|
||||
</ChatBubble>
|
||||
</Line>
|
||||
);
|
||||
}
|
||||
if (messageContent.type === 'reasoning') {
|
||||
return (
|
||||
<Line key={key} justifyContent="flex-start">
|
||||
<ChatBubble role="assistant">
|
||||
<ChatMarkdownText
|
||||
source={messageContent.summary.text}
|
||||
/>
|
||||
</ChatBubble>
|
||||
</Line>
|
||||
);
|
||||
}
|
||||
if (messageContent.type === 'function_call') {
|
||||
const existingFunctionCallOutput = functionCallToFunctionCallOutput.get(
|
||||
messageContent
|
||||
);
|
||||
// If there is already an existing function call output,
|
||||
// there can't be an editor function call result.
|
||||
// Indeed, sometimes, two functions will
|
||||
// have the same call_id (because of the way some LLM APIs are implemented).
|
||||
// The editorFunctionCallResult always applies to the last function call,
|
||||
// which has no function call output associated to it yet.
|
||||
const editorFunctionCallResult =
|
||||
(!existingFunctionCallOutput &&
|
||||
editorFunctionCallResults &&
|
||||
editorFunctionCallResults.find(
|
||||
functionCallOutput =>
|
||||
functionCallOutput.call_id === messageContent.call_id
|
||||
)) ||
|
||||
null;
|
||||
return (
|
||||
<FunctionCallRow
|
||||
project={project}
|
||||
key={key}
|
||||
onProcessFunctionCalls={onProcessFunctionCalls}
|
||||
functionCall={messageContent}
|
||||
editorFunctionCallResult={editorFunctionCallResult}
|
||||
existingFunctionCallOutput={existingFunctionCallOutput}
|
||||
editorCallbacks={editorCallbacks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean),
|
||||
];
|
||||
}
|
||||
if (message.type === 'function_call_output') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
})}
|
||||
|
||||
{aiRequest.status === 'error' ? (
|
||||
<Line justifyContent="flex-start">
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
The AI encountered an error while handling your request - this was
|
||||
request was not counted in your AI usage. Try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</Line>
|
||||
) : aiRequest.status === 'working' ? (
|
||||
<Line justifyContent="flex-start">
|
||||
<div className={classes.thinkingText}>
|
||||
<LeftLoader isLoading>
|
||||
<Text noMargin displayInlineAsSpan>
|
||||
<Trans>Thinking about your request...</Trans>
|
||||
</Text>
|
||||
</LeftLoader>
|
||||
</div>
|
||||
</Line>
|
||||
) : null}
|
||||
{dislikeFeedbackDialogOpenedFor && (
|
||||
<DislikeFeedbackDialog
|
||||
mode={aiRequest.mode || 'chat'}
|
||||
open
|
||||
onClose={() => setDislikeFeedbackDialogOpenedFor(null)}
|
||||
onSendFeedback={(reason: string, freeFormDetails: string) => {
|
||||
onSendFeedback(
|
||||
dislikeFeedbackDialogOpenedFor.aiRequestId,
|
||||
dislikeFeedbackDialogOpenedFor.messageIndex,
|
||||
'dislike',
|
||||
reason,
|
||||
freeFormDetails
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
@keyframes thinking-appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
/* Start after the bubble container and when the bubble content is almost finished animating. */
|
||||
60% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0px);
|
||||
}
|
||||
}
|
||||
|
||||
.thinkingText {
|
||||
animation: thinking-appear 1s;
|
||||
}
|
||||
|
||||
.feedbackButtonsContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
@@ -1,22 +1,22 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Window from '../../../Utils/Window';
|
||||
import memoize from '../../../Utils/Memoize';
|
||||
import { useRefWithInit } from '../../../Utils/UseRefInitHook';
|
||||
import { getHelpLink } from '../../../Utils/HelpLink';
|
||||
import { ExtensionStoreContext } from '../../../AssetStore/ExtensionStore/ExtensionStoreContext';
|
||||
import { IconContainer } from '../../../UI/IconContainer';
|
||||
import Window from '../../Utils/Window';
|
||||
import memoize from '../../Utils/Memoize';
|
||||
import { useRefWithInit } from '../../Utils/UseRefInitHook';
|
||||
import { getHelpLink } from '../../Utils/HelpLink';
|
||||
import { ExtensionStoreContext } from '../../AssetStore/ExtensionStore/ExtensionStoreContext';
|
||||
import { IconContainer } from '../../UI/IconContainer';
|
||||
import {
|
||||
type ExtensionShortHeader,
|
||||
type EventsFunctionInsideExtensionShortHeader,
|
||||
} from '../../../Utils/GDevelopServices/Extension';
|
||||
import { mapFor } from '../../../Utils/MapFor';
|
||||
} from '../../Utils/GDevelopServices/Extension';
|
||||
import { mapFor } from '../../Utils/MapFor';
|
||||
import classes from './ChatMarkdownText.module.css';
|
||||
import classNames from 'classnames';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { MarkdownText } from '../../../UI/MarkdownText';
|
||||
import Text from '../../../UI/Text';
|
||||
import { MarkdownText } from '../../UI/MarkdownText';
|
||||
import Text from '../../UI/Text';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
@@ -0,0 +1,178 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
import Text from '../../UI/Text';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
|
||||
import Radio from '@material-ui/core/Radio';
|
||||
import RadioGroup from '@material-ui/core/RadioGroup';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import { CompactTextAreaField } from '../../UI/CompactTextAreaField';
|
||||
|
||||
type DislikeFeedbackDialogProps = {|
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onSendFeedback: (reason: string, freeFormDetails: string) => void,
|
||||
mode: 'chat' | 'agent',
|
||||
|};
|
||||
|
||||
export const DislikeFeedbackDialog = ({
|
||||
mode,
|
||||
open,
|
||||
onClose,
|
||||
onSendFeedback,
|
||||
}: DislikeFeedbackDialogProps) => {
|
||||
const [selectedReason, setSelectedReason] = React.useState<?string>(null);
|
||||
const [freeFormDetails, setFreeFormDetails] = React.useState<string>('');
|
||||
|
||||
const handleChange = (event: { target: { value: string } }) => {
|
||||
setSelectedReason(event.target.value);
|
||||
};
|
||||
|
||||
const handleSendFeedback = () => {
|
||||
if (selectedReason) {
|
||||
onSendFeedback(selectedReason, freeFormDetails);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Dialog
|
||||
title={
|
||||
mode === 'agent' ? (
|
||||
<Trans>What went wrong?</Trans>
|
||||
) : (
|
||||
<Trans>What could be improved?</Trans>
|
||||
)
|
||||
}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={<Trans>Cancel</Trans>}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
<DialogPrimaryButton
|
||||
key="send"
|
||||
primary
|
||||
label={<Trans>Send feedback</Trans>}
|
||||
onClick={handleSendFeedback}
|
||||
disabled={!selectedReason}
|
||||
/>,
|
||||
]}
|
||||
open={open}
|
||||
onRequestClose={onClose}
|
||||
maxWidth="sm"
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Text>
|
||||
{mode === 'agent' ? (
|
||||
<Trans>
|
||||
The AI agent is in beta. Help us make it better by telling us
|
||||
what went wrong:
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Help us improve by telling us what could be improved:
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<RadioGroup value={selectedReason || ''} onChange={handleChange}>
|
||||
{mode === 'agent' ? (
|
||||
<>
|
||||
<FormControlLabel
|
||||
value="not-as-good-as-it-could-be"
|
||||
control={<Radio color="secondary" />}
|
||||
label={
|
||||
<Trans>
|
||||
The result wasn't as good as it could have been
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="too-little-work"
|
||||
control={<Radio color="secondary" />}
|
||||
label={<Trans>It didn't do enough</Trans>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="does-not-work"
|
||||
control={<Radio color="secondary" />}
|
||||
label={<Trans>It didn't work at all</Trans>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="too-much-modified-or-broken"
|
||||
control={<Radio color="secondary" />}
|
||||
label={
|
||||
<Trans>Too many things were changed or broken</Trans>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormControlLabel
|
||||
value="not-in-my-language"
|
||||
control={<Radio color="secondary" />}
|
||||
label={<Trans>The answer is not in my language</Trans>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="non-existing-things"
|
||||
control={<Radio color="secondary" />}
|
||||
label={
|
||||
<Trans>
|
||||
Some things in the answer don't exist in GDevelop
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="not-as-good-as-it-could-be"
|
||||
control={<Radio color="secondary" />}
|
||||
label={
|
||||
<Trans>The answer is not as good as it could be</Trans>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="very-wrong-answer"
|
||||
control={<Radio color="secondary" />}
|
||||
label={<Trans>The answer is entirely wrong</Trans>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="out-of-scope"
|
||||
control={<Radio color="secondary" />}
|
||||
label={
|
||||
<Trans>The answer is out of scope for GDevelop</Trans>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="too-short"
|
||||
control={<Radio color="secondary" />}
|
||||
label={<Trans>The answer is too short</Trans>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="too-long"
|
||||
control={<Radio color="secondary" />}
|
||||
label={<Trans>The answer is too long</Trans>}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormControlLabel
|
||||
value="other"
|
||||
control={<Radio color="secondary" />}
|
||||
label={<Trans>Other reason</Trans>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
<CompactTextAreaField
|
||||
label={i18n._(t`More details (optional)`)}
|
||||
value={freeFormDetails}
|
||||
onChange={value => setFreeFormDetails(value)}
|
||||
rows={5}
|
||||
maxLength={10000}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
89
newIDE/app/src/AiGeneration/AiRequestChat/FeedbackBanner.js
Normal file
89
newIDE/app/src/AiGeneration/AiRequestChat/FeedbackBanner.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import Paper from '../../UI/Paper';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import Like from '../../UI/CustomSvgIcons/Like';
|
||||
import Dislike from '../../UI/CustomSvgIcons/Dislike';
|
||||
import Text from '../../UI/Text';
|
||||
import classes from './FeedbackBanner.module.css';
|
||||
import { DislikeFeedbackDialog } from './DislikeFeedbackDialog';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
|
||||
type Props = {
|
||||
onSendFeedback: (
|
||||
feedback: 'like' | 'dislike',
|
||||
reason?: string,
|
||||
freeFormDetails?: string
|
||||
) => void,
|
||||
};
|
||||
|
||||
export const FeedbackBanner = ({ onSendFeedback }: Props) => {
|
||||
const [currentFeedback, setCurrentFeedback] = React.useState<
|
||||
'like' | 'dislike' | null
|
||||
>(null);
|
||||
const theme = React.useContext(GDevelopThemeContext);
|
||||
const [
|
||||
dislikeFeedbackDialogOpened,
|
||||
setDislikeFeedbackDialogOpened,
|
||||
] = React.useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Line noMargin justifyContent="center">
|
||||
<Paper background="dark" variant="outlined">
|
||||
<Column expand>
|
||||
<div className={classes.textAndButtonsContainer}>
|
||||
<Text size="block-title" color="inherit">
|
||||
<Trans>Did it work?</Trans>
|
||||
</Text>
|
||||
<Line alignItems="center" noMargin neverShrink>
|
||||
<IconButton
|
||||
tooltip={t`This was helpful`}
|
||||
onClick={() => {
|
||||
setCurrentFeedback('like');
|
||||
onSendFeedback('like');
|
||||
}}
|
||||
color="inherit"
|
||||
>
|
||||
<Like
|
||||
htmlColor={
|
||||
currentFeedback === 'like' ? theme.message.valid : undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
tooltip={t`There was a problem`}
|
||||
onClick={() => {
|
||||
setDislikeFeedbackDialogOpened(true);
|
||||
}}
|
||||
color="inherit"
|
||||
>
|
||||
<Dislike
|
||||
htmlColor={
|
||||
currentFeedback === 'dislike'
|
||||
? theme.message.warning
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Line>
|
||||
</div>
|
||||
</Column>
|
||||
</Paper>
|
||||
{dislikeFeedbackDialogOpened && (
|
||||
<DislikeFeedbackDialog
|
||||
mode="agent"
|
||||
open={dislikeFeedbackDialogOpened}
|
||||
onClose={() => setDislikeFeedbackDialogOpened(false)}
|
||||
onSendFeedback={(reason: string, freeFormDetails: string) => {
|
||||
setDislikeFeedbackDialogOpened(false);
|
||||
onSendFeedback('dislike', reason, freeFormDetails);
|
||||
setCurrentFeedback('dislike');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
);
|
||||
};
|
@@ -0,0 +1,12 @@
|
||||
.textAndButtonsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding-left: 8px;
|
||||
padding-right: 2px;
|
||||
color: var(--theme-text-secondary-color);
|
||||
opacity: 0.9;
|
||||
min-width: 0;
|
||||
}
|
228
newIDE/app/src/AiGeneration/AiRequestChat/FunctionCallRow.js
Normal file
228
newIDE/app/src/AiGeneration/AiRequestChat/FunctionCallRow.js
Normal file
@@ -0,0 +1,228 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
type AiRequestMessageAssistantFunctionCall,
|
||||
type AiRequestFunctionCallOutput,
|
||||
} from '../../Utils/GDevelopServices/Generation';
|
||||
import { type EditorFunctionCallResult } from '../../EditorFunctions/EditorFunctionCallRunner';
|
||||
import CircularProgress from '../../UI/CircularProgress';
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
import Text from '../../UI/Text';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import FlatButtonWithSplitMenu from '../../UI/FlatButtonWithSplitMenu';
|
||||
import Check from '../../UI/CustomSvgIcons/Check';
|
||||
import Error from '../../UI/CustomSvgIcons/Error';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import classes from './FunctionCallRow.module.css';
|
||||
import {
|
||||
editorFunctions,
|
||||
type EditorFunction,
|
||||
type EditorCallbacks,
|
||||
} from '../../EditorFunctions';
|
||||
import Link from '../../UI/Link';
|
||||
import { LineStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight';
|
||||
import ChevronArrowBottom from '../../UI/CustomSvgIcons/ChevronArrowBottom';
|
||||
import Paper from '../../UI/Paper';
|
||||
import { Line, Column } from '../../UI/Grid';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject | null,
|
||||
functionCall: AiRequestMessageAssistantFunctionCall,
|
||||
editorFunctionCallResult: ?EditorFunctionCallResult,
|
||||
existingFunctionCallOutput: ?AiRequestFunctionCallOutput,
|
||||
onProcessFunctionCalls: (
|
||||
functionCalls: Array<AiRequestMessageAssistantFunctionCall>,
|
||||
options: ?{|
|
||||
ignore?: boolean,
|
||||
|}
|
||||
) => Promise<void>,
|
||||
editorCallbacks: EditorCallbacks,
|
||||
|};
|
||||
|
||||
export const FunctionCallRow = React.memo<Props>(function FunctionCallRow({
|
||||
project,
|
||||
functionCall,
|
||||
editorFunctionCallResult,
|
||||
existingFunctionCallOutput,
|
||||
onProcessFunctionCalls,
|
||||
editorCallbacks,
|
||||
}: Props) {
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
let existingParsedOutput;
|
||||
try {
|
||||
if (existingFunctionCallOutput) {
|
||||
// While this could be slightly expensive in a component to render, the component
|
||||
// is memoized, so this won't impact rendering of large chats.
|
||||
existingParsedOutput = JSON.parse(existingFunctionCallOutput.output);
|
||||
}
|
||||
} catch (error) {
|
||||
existingParsedOutput = null;
|
||||
}
|
||||
|
||||
const isIgnored =
|
||||
(!!editorFunctionCallResult &&
|
||||
editorFunctionCallResult.status === 'ignored') ||
|
||||
(existingParsedOutput && !!existingParsedOutput.ignored);
|
||||
const isFinished =
|
||||
!!existingFunctionCallOutput ||
|
||||
(!!editorFunctionCallResult &&
|
||||
editorFunctionCallResult.status === 'finished');
|
||||
const functionCallResultIsErrored =
|
||||
editorFunctionCallResult &&
|
||||
editorFunctionCallResult.status === 'finished' &&
|
||||
editorFunctionCallResult.success === false;
|
||||
const hasErrored =
|
||||
functionCallResultIsErrored ||
|
||||
(existingParsedOutput && existingParsedOutput.success === false);
|
||||
const isWorking =
|
||||
!isFinished &&
|
||||
!!editorFunctionCallResult &&
|
||||
editorFunctionCallResult.status === 'working';
|
||||
|
||||
const editorFunction: EditorFunction | null =
|
||||
editorFunctions[functionCall.name] || null;
|
||||
let text;
|
||||
let details;
|
||||
let hasDetailsToShow = false;
|
||||
if (!editorFunction) {
|
||||
text = (
|
||||
<Trans>
|
||||
The AI tried to use a function of the editor that is unknown.
|
||||
</Trans>
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
const result = editorFunction.renderForEditor({
|
||||
project,
|
||||
args: JSON.parse(functionCall.arguments),
|
||||
editorCallbacks,
|
||||
shouldShowDetails: showDetails,
|
||||
});
|
||||
|
||||
text = result.text;
|
||||
details = result.details;
|
||||
hasDetailsToShow = result.hasDetailsToShow;
|
||||
} catch (error) {
|
||||
console.error('Error rendering function call:', error);
|
||||
text = (
|
||||
<Trans>
|
||||
The editor was unable to display the operation ({functionCall.name})
|
||||
used by the AI.
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.functionCallContainer}>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Tooltip
|
||||
title={JSON.stringify(
|
||||
existingFunctionCallOutput || editorFunctionCallResult
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{hasErrored ? (
|
||||
<Error htmlColor={gdevelopTheme.message.error} />
|
||||
) : isIgnored ? (
|
||||
<Check htmlColor={gdevelopTheme.text.color.disabled} />
|
||||
) : isFinished ? (
|
||||
<Check htmlColor={gdevelopTheme.message.valid} />
|
||||
) : (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
value={100}
|
||||
variant={isWorking ? 'indeterminate' : 'determinate'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
expand
|
||||
noOverflowParent
|
||||
>
|
||||
<LineStackLayout noMargin alignItems="baseline">
|
||||
<Text>{text || 'Working...'}</Text>
|
||||
{hasDetailsToShow && (
|
||||
<Text size="body-small" color="secondary">
|
||||
<Link
|
||||
color="inherit"
|
||||
href={'#'}
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
<Trans>Details</Trans>
|
||||
{details ? (
|
||||
<ChevronArrowBottom
|
||||
fontSize="small"
|
||||
style={{
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ChevronArrowRight
|
||||
fontSize="small"
|
||||
style={{
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<LineStackLayout
|
||||
noMargin
|
||||
alignItems="baseline"
|
||||
justifyContent="flex-end"
|
||||
neverShrink
|
||||
>
|
||||
{!isFinished && !isWorking && (
|
||||
<FlatButtonWithSplitMenu
|
||||
primary
|
||||
style={{ flexShrink: 0 }}
|
||||
onClick={() => onProcessFunctionCalls([functionCall])}
|
||||
label={<Trans>Execute this action</Trans>}
|
||||
buildMenuTemplate={i18n => [
|
||||
{
|
||||
label: i18n._(t`Ignore this`),
|
||||
click: () => {
|
||||
onProcessFunctionCalls([functionCall], {
|
||||
ignore: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{functionCallResultIsErrored && (
|
||||
<RaisedButton
|
||||
color="primary"
|
||||
onClick={() => onProcessFunctionCalls([functionCall])}
|
||||
label={<Trans>Retry</Trans>}
|
||||
/>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
</LineStackLayout>
|
||||
{details && (
|
||||
<div className={classes.detailsPaperContainer}>
|
||||
<Paper background="medium" elevation={0} square variant="outlined">
|
||||
<Line expand>
|
||||
<Column expand>
|
||||
<Text noMargin color="secondary">
|
||||
{details}
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
</Paper>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@@ -0,0 +1,38 @@
|
||||
@keyframes function-call-appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
/* Start after the bubble container and when the bubble content is almost finished animating. */
|
||||
60% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0px);
|
||||
}
|
||||
}
|
||||
|
||||
.functionCallContainer {
|
||||
animation: function-call-appear 0.5s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@keyframes details-paper-appear {
|
||||
0% {
|
||||
transform: scaleY(0.9);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.detailsPaperContainer {
|
||||
transform-origin: top left;
|
||||
animation: details-paper-appear 0.1s;
|
||||
}
|
815
newIDE/app/src/AiGeneration/AiRequestChat/index.js
Normal file
815
newIDE/app/src/AiGeneration/AiRequestChat/index.js
Normal file
@@ -0,0 +1,815 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n as I18nType } from '@lingui/core';
|
||||
import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout';
|
||||
import Text from '../../UI/Text';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
type AiRequest,
|
||||
type AiRequestMessageAssistantFunctionCall,
|
||||
} from '../../Utils/GDevelopServices/Generation';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { CompactTextAreaFieldWithControls } from '../../UI/CompactTextAreaFieldWithControls';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Paper from '../../UI/Paper';
|
||||
import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import classes from './AiRequestChat.module.css';
|
||||
import RobotIcon from '../../ProjectCreation/RobotIcon';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import GetSubscriptionCard from '../../Profile/Subscription/GetSubscriptionCard';
|
||||
import {
|
||||
type Quota,
|
||||
type UsagePrice,
|
||||
} from '../../Utils/GDevelopServices/Usage';
|
||||
import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
|
||||
import Link from '../../UI/Link';
|
||||
import { getHelpLink } from '../../Utils/HelpLink';
|
||||
import Window from '../../Utils/Window';
|
||||
import { type EditorFunctionCallResult } from '../../EditorFunctions/EditorFunctionCallRunner';
|
||||
import { type EditorCallbacks } from '../../EditorFunctions';
|
||||
import { getFunctionCallsToProcess } from './AiRequestUtils';
|
||||
import CircularProgress from '../../UI/CircularProgress';
|
||||
import TwoStatesButton from '../../UI/TwoStatesButton';
|
||||
import Help from '../../UI/CustomSvgIcons/Help';
|
||||
import Hammer from '../../UI/CustomSvgIcons/Hammer';
|
||||
import { ChatMessages } from './ChatMessages';
|
||||
import Send from '../../UI/CustomSvgIcons/Send';
|
||||
import { FeedbackBanner } from './FeedbackBanner';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const TOO_MANY_USER_MESSAGES_WARNING_COUNT = 5;
|
||||
const TOO_MANY_USER_MESSAGES_ERROR_COUNT = 10;
|
||||
|
||||
const styles = {
|
||||
chatScrollView: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
paddingBottom: 8,
|
||||
paddingTop: 14,
|
||||
maskImage: 'linear-gradient(to bottom, transparent, black 14px)',
|
||||
maskSize: '100% 100%',
|
||||
maskRepeat: 'no-repeat',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
project: gdProject | null,
|
||||
i18n: I18nType,
|
||||
aiRequest: AiRequest | null,
|
||||
|
||||
isSending: boolean,
|
||||
onStartNewAiRequest: (options: {|
|
||||
userRequest: string,
|
||||
mode: 'chat' | 'agent',
|
||||
|}) => void,
|
||||
onSendMessage: (options: {|
|
||||
userMessage: string,
|
||||
|}) => Promise<void>,
|
||||
onSendFeedback: (
|
||||
aiRequestId: string,
|
||||
messageIndex: number,
|
||||
feedback: 'like' | 'dislike',
|
||||
reason?: string,
|
||||
freeFormDetails?: string
|
||||
) => Promise<void>,
|
||||
hasOpenedProject: boolean,
|
||||
isAutoProcessingFunctionCalls: boolean,
|
||||
setAutoProcessFunctionCalls: boolean => void,
|
||||
onStartNewChat: () => void,
|
||||
|
||||
onProcessFunctionCalls: (
|
||||
functionCalls: Array<AiRequestMessageAssistantFunctionCall>,
|
||||
options: ?{| ignore?: boolean |}
|
||||
) => Promise<void>,
|
||||
editorFunctionCallResults: Array<EditorFunctionCallResult> | null,
|
||||
editorCallbacks: EditorCallbacks,
|
||||
// Error that occurred while sending the last request.
|
||||
lastSendError: ?Error,
|
||||
|
||||
// Quota available for using the feature.
|
||||
quota: Quota | null,
|
||||
increaseQuotaOffering: 'subscribe' | 'upgrade' | 'none',
|
||||
price: UsagePrice | null,
|
||||
availableCredits: number,
|
||||
};
|
||||
|
||||
export type AiRequestChatInterface = {|
|
||||
resetUserInput: (aiRequestId: string | null) => void,
|
||||
|};
|
||||
|
||||
const getQuotaOrCreditsText = ({
|
||||
quota,
|
||||
increaseQuotaOffering,
|
||||
price,
|
||||
availableCredits,
|
||||
isMobile,
|
||||
}: {|
|
||||
quota: Quota | null,
|
||||
increaseQuotaOffering: 'subscribe' | 'upgrade' | 'none',
|
||||
price: UsagePrice | null,
|
||||
availableCredits: number,
|
||||
isMobile: boolean,
|
||||
|}) => {
|
||||
if (!quota) return null;
|
||||
|
||||
const quotaOrCreditsText = !quota.limitReached ? (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
{increaseQuotaOffering === 'subscribe' ? (
|
||||
<Trans>
|
||||
Get GDevelop premium to get more free requests every month.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
These are parts of your GDevelop premium membership ({quota.max}{' '}
|
||||
free requests per month).
|
||||
</Trans>
|
||||
)}{' '}
|
||||
<Trans>Free requests do not consume credits on your account.</Trans>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{isMobile ? (
|
||||
<Trans>{quota.max - quota.current} free requests left</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{quota.max - quota.current} of {quota.max} free requests left this
|
||||
month
|
||||
</Trans>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Trans>{Math.max(0, availableCredits)} credits available</Trans>
|
||||
);
|
||||
|
||||
return quotaOrCreditsText;
|
||||
};
|
||||
|
||||
const getPriceText = ({
|
||||
aiRequestMode,
|
||||
price,
|
||||
lastUserMessagePriceInCredits,
|
||||
}: {|
|
||||
aiRequestMode: 'chat' | 'agent',
|
||||
price: UsagePrice | null,
|
||||
lastUserMessagePriceInCredits: number | null,
|
||||
|}) => {
|
||||
if (!price) return null;
|
||||
|
||||
const priceInCredits = price.priceInCredits;
|
||||
const maximumPriceInCredits =
|
||||
(price.variablePrice &&
|
||||
price.variablePrice[aiRequestMode] &&
|
||||
price.variablePrice[aiRequestMode]['default'] &&
|
||||
price.variablePrice[aiRequestMode]['default'].maximumPriceInCredits) ||
|
||||
null;
|
||||
const minimumPriceInCredits =
|
||||
(price.variablePrice &&
|
||||
price.variablePrice[aiRequestMode] &&
|
||||
price.variablePrice[aiRequestMode]['default'] &&
|
||||
price.variablePrice[aiRequestMode]['default'].minimumPriceInCredits) ||
|
||||
null;
|
||||
|
||||
const priceText = maximumPriceInCredits ? (
|
||||
<Trans>
|
||||
{minimumPriceInCredits || priceInCredits} to {maximumPriceInCredits}
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>{minimumPriceInCredits || priceInCredits}</Trans>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
aiRequestMode === 'agent' ? (
|
||||
<>
|
||||
<Trans>
|
||||
Each request to the AI agent costs {priceText} credits. It depends
|
||||
on the amount of work the agent will do and the number of times it
|
||||
generates events.
|
||||
</Trans>{' '}
|
||||
{lastUserMessagePriceInCredits ? (
|
||||
<Trans>
|
||||
The last request used {lastUserMessagePriceInCredits} credits.
|
||||
</Trans>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Trans>Each answer from the AI costs {priceText} credits.</Trans>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<LineStackLayout alignItems="center" noMargin>
|
||||
{aiRequestMode === 'agent' ? (
|
||||
<Hammer fontSize="small" />
|
||||
) : (
|
||||
<Help fontSize="small" />
|
||||
)}
|
||||
{aiRequestMode === 'agent' ? (
|
||||
<Trans>{priceText} credits/request</Trans>
|
||||
) : (
|
||||
<Trans>{priceText} credits/answer</Trans>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
(
|
||||
{
|
||||
project,
|
||||
aiRequest,
|
||||
isSending,
|
||||
onStartNewAiRequest,
|
||||
onSendMessage,
|
||||
onSendFeedback,
|
||||
onStartNewChat,
|
||||
quota,
|
||||
increaseQuotaOffering,
|
||||
lastSendError,
|
||||
price,
|
||||
availableCredits,
|
||||
hasOpenedProject,
|
||||
editorFunctionCallResults,
|
||||
onProcessFunctionCalls,
|
||||
isAutoProcessingFunctionCalls,
|
||||
setAutoProcessFunctionCalls,
|
||||
i18n,
|
||||
editorCallbacks,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
// TODO: store the default mode in the user preferences?
|
||||
const [newAiRequestMode, setNewAiRequestMode] = React.useState<
|
||||
'chat' | 'agent'
|
||||
>('agent');
|
||||
const aiRequestId: string = aiRequest ? aiRequest.id : '';
|
||||
const [
|
||||
userRequestTextPerAiRequestId,
|
||||
setUserRequestTextPerRequestId,
|
||||
] = React.useState<{ [string]: string }>({});
|
||||
const scrollViewRef = React.useRef<ScrollViewInterface | null>(null);
|
||||
const requiredGameId = (aiRequest && aiRequest.gameId) || null;
|
||||
|
||||
const newChatPlaceholder = React.useMemo(
|
||||
() => {
|
||||
const newChatPlaceholders: Array<MessageDescriptor> =
|
||||
newAiRequestMode === 'agent'
|
||||
? hasOpenedProject
|
||||
? [
|
||||
t`Add solid rocks that falls from the sky at a random position around the player every 0.5 seconds`,
|
||||
t`Add a score and display it on the screen`,
|
||||
t`Create a 3D explosion when the player is hit`,
|
||||
]
|
||||
: [
|
||||
t`Build a platformer game with a score and coins to collect`,
|
||||
t`Make a quizz game with a question and 4 answers`,
|
||||
t`Make a game where the player must avoid obstacles`,
|
||||
]
|
||||
: [
|
||||
t`How to add a leaderboard?`,
|
||||
t`How to display the health of my player?`,
|
||||
t`How to add an explosion when an enemy is destroyed?`,
|
||||
t`How to create a main menu for my game?`,
|
||||
...(hasOpenedProject
|
||||
? [
|
||||
t`What would you add to my game?`,
|
||||
t`How to make my game more fun?`,
|
||||
t`What is a good GDevelop feature I could use in my game?`,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return newChatPlaceholders[
|
||||
Math.floor(Math.random() * newChatPlaceholders.length)
|
||||
];
|
||||
},
|
||||
[newAiRequestMode, hasOpenedProject]
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
resetUserInput: (aiRequestId: string | null) => {
|
||||
const aiRequestIdToReset: string = aiRequestId || '';
|
||||
setUserRequestTextPerRequestId(userRequestTextPerAiRequestId => ({
|
||||
...userRequestTextPerAiRequestId,
|
||||
[aiRequestIdToReset]: '',
|
||||
}));
|
||||
|
||||
if (scrollViewRef.current) {
|
||||
scrollViewRef.current.scrollToBottom({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
|
||||
const priceText = (
|
||||
<Text size="body-small" color="secondary" noMargin>
|
||||
{getPriceText({
|
||||
aiRequestMode: aiRequest
|
||||
? aiRequest.mode || 'chat'
|
||||
: newAiRequestMode,
|
||||
price,
|
||||
lastUserMessagePriceInCredits:
|
||||
(aiRequest && aiRequest.lastUserMessagePriceInCredits) || null,
|
||||
}) || '\u00A0'}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const subscriptionBanner =
|
||||
quota && quota.limitReached && increaseQuotaOffering !== 'none' ? (
|
||||
<GetSubscriptionCard
|
||||
placementId="ai-requests"
|
||||
subscriptionDialogOpeningReason={
|
||||
increaseQuotaOffering === 'subscribe'
|
||||
? 'AI requests (subscribe)'
|
||||
: 'AI requests (upgrade)'
|
||||
}
|
||||
label={
|
||||
increaseQuotaOffering === 'subscribe' ? (
|
||||
<Trans>Get GDevelop premium</Trans>
|
||||
) : (
|
||||
<Trans>Upgrade</Trans>
|
||||
)
|
||||
}
|
||||
recommendedPlanIdIfNoSubscription="gdevelop_gold"
|
||||
canHide
|
||||
>
|
||||
<Line>
|
||||
<Column noMargin>
|
||||
<Text noMargin>
|
||||
{increaseQuotaOffering === 'subscribe' ? (
|
||||
<Trans>
|
||||
Unlock AI requests included with a GDevelop premium plan.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Get even more AI requests included with a higher premium
|
||||
plan.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
</GetSubscriptionCard>
|
||||
) : null;
|
||||
|
||||
const errorText = lastSendError ? (
|
||||
<Text size="body-small" color="error" noMargin>
|
||||
<Trans>
|
||||
An error happened when sending your request, please try again.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : null;
|
||||
|
||||
const quotaOrCreditsText = (
|
||||
<Text size="body-small" color="secondary" noMargin>
|
||||
{getQuotaOrCreditsText({
|
||||
quota,
|
||||
increaseQuotaOffering,
|
||||
price,
|
||||
availableCredits,
|
||||
isMobile,
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
|
||||
if (!aiRequest) {
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.newChatContainer]: true,
|
||||
// Move the entire screen up when the soft keyboard is open:
|
||||
'avoid-soft-keyboard': true,
|
||||
})}
|
||||
>
|
||||
<ColumnStackLayout justifyContent="center" expand>
|
||||
<Line noMargin justifyContent="center">
|
||||
<RobotIcon rotating size={40} />
|
||||
</Line>
|
||||
<Column noMargin alignItems="center">
|
||||
<Text size="bold-title">
|
||||
{newAiRequestMode === 'agent' ? (
|
||||
<Trans>What do you want to make?</Trans>
|
||||
) : (
|
||||
<Trans>Ask any gamedev question</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Column>
|
||||
<Line noMargin justifyContent="center">
|
||||
<TwoStatesButton
|
||||
value={newAiRequestMode}
|
||||
leftButton={{
|
||||
icon: <Hammer fontSize="small" />,
|
||||
label: <Trans>Build for me (beta)</Trans>,
|
||||
value: 'agent',
|
||||
}}
|
||||
rightButton={{
|
||||
icon: <Help fontSize="small" />,
|
||||
label: <Trans>Ask a question</Trans>,
|
||||
value: 'chat',
|
||||
}}
|
||||
onChange={value => {
|
||||
if (value !== 'chat' && value !== 'agent') {
|
||||
return;
|
||||
}
|
||||
setNewAiRequestMode(value);
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
<form
|
||||
onSubmit={() => {
|
||||
onStartNewAiRequest({
|
||||
mode: newAiRequestMode,
|
||||
userRequest: userRequestTextPerAiRequestId[''],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ColumnStackLayout justifyContent="center" noMargin>
|
||||
<Column noMargin alignItems="stretch" justifyContent="stretch">
|
||||
<Spacer />
|
||||
<CompactTextAreaFieldWithControls
|
||||
maxLength={6000}
|
||||
value={userRequestTextPerAiRequestId[''] || ''}
|
||||
disabled={isSending}
|
||||
hasNeonCorner
|
||||
hasAnimatedNeonCorner={isSending}
|
||||
errored={!!lastSendError}
|
||||
onChange={userRequestText =>
|
||||
setUserRequestTextPerRequestId(
|
||||
userRequestTextPerAiRequestId => ({
|
||||
...userRequestTextPerAiRequestId,
|
||||
'': userRequestText,
|
||||
})
|
||||
)
|
||||
}
|
||||
onSubmit={() => {
|
||||
onStartNewAiRequest({
|
||||
mode: newAiRequestMode,
|
||||
userRequest: userRequestTextPerAiRequestId[''],
|
||||
});
|
||||
}}
|
||||
placeholder={newChatPlaceholder}
|
||||
rows={5}
|
||||
controls={
|
||||
<Column>
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<RaisedButton
|
||||
color="primary"
|
||||
icon={<Send />}
|
||||
label={
|
||||
newAiRequestMode === 'agent' ? (
|
||||
hasOpenedProject ? (
|
||||
<Trans>Build this on my game</Trans>
|
||||
) : (
|
||||
<Trans>Start building the game</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>Send question</Trans>
|
||||
)
|
||||
}
|
||||
style={{ flexShrink: 0 }}
|
||||
disabled={
|
||||
isSending ||
|
||||
!userRequestTextPerAiRequestId[aiRequestId]
|
||||
}
|
||||
onClick={() => {
|
||||
onStartNewAiRequest({
|
||||
mode: newAiRequestMode,
|
||||
userRequest: userRequestTextPerAiRequestId[''],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
}
|
||||
/>
|
||||
</Column>
|
||||
<Line noMargin>
|
||||
<LineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
expand
|
||||
>
|
||||
{errorText || priceText}
|
||||
{errorText ? null : quotaOrCreditsText}
|
||||
</LineStackLayout>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</form>
|
||||
{subscriptionBanner ? (
|
||||
<>
|
||||
<Spacer />
|
||||
{subscriptionBanner}
|
||||
</>
|
||||
) : null}
|
||||
</ColumnStackLayout>
|
||||
<Column justifyContent="center">
|
||||
{newAiRequestMode === 'agent' ? (
|
||||
<Text size="body-small" color="secondary" align="center" noMargin>
|
||||
<Trans>
|
||||
The AI agent will build simple games or features for you.{' '}
|
||||
<Link
|
||||
href={getHelpLink('/interface/ai')}
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
Window.openExternalURL(getHelpLink('/interface/ai'))
|
||||
}
|
||||
>
|
||||
It can inspect your game objects and events.
|
||||
</Link>
|
||||
</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="body-small" color="secondary" align="center" noMargin>
|
||||
<Trans>
|
||||
The AI chat is experimental and still being improved.{' '}
|
||||
<Link
|
||||
href={getHelpLink('/interface/ai')}
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
Window.openExternalURL(getHelpLink('/interface/ai'))
|
||||
}
|
||||
>
|
||||
It has access to your game objects but not events.
|
||||
</Link>
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{newAiRequestMode === 'agent' ? (
|
||||
<Text size="body-small" color="secondary" align="center" noMargin>
|
||||
<Trans>
|
||||
Results may vary: experiment and use it for learning.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="body-small" color="secondary" align="center" noMargin>
|
||||
<Trans>Answers may have mistakes: always verify them.</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Column>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const userMessagesCount = aiRequest.output.filter(
|
||||
message => message.type === 'message' && message.role === 'user'
|
||||
).length;
|
||||
|
||||
const hasWorkingFunctionCalls =
|
||||
editorFunctionCallResults &&
|
||||
editorFunctionCallResults.some(
|
||||
functionCallOutput => functionCallOutput.status === 'working'
|
||||
);
|
||||
const allFunctionCallsToProcess = getFunctionCallsToProcess({
|
||||
aiRequest,
|
||||
editorFunctionCallResults,
|
||||
});
|
||||
const isPausedAndHasFunctionCallsToProcess =
|
||||
!isAutoProcessingFunctionCalls && allFunctionCallsToProcess.length > 0;
|
||||
|
||||
const lastMessageIndex = aiRequest.output.length - 1;
|
||||
const lastMessage = aiRequest.output[lastMessageIndex];
|
||||
const shouldDisplayFeedbackBanner =
|
||||
!hasWorkingFunctionCalls &&
|
||||
!isPausedAndHasFunctionCallsToProcess &&
|
||||
!isSending &&
|
||||
aiRequest.status === 'ready' &&
|
||||
aiRequest.mode === 'agent' &&
|
||||
lastMessage.type === 'message' &&
|
||||
lastMessage.role === 'assistant';
|
||||
const lastMessageFeedbackBanner = shouldDisplayFeedbackBanner && (
|
||||
<FeedbackBanner
|
||||
onSendFeedback={(
|
||||
feedback: 'like' | 'dislike',
|
||||
reason?: string,
|
||||
freeFormDetails?: string
|
||||
) => {
|
||||
onSendFeedback(
|
||||
aiRequestId,
|
||||
lastMessageIndex,
|
||||
feedback,
|
||||
reason,
|
||||
freeFormDetails
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const isForAnotherProject =
|
||||
!!requiredGameId &&
|
||||
(!project || requiredGameId !== project.getProjectUuid());
|
||||
const isForAnotherProjectText = isForAnotherProject ? (
|
||||
<Text size="body-small" color="secondary" align="center" noMargin>
|
||||
<Trans>
|
||||
This request is for another project.{' '}
|
||||
<Link href="#" onClick={onStartNewChat}>
|
||||
Start a new chat
|
||||
</Link>{' '}
|
||||
to build on a new project.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.aiRequestChatContainer]: true,
|
||||
})}
|
||||
>
|
||||
<ScrollView ref={scrollViewRef} style={styles.chatScrollView}>
|
||||
<ChatMessages
|
||||
aiRequest={aiRequest}
|
||||
onSendFeedback={onSendFeedback}
|
||||
editorFunctionCallResults={editorFunctionCallResults}
|
||||
editorCallbacks={editorCallbacks}
|
||||
project={project}
|
||||
onProcessFunctionCalls={onProcessFunctionCalls}
|
||||
/>
|
||||
<Spacer />
|
||||
<ColumnStackLayout noMargin>
|
||||
{lastMessageFeedbackBanner}
|
||||
{userMessagesCount >= TOO_MANY_USER_MESSAGES_WARNING_COUNT ? (
|
||||
<AlertMessage
|
||||
kind={
|
||||
userMessagesCount >= TOO_MANY_USER_MESSAGES_ERROR_COUNT
|
||||
? 'error'
|
||||
: 'warning'
|
||||
}
|
||||
>
|
||||
<Trans>
|
||||
The chat is becoming long - consider creating a new chat to
|
||||
ask other questions. The AI will better analyze your game and
|
||||
request in a new chat.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : (
|
||||
subscriptionBanner
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
<form
|
||||
onSubmit={() => {
|
||||
onSendMessage({
|
||||
userMessage: userRequestTextPerAiRequestId[aiRequestId] || '',
|
||||
});
|
||||
}}
|
||||
className={classNames({
|
||||
// Move the form up when the soft keyboard is open:
|
||||
'avoid-soft-keyboard': true,
|
||||
})}
|
||||
>
|
||||
<ColumnStackLayout
|
||||
justifyContent="stretch"
|
||||
alignItems="stretch"
|
||||
noMargin
|
||||
>
|
||||
{aiRequest.mode === 'agent' &&
|
||||
isAutoProcessingFunctionCalls &&
|
||||
(hasWorkingFunctionCalls ||
|
||||
isSending ||
|
||||
aiRequest.status === 'working') ? (
|
||||
<Paper background="dark" variant="outlined">
|
||||
<Column>
|
||||
<LineStackLayout
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<LineStackLayout alignItems="center" noMargin>
|
||||
<CircularProgress variant="indeterminate" size={12} />
|
||||
<Text size="body" color="secondary" noMargin>
|
||||
<Trans>The AI is building your request.</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
<Text size="body" noMargin>
|
||||
<Link
|
||||
href={'#'}
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setAutoProcessFunctionCalls(false);
|
||||
}}
|
||||
>
|
||||
<Trans>Pause</Trans>
|
||||
</Link>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
</Paper>
|
||||
) : aiRequest.mode === 'agent' &&
|
||||
isPausedAndHasFunctionCallsToProcess ? (
|
||||
<Paper background="dark" variant="outlined">
|
||||
<Column>
|
||||
<LineStackLayout
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<LineStackLayout alignItems="center" noMargin>
|
||||
<Text size="body" color="secondary" noMargin>
|
||||
<Trans>The AI agent is paused.</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
<Text size="body" noMargin>
|
||||
<Link
|
||||
href={'#'}
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setAutoProcessFunctionCalls(true);
|
||||
onProcessFunctionCalls(allFunctionCallsToProcess);
|
||||
}}
|
||||
>
|
||||
<Trans>Resume all</Trans>
|
||||
</Link>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
</Paper>
|
||||
) : null}
|
||||
<CompactTextAreaFieldWithControls
|
||||
maxLength={6000}
|
||||
value={userRequestTextPerAiRequestId[aiRequestId] || ''}
|
||||
disabled={isSending || isForAnotherProject}
|
||||
errored={!!lastSendError}
|
||||
hasNeonCorner
|
||||
hasAnimatedNeonCorner={isSending}
|
||||
onChange={userRequestText =>
|
||||
setUserRequestTextPerRequestId(
|
||||
userRequestTextPerAiRequestId => ({
|
||||
...userRequestTextPerAiRequestId,
|
||||
[aiRequestId]: userRequestText,
|
||||
})
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
aiRequest.mode === 'agent'
|
||||
? isForAnotherProject
|
||||
? t`You must re-open the project to continue this chat.`
|
||||
: t`Specify something more to the AI to build`
|
||||
: t`Ask a follow up question`
|
||||
}
|
||||
rows={2}
|
||||
onSubmit={() => {
|
||||
onSendMessage({
|
||||
userMessage: userRequestTextPerAiRequestId[aiRequestId] || '',
|
||||
});
|
||||
}}
|
||||
controls={
|
||||
<Column>
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<RaisedButton
|
||||
color="primary"
|
||||
disabled={
|
||||
aiRequest.status === 'working' ||
|
||||
isSending ||
|
||||
isForAnotherProject ||
|
||||
!userRequestTextPerAiRequestId[aiRequestId]
|
||||
}
|
||||
icon={<Send />}
|
||||
onClick={() => {
|
||||
onSendMessage({
|
||||
userMessage:
|
||||
userRequestTextPerAiRequestId[aiRequestId] || '',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
}
|
||||
/>
|
||||
<Column noMargin alignItems="stretch">
|
||||
<LineStackLayout
|
||||
expand
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{isForAnotherProjectText || errorText || priceText}
|
||||
{errorText || isForAnotherProjectText
|
||||
? null
|
||||
: quotaOrCreditsText}
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
</ColumnStackLayout>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
1081
newIDE/app/src/AiGeneration/AskAiEditorContainer.js
Normal file
1081
newIDE/app/src/AiGeneration/AskAiEditorContainer.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,25 +2,25 @@
|
||||
import * as React from 'react';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import { Line, Column } from '../../../UI/Grid';
|
||||
import { ColumnStackLayout } from '../../../UI/Layout';
|
||||
import Text from '../../../UI/Text';
|
||||
import { Line, Column } from '../UI/Grid';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import Text from '../UI/Text';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import {
|
||||
getAiRequests,
|
||||
type AiRequest,
|
||||
} from '../../../Utils/GDevelopServices/Generation';
|
||||
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
|
||||
import Paper from '../../../UI/Paper';
|
||||
import ScrollView from '../../../UI/ScrollView';
|
||||
import FlatButton from '../../../UI/FlatButton';
|
||||
import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import EmptyMessage from '../../../UI/EmptyMessage';
|
||||
import CircularProgress from '../../../UI/CircularProgress';
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import Paper from '../UI/Paper';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
import CircularProgress from '../UI/CircularProgress';
|
||||
import formatDate from 'date-fns/format';
|
||||
import DrawerTopBar from '../../../UI/DrawerTopBar';
|
||||
import PlaceholderError from '../../../UI/PlaceholderError';
|
||||
import { textEllipsisStyle } from '../../../UI/TextEllipsis';
|
||||
import DrawerTopBar from '../UI/DrawerTopBar';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
|
||||
type Props = {|
|
||||
open: boolean,
|
||||
@@ -61,8 +61,15 @@ const getFirstUserRequestText = (aiRequest: AiRequest): string => {
|
||||
if (!aiRequest.output || aiRequest.output.length === 0) return '';
|
||||
|
||||
// Find the first user message
|
||||
const userMessage = aiRequest.output.find(message => message.role === 'user');
|
||||
if (!userMessage) return '';
|
||||
const userMessage = aiRequest.output.find(
|
||||
message => message.type === 'message' && message.role === 'user'
|
||||
);
|
||||
if (
|
||||
!userMessage ||
|
||||
userMessage.type !== 'message' ||
|
||||
userMessage.role !== 'user'
|
||||
)
|
||||
return '';
|
||||
|
||||
// Extract text from user message content
|
||||
return userMessage.content
|
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { ToolbarGroup } from '../../../UI/Toolbar';
|
||||
import AddIcon from '../../../UI/CustomSvgIcons/Add';
|
||||
import HistoryIcon from '../../../UI/CustomSvgIcons/History';
|
||||
import RaisedButton from '../../../UI/RaisedButton';
|
||||
import IconButton from '../../../UI/IconButton';
|
||||
import { ToolbarGroup } from '../UI/Toolbar';
|
||||
import AddIcon from '../UI/CustomSvgIcons/Add';
|
||||
import HistoryIcon from '../UI/CustomSvgIcons/History';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import IconButton from '../UI/IconButton';
|
||||
|
||||
type Props = {|
|
||||
onStartNewChat: () => void,
|
146
newIDE/app/src/AiGeneration/UseCreateAiProjectDialog.js
Normal file
146
newIDE/app/src/AiGeneration/UseCreateAiProjectDialog.js
Normal file
@@ -0,0 +1,146 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import EmptyAndStartingPointProjects from '../ProjectCreation/EmptyAndStartingPointProjects';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import UrlStorageProvider from '../ProjectsStorage/UrlStorageProvider';
|
||||
import { generateProjectName } from '../ProjectCreation/NewProjectSetupDialog';
|
||||
import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog';
|
||||
import { Spacer } from '../UI/Grid';
|
||||
|
||||
type RenderCreateAiProjectDialogProps = {
|
||||
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
|
||||
onCreateProjectFromExample: (
|
||||
exampleShortHeader: ExampleShortHeader,
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<void>,
|
||||
};
|
||||
|
||||
type CreateAiProjectDialogProps = {
|
||||
onClose: () => void,
|
||||
onSelectExampleShortHeader: (
|
||||
exampleShortHeader: ExampleShortHeader
|
||||
) => Promise<void>,
|
||||
onSelectEmptyProject: () => Promise<void>,
|
||||
};
|
||||
|
||||
const CreateAiProjectDialog = ({
|
||||
onClose,
|
||||
onSelectExampleShortHeader,
|
||||
onSelectEmptyProject,
|
||||
}: CreateAiProjectDialogProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={<Trans>Ok! Choose a game style to start with</Trans>}
|
||||
id="ai-project-dialog"
|
||||
maxWidth="md"
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={<Trans>Cancel</Trans>}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
]}
|
||||
onRequestClose={onClose}
|
||||
flexColumnBody
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Spacer />
|
||||
<EmptyAndStartingPointProjects
|
||||
onSelectExampleShortHeader={exampleShortHeader => {
|
||||
onSelectExampleShortHeader(exampleShortHeader);
|
||||
}}
|
||||
onSelectEmptyProject={() => {
|
||||
onSelectEmptyProject();
|
||||
}}
|
||||
/>
|
||||
{/* Use a spacer to avoid extra scrollbars when template tiles are hovered. */}
|
||||
<Spacer />
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
type CreateAiProjectResult = 'canceled' | 'created';
|
||||
|
||||
export const useCreateAiProjectDialog = () => {
|
||||
const [createPromise, setCreatePromise] = React.useState<null | {|
|
||||
onFinished: (result: CreateAiProjectResult) => void,
|
||||
promise: Promise<CreateAiProjectResult>,
|
||||
|}>(null);
|
||||
|
||||
const createAiProject: () => Promise<CreateAiProjectResult> = React.useCallback(
|
||||
() => {
|
||||
if (createPromise) {
|
||||
return createPromise.promise;
|
||||
}
|
||||
|
||||
// Make a promise that we can resolve later from the creation dialog.
|
||||
let resolve: (result: CreateAiProjectResult) => void = () => {};
|
||||
const promise = new Promise(resolveFn => {
|
||||
resolve = resolveFn;
|
||||
});
|
||||
|
||||
setCreatePromise({
|
||||
onFinished: (result: CreateAiProjectResult) => {
|
||||
setCreatePromise(null);
|
||||
resolve(result);
|
||||
},
|
||||
promise,
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
[createPromise]
|
||||
);
|
||||
|
||||
return {
|
||||
createAiProject,
|
||||
renderCreateAiProjectDialog: (props: RenderCreateAiProjectDialogProps) => {
|
||||
if (!createPromise) return null;
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<CreateAiProjectDialog
|
||||
onClose={() => {
|
||||
createPromise.onFinished('canceled');
|
||||
}}
|
||||
onSelectExampleShortHeader={async exampleShortHeader => {
|
||||
const newProjectSetup: NewProjectSetup = {
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
};
|
||||
await props.onCreateProjectFromExample(
|
||||
exampleShortHeader,
|
||||
newProjectSetup,
|
||||
i18n,
|
||||
false // isQuickCustomization
|
||||
);
|
||||
createPromise.onFinished('created');
|
||||
}}
|
||||
onSelectEmptyProject={async () => {
|
||||
await props.onCreateEmptyProject({
|
||||
projectName: generateProjectName('AI starter'),
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
});
|
||||
createPromise.onFinished('created');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
54
newIDE/app/src/AiGeneration/UseEnsureExtensionInstalled.js
Normal file
54
newIDE/app/src/AiGeneration/UseEnsureExtensionInstalled.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { ExtensionStoreContext } from '../AssetStore/ExtensionStore/ExtensionStoreContext';
|
||||
import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import { installExtension } from '../AssetStore/ExtensionStore/InstallExtension';
|
||||
|
||||
type EnsureExtensionInstalledOptions = {|
|
||||
extensionName: string,
|
||||
|};
|
||||
|
||||
export const useEnsureExtensionInstalled = ({
|
||||
project,
|
||||
i18n,
|
||||
}: {|
|
||||
project: ?gdProject,
|
||||
i18n: I18nType,
|
||||
|}) => {
|
||||
const { translatedExtensionShortHeadersByName } = React.useContext(
|
||||
ExtensionStoreContext
|
||||
);
|
||||
const eventsFunctionsExtensionsState = React.useContext(
|
||||
EventsFunctionsExtensionsContext
|
||||
);
|
||||
|
||||
return {
|
||||
ensureExtensionInstalled: React.useCallback(
|
||||
async ({ extensionName }: EnsureExtensionInstalledOptions) => {
|
||||
if (!project) return;
|
||||
if (project.getCurrentPlatform().isExtensionLoaded(extensionName))
|
||||
return;
|
||||
|
||||
const extensionShortHeader =
|
||||
translatedExtensionShortHeadersByName[extensionName];
|
||||
if (!extensionShortHeader) {
|
||||
throw new Error(`Can't find extension named ${extensionName}.`);
|
||||
}
|
||||
|
||||
await installExtension(
|
||||
i18n,
|
||||
project,
|
||||
eventsFunctionsExtensionsState,
|
||||
extensionShortHeader
|
||||
);
|
||||
},
|
||||
[
|
||||
eventsFunctionsExtensionsState,
|
||||
i18n,
|
||||
project,
|
||||
translatedExtensionShortHeadersByName,
|
||||
]
|
||||
),
|
||||
};
|
||||
};
|
107
newIDE/app/src/AiGeneration/UseGenerateEvents.js
Normal file
107
newIDE/app/src/AiGeneration/UseGenerateEvents.js
Normal file
@@ -0,0 +1,107 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { retryIfFailed } from '../Utils/RetryIfFailed';
|
||||
import { delay } from '../Utils/Delay';
|
||||
import {
|
||||
getAiGeneratedEvent,
|
||||
createAiGeneratedEvent,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
|
||||
import { type EventsGenerationResult } from '../EditorFunctions';
|
||||
import { makeSimplifiedProjectBuilder } from '../EditorFunctions/SimplifiedProject/SimplifiedProject';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
export const useGenerateEvents = ({ project }: {| project: ?gdProject |}) => {
|
||||
const { profile, getAuthorizationHeader } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
|
||||
const generateEvents = React.useCallback(
|
||||
async ({
|
||||
sceneName,
|
||||
eventsDescription,
|
||||
extensionNamesList,
|
||||
objectsList,
|
||||
existingEventsAsText,
|
||||
placementHint,
|
||||
relatedAiRequestId,
|
||||
}: {|
|
||||
sceneName: string,
|
||||
eventsDescription: string,
|
||||
extensionNamesList: string,
|
||||
objectsList: string,
|
||||
existingEventsAsText: string,
|
||||
placementHint: string,
|
||||
relatedAiRequestId: string,
|
||||
|}): Promise<EventsGenerationResult> => {
|
||||
if (!project) throw new Error('No project is opened.');
|
||||
if (!profile) throw new Error('User should be authenticated.');
|
||||
|
||||
const simplifiedProjectBuilder = makeSimplifiedProjectBuilder(gd);
|
||||
const projectSpecificExtensionsSummaryJson = JSON.stringify(
|
||||
simplifiedProjectBuilder.getProjectSpecificExtensionsSummary(project)
|
||||
);
|
||||
|
||||
const createResult = await retryIfFailed({ times: 2 }, () =>
|
||||
createAiGeneratedEvent(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
partialGameProjectJson: JSON.stringify(
|
||||
simplifiedProjectBuilder.getSimplifiedProject(project, {
|
||||
scopeToScene: sceneName,
|
||||
}),
|
||||
null,
|
||||
2
|
||||
),
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
sceneName,
|
||||
eventsDescription,
|
||||
extensionNamesList,
|
||||
objectsList,
|
||||
existingEventsAsText,
|
||||
placementHint,
|
||||
relatedAiRequestId,
|
||||
})
|
||||
);
|
||||
|
||||
if (!createResult.creationSucceeded) {
|
||||
return {
|
||||
generationCompleted: false,
|
||||
errorMessage: createResult.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
let remainingAttempts = 50;
|
||||
let aiGeneratedEvent = createResult.aiGeneratedEvent;
|
||||
while (aiGeneratedEvent.status === 'working') {
|
||||
remainingAttempts--;
|
||||
await delay(1000);
|
||||
|
||||
try {
|
||||
aiGeneratedEvent = await getAiGeneratedEvent(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
aiGeneratedEventId: aiGeneratedEvent.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Error while checking status of AI generated event - continuing...',
|
||||
error
|
||||
);
|
||||
}
|
||||
if (remainingAttempts <= 0) {
|
||||
return {
|
||||
generationCompleted: false,
|
||||
errorMessage:
|
||||
'Event generation started but failed to complete in time.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { generationCompleted: true, aiGeneratedEvent };
|
||||
},
|
||||
[getAuthorizationHeader, project, profile]
|
||||
);
|
||||
|
||||
return { generateEvents };
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user