Compare commits
23 Commits
cocos-main
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb78d8bf5c | ||
|
|
c4641aee19 | ||
|
|
782f76170f | ||
|
|
d83654837a | ||
|
|
899adbc9d4 | ||
|
|
4499d7fdef | ||
|
|
11d7496b54 | ||
|
|
1012af6039 | ||
|
|
37e0413576 | ||
|
|
8dfd42397f | ||
|
|
19bb63cd87 | ||
|
|
bae333ba7c | ||
|
|
aaccb73773 | ||
|
|
9c27042e27 | ||
|
|
6a3e797a9a | ||
| 74b983c74e | |||
| a699896116 | |||
|
|
43b983fb99 | ||
|
|
4fc07b1458 | ||
|
|
3bafb1e10d | ||
| b08562d603 | |||
| 33070ad827 | |||
| 6e0a1fbcbb |
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
.secrets
|
||||
40
nginx.conf
@@ -4,10 +4,6 @@ server {
|
||||
index index.html index.htm default.htm default.html;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# 1. 添加 resolver 指令
|
||||
# 127.0.0.11 是 Docker 内置的 DNS 服务器地址 并且将缓存设置为10s
|
||||
# resolver 127.0.0.11 valid=10s;
|
||||
|
||||
# gzip 配置
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
@@ -22,37 +18,21 @@ server {
|
||||
return 301 /competition.html?competition_id=13;
|
||||
}
|
||||
|
||||
# Unity 预压缩文件:禁止再次 gzip,避免 Content-Length 与实际内容不一致
|
||||
# Brotli 压缩 WebAssembly 文件处理
|
||||
location ~* \.wasm\.br$ {
|
||||
gzip off;
|
||||
gzip_static off;
|
||||
default_type application/wasm;
|
||||
default_type "";
|
||||
add_header Content-Encoding br;
|
||||
add_header Content-Type application/wasm always;
|
||||
add_header Vary Accept-Encoding;
|
||||
add_header Accept-Ranges bytes;
|
||||
expires 1h;
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
location ~* \.js\.br$ {
|
||||
gzip off;
|
||||
gzip_static off;
|
||||
default_type application/javascript;
|
||||
location ~* \.asm\.js\.br$ {
|
||||
default_type "";
|
||||
add_header Content-Encoding br;
|
||||
add_header Content-Type application/javascript always;
|
||||
add_header Vary Accept-Encoding;
|
||||
add_header Accept-Ranges bytes;
|
||||
expires 1h;
|
||||
}
|
||||
|
||||
location ~* \.data\.br$ {
|
||||
gzip off;
|
||||
gzip_static off;
|
||||
default_type application/octet-stream;
|
||||
add_header Content-Encoding br;
|
||||
add_header Vary Accept-Encoding;
|
||||
add_header Accept-Ranges bytes;
|
||||
expires 1h;
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
# 通用 Brotli 文件处理
|
||||
@@ -78,12 +58,10 @@ server {
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 纯 CDN:/cdn-unity/ → OSS 游戏包(config.js unityCdnUseSiteProxy: true)
|
||||
# 路径需与 unityCdnRemote 保持一致,更新 OSS 目录时同步修改
|
||||
# location /cdn-unity/ {
|
||||
# # 2. 将域名赋值给一个变量
|
||||
# set $oss_backend "https://oss.eanic.cn";
|
||||
# # 3. proxy_pass 使用变量,这样 Nginx 就会在运行时解析域名
|
||||
# proxy_pass $oss_backend/001_code_cocos_res_20260616/;
|
||||
# proxy_pass https://oss.eanic.cn/001_code_cocos_res_20260616/;
|
||||
# proxy_ssl_server_name on;
|
||||
# proxy_set_header Host oss.eanic.cn;
|
||||
# proxy_hide_header Access-Control-Allow-Origin;
|
||||
@@ -103,4 +81,4 @@ server {
|
||||
# 错误日志和访问日志配置
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
}
|
||||
}
|
||||
|
||||
0
scratch-blocks/build.py
Normal file → Executable file
0
scratch-blocks/cleanup.sh
Normal file → Executable file
0
scratch-blocks/i18n/create_messages.py
Normal file → Executable file
0
scratch-blocks/i18n/dedup_json.py
Normal file → Executable file
0
scratch-blocks/i18n/js_to_json.py
Normal file → Executable file
0
scratch-blocks/i18n/json_to_js.py
Normal file → Executable file
0
scratch-blocks/i18n/sync_tx_translations.js
Normal file → Executable file
0
scratch-blocks/i18n/xliff_to_json.py
Normal file → Executable file
0
scratch-blocks/local_build.sh
Normal file → Executable file
0
scratch-blocks/media/icons/control_forever.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
0
scratch-blocks/media/icons/control_repeat.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
0
scratch-blocks/media/icons/control_wait.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
0
scratch-blocks/media/icons/event_broadcast_blue.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_broadcast_coral.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_broadcast_green.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_broadcast_magenta.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_broadcast_orange.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_broadcast_purple.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_when-broadcast-received_blue.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_when-broadcast-received_coral.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_when-broadcast-received_green.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_when-broadcast-received_magenta.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_when-broadcast-received_orange.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_when-broadcast-received_purple.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
scratch-blocks/media/icons/event_whenflagclicked.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 634 B |
0
scratch-blocks/media/icons/set-led_blue.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
0
scratch-blocks/media/icons/set-led_coral.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
0
scratch-blocks/media/icons/set-led_green.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
0
scratch-blocks/media/icons/set-led_magenta.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
0
scratch-blocks/media/icons/set-led_mystery.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
0
scratch-blocks/media/icons/set-led_orange.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
0
scratch-blocks/media/icons/set-led_purple.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
0
scratch-blocks/media/icons/set-led_white.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
0
scratch-blocks/media/icons/set-led_yellow.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
0
scratch-blocks/media/icons/wedo_motor-clockwise.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
0
scratch-blocks/media/icons/wedo_motor-counterclockwise.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
0
scratch-blocks/media/icons/wedo_when-distance_close.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
0
scratch-blocks/media/icons/wedo_when-tilt-backward.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
0
scratch-blocks/media/icons/wedo_when-tilt-forward.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
0
scratch-blocks/media/icons/wedo_when-tilt-left.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
0
scratch-blocks/media/icons/wedo_when-tilt-right.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
0
scratch-blocks/media/icons/wedo_when-tilt.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
7
scratch-blocks/package-lock.json
generated
@@ -144,7 +144,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz",
|
||||
"integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
@@ -915,7 +914,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||
"integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"co": "^4.6.0",
|
||||
"fast-deep-equal": "^1.0.0",
|
||||
@@ -1573,7 +1571,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001565",
|
||||
"electron-to-chromium": "^1.4.601",
|
||||
@@ -6912,7 +6909,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -7808,7 +7804,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -8946,7 +8941,6 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz",
|
||||
"integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.9.0",
|
||||
"@webassemblyjs/helper-module-context": "1.9.0",
|
||||
@@ -9101,7 +9095,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
||||
0
scratch-blocks/pull_from_blockly.sh
Normal file → Executable file
BIN
scratch-gui/competitio_login/assets/gongan-beian.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -384,7 +384,7 @@
|
||||
<footer>
|
||||
<div class="footer-line" role="presentation"></div>
|
||||
<p class="footer-text">ICP备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="color:#478ac9 ;">沪ICP备2026042325号</a>
|
||||
<!-- 公安备案号:<a href="https://beian.mps.gov.cn/" target="_blank" style="color:#478ac9 ;">川公网安备51011202000980号</a> -->
|
||||
公安备案号:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=31011502407417" rel="noreferrer" target="_blank" style="color:#478ac9 ;"><img src="assets/gongan-beian.png" alt="" width="16" height="16" style="width:16px;height:16px;vertical-align:-3px;margin-right:4px;" />沪公网安备31011502407417号</a>
|
||||
© 灵丌编程(001CODE.COM)</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
9
scratch-gui/package-lock.json
generated
@@ -7688,9 +7688,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001649",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz",
|
||||
"integrity": "sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==",
|
||||
"version": "1.0.30001810",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
|
||||
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7704,7 +7704,8 @@
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvas-toBlob": {
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"build:cdn-pure": "npm run clean && cross-env NODE_ENV=production UNITY_CDN_PURE=1 webpack --colors --bail",
|
||||
"build:test": "npm run clean && cross-env NODE_ENV=production node scripts\\patch-config.js && webpack --colors --bail",
|
||||
"build:prod": "npm run clean && cross-env NODE_ENV=production node scripts\\patch-config.js && webpack --colors --bail",
|
||||
"clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist",
|
||||
"clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist && rimraf ./node_modules/.cache",
|
||||
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"[skip ci] Build for $(git log --pretty=format:%H -n1)\"",
|
||||
"prepublish": "node scripts/prepublish.mjs",
|
||||
"prune": "./prune-gh-pages.sh",
|
||||
@@ -28,6 +28,8 @@
|
||||
"test:lint": "eslint . --ext .js,.jsx",
|
||||
"test:unit": "jest test[\\\\/]unit[\\\\/]addons",
|
||||
"test:smoke": "jest --runInBand test[\\\\/]smoke",
|
||||
"test:cpp": "node scripts/test-cpp-regressions.cjs",
|
||||
"test:devtool": "node scripts/test-devtool-control.cjs",
|
||||
"watch": "webpack --colors --watch"
|
||||
},
|
||||
"config": {
|
||||
|
||||
0
scratch-gui/prune-gh-pages.sh
Normal file → Executable file
71
scratch-gui/scripts/diagnose-cpp.cjs
Normal file
@@ -0,0 +1,71 @@
|
||||
// Read-only diagnostic: exercise the current editor interpreter with mocked game APIs.
|
||||
// Run from scratch-gui: node scripts/diagnose-cpp.cjs
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const babel = require('@babel/core');
|
||||
const filename = path.resolve(__dirname, '../src/components/stage-cpp/cpp-parser.js');
|
||||
const transformed = babel.transformSync(fs.readFileSync(filename, 'utf8'), {
|
||||
filename, configFile: false, babelrc: false,
|
||||
plugins: ['@babel/plugin-transform-modules-commonjs']
|
||||
}).code;
|
||||
const context = {
|
||||
exports: {}, require, setTimeout,
|
||||
console: {log() {}, warn() {}, error() {}}, window: {}
|
||||
};
|
||||
vm.runInNewContext(transformed, context, {filename});
|
||||
const {CppCodeExecutor, validateCppSyntax} = context.exports;
|
||||
const main = body => `int main() {\n${body}\n}`;
|
||||
const customer = `void test() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
turnLeft(-1);
|
||||
for (int i = 0; i < 1; i++) {
|
||||
test();
|
||||
playerMove(i);
|
||||
}
|
||||
}`;
|
||||
const cases = [
|
||||
['customer_local_i', customer, {moves: [0]}],
|
||||
['customer_renamed_inner_j', customer.replace('int i = 0; i < 2; i++', 'int j = 0; j < 2; j++'), {moves: [0]}],
|
||||
['cout_literal', main('cout << "Hello" << endl;'), {output: ['Hello\n']}],
|
||||
['cout_variable', main('int i = 7;\ncout << "i=" << i << endl;'), {output: ['i=7\n']}],
|
||||
['cout_string_assignment_shape', main('cout << "value=42" << endl;'), {output: ['value=42\n']}],
|
||||
['cout_increment_expression', main('int i = 0;\ncout << i++ << endl;\nplayerMove(i);'), {output: ['0\n'], moves: [1]}],
|
||||
['cout_increment_literal', main('cout << "C++" << endl;'), {output: ['C++\n']}],
|
||||
['cout_std_qualified', main('std::cout << "Hello" << std::endl;'), {output: ['Hello\n']}],
|
||||
['main_return_zero', main('return 0;'), {error: null}],
|
||||
['legal_space_before_call', main('playerMove (1);'), {valid: true, moves: [1]}],
|
||||
['nested_loop_shadowing', main('for (int i = 0; i < 2; i++) {\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);\n}'), {moves: [0, 1]}],
|
||||
['negative_integer_division', main('int a = -3;\na /= 2;\nplayerMove(a);'), {moves: [-1]}],
|
||||
['function_return_value', 'int answer() {\nreturn 7;\n}\n' + main('int n = answer();\nplayerMove(n);'), {moves: [7]}],
|
||||
['nested_if_break', main('for (int i = 0; i < 3; i++) {\nif (i == 1) {\nbreak;\n}\nplayerMove(i);\n}'), {moves: [0]}],
|
||||
['floating_point', main('double x = 1.5;\ncout << x << endl;'), {output: ['1.5\n']}],
|
||||
['array_index', main('int a[2] = {3, 7};\nplayerMove(a[1]);'), {moves: [7]}],
|
||||
['unknown_variable', main('playerMove(missing);'), {valid: false}],
|
||||
['early_return_zero', 'int stop() {\nreturn 0;\nplayerMove(9);\n}\n' + main('stop();'), {moves: []}]
|
||||
];
|
||||
(async () => {
|
||||
const results = [];
|
||||
for (const [name, code, expected] of cases) {
|
||||
const actual = {valid: false, moves: [], turns: [], output: [], error: null};
|
||||
context.window = {isScriptRunOver: false, cppGameAPI: {
|
||||
playerMove: async n => actual.moves.push(n),
|
||||
turnLeft: async n => actual.turns.push(n),
|
||||
cmdPrint: text => actual.output.push(text)
|
||||
}};
|
||||
const validation = await validateCppSyntax(code);
|
||||
actual.valid = validation.isValid;
|
||||
if (!actual.valid) actual.validationErrors = validation.errors;
|
||||
const executor = new CppCodeExecutor(code, null);
|
||||
if (actual.valid) {
|
||||
try { await executor.run(); } catch (error) {actual.error = error.message;}
|
||||
}
|
||||
const matchesExpected = Object.entries(expected).every(([key, value]) =>
|
||||
JSON.stringify(actual[key]) === JSON.stringify(value));
|
||||
results.push({name, expected, actual, matchesExpected});
|
||||
}
|
||||
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
@@ -21,7 +21,7 @@ function getConfigForEnv(env) {
|
||||
case 'test':
|
||||
return { url: 'https://test-service.001code.cn', disabledev: false };
|
||||
case 'prod':
|
||||
return { url: 'https://service.001code.cn', disabledev: true };
|
||||
return { url: 'https://service.001code.com', disabledev: true };
|
||||
case 'start':
|
||||
default:
|
||||
return { url: 'http://127.0.0.1:8000', disabledev: false };
|
||||
|
||||
139
scratch-gui/scripts/test-cpp-regressions.cjs
Normal file
@@ -0,0 +1,139 @@
|
||||
// Regression tests for variable scopes and return control flow in the editor interpreter.
|
||||
// Run: node scripts/test-cpp-regressions.cjs
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const babel = require('@babel/core');
|
||||
const parserPath = path.resolve(__dirname, '../src/components/stage-cpp/cpp-parser.js');
|
||||
const source = fs.readFileSync(parserPath, 'utf8');
|
||||
const compiled = babel.transformSync(source, {
|
||||
filename: parserPath, configFile: false, babelrc: false,
|
||||
plugins: ['@babel/plugin-transform-modules-commonjs'],
|
||||
presets: process.argv.includes('--transpiled') ? ['@babel/preset-env'] : []
|
||||
}).code;
|
||||
const silentConsole = {log() {}, warn() {}, error() {}};
|
||||
const context = {exports: {}, require, setTimeout, console: silentConsole, window: {}};
|
||||
vm.runInNewContext(compiled, context, {filename: parserPath});
|
||||
const {CppCodeExecutor, validateCppSyntax, createCppExecutor} = context.exports;
|
||||
const main = body => `int main() {\n${body}\n}`;
|
||||
// Complete program from the first email screenshot; the second repeats this issue.
|
||||
const customerEmailCode = `void test() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 在这里编写你的C++代码来控制角色
|
||||
turnLeft(-1);
|
||||
for (int i = 0; i < 1; i++) {
|
||||
test();
|
||||
playerMove(i);
|
||||
}
|
||||
return 0;
|
||||
}`;
|
||||
async function run(code, expected, configure) {
|
||||
const moves = [];
|
||||
const turns = [];
|
||||
context.window = {isScriptRunOver: false, cppGameAPI: {
|
||||
playerMove: async n => moves.push(n),
|
||||
turnLeft: async n => turns.push(n),
|
||||
cmdPrint: () => {}
|
||||
}};
|
||||
const executor = new CppCodeExecutor(code, null);
|
||||
if (configure) configure(executor, context.window);
|
||||
const validation = await validateCppSyntax(code);
|
||||
assert.strictEqual(validation.isValid, true, JSON.stringify(validation.errors));
|
||||
const exitCode = await executor.run();
|
||||
if (Object.prototype.hasOwnProperty.call(expected, 'exitCode')) assert.strictEqual(exitCode, expected.exitCode);
|
||||
if (expected.moves) assert.deepStrictEqual(moves, expected.moves);
|
||||
if (expected.turns) assert.deepStrictEqual(turns, expected.turns);
|
||||
assert.strictEqual(executor.variables, executor.globalVariables, 'scope must unwind after execution');
|
||||
assert.strictEqual(executor.callStack.length, 0);
|
||||
assert.strictEqual(executor.currentExecutionContext, null);
|
||||
assert.strictEqual(executor.isRunning, false);
|
||||
return executor;
|
||||
}
|
||||
|
||||
const cases = [
|
||||
['screenshot: main return 1 is a normal exit', main('playerMove(4);\nreturn 1;'), {moves: [4], exitCode: 1}],
|
||||
['return 1 skips remaining main statements', main('return 1;\nplayerMove(9);'), {moves: [], exitCode: 1}],
|
||||
['return 0 also stops main early', main('return 0;\nplayerMove(9);'), {moves: [], exitCode: 0}],
|
||||
['negative exit code', main('return -1;'), {moves: [], exitCode: -1}],
|
||||
['parenthesized return expression', main('int status = 2;\nreturn (status + 1);'), {moves: [], exitCode: 3}],
|
||||
['main fallthrough returns zero', main('playerMove(1);'), {moves: [1], exitCode: 0}],
|
||||
['return from for loop exits main', main('for (int i = 0; i < 3; i++) {\nplayerMove(i);\nreturn 1;\n}\nplayerMove(9);'), {moves: [0], exitCode: 1}],
|
||||
['return zero inside loop is not ignored', main('for (int i = 0; i < 3; i++) {\nreturn 0;\n}\nplayerMove(9);'), {moves: [], exitCode: 0}],
|
||||
['return through while and if exits main', main('while (true) {\nif (true) {\nreturn 2;\n}\nplayerMove(9);\n}\nplayerMove(8);'), {moves: [], exitCode: 2}],
|
||||
['function return 0 skips only its own remaining statements', 'int f() {\nreturn 0;\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);\nreturn 1;'), {moves: [1], exitCode: 1}],
|
||||
['function return inside loop resumes caller', 'int f() {\nfor (int i = 0; i < 3; i++) {\nreturn 1;\n}\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);'), {moves: [1], exitCode: 0}],
|
||||
['identifier starting with return is not a return statement', main('int returnValue = 1;\nreturnValue = 2;\nplayerMove(returnValue);\nreturn 0;'), {moves: [2], exitCode: 0}],
|
||||
['customer email complete program (screenshots 1 and 2)', customerEmailCode, {moves: [0], turns: [-1]}],
|
||||
['nested loops', main('for (int i = 0; i < 2; i++) {\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);\n}'), {moves: [0, 1]}],
|
||||
['loop initializer restores shadowed value', main('int i = 9;\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [9]}],
|
||||
['loop assignment updates existing value', main('int i = 9;\nfor (i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [2]}],
|
||||
['block declaration versus assignment', main('int n = 1;\nif (true) {\nint n = 7;\nplayerMove(n);\n}\nif (true) {\nn = 3;\n}\nplayerMove(n);'), {moves: [7, 3]}],
|
||||
['for body scope per iteration', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nplayerMove(x);\nint x = 2;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}],
|
||||
['while body scope per iteration', main('int x = 9;\nint i = 0;\nwhile (i < 2) {\nplayerMove(x);\nint x = 2;\ni++;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}],
|
||||
['function locals', 'void f() {\nint n = 7;\nplayerMove(n);\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [7, 2]}],
|
||||
['all arguments evaluated before binding', 'void f(int a, int b) {\nplayerMove(a);\nplayerMove(b);\n}\n' + main('int a = 2;\nf(7, a);\nplayerMove(a);'), {moves: [7, 2, 2]}],
|
||||
['recursive locals and parameters', 'void f(int n) {\nint saved = n;\nif (n > 0) {\nf(n - 1);\n}\nplayerMove(saved);\n}\n' + main('f(2);'), {moves: [0, 1, 2]}],
|
||||
['early void return unwinds scope', 'void f() {\nint n = 8;\nreturn;\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [2]}],
|
||||
['continue unwinds body scope', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nint x = 2;\ncontinue;\n}\nplayerMove(x);'), {moves: [9]}],
|
||||
['break does not increment', main('int i = 0;\nfor (i = 0; i < 2; i++) {\nbreak;\n}\nplayerMove(i);'), {moves: [0]}],
|
||||
];
|
||||
|
||||
(async () => {
|
||||
for (const [name, code, expected] of cases) {
|
||||
try { await run(code, expected); } catch (error) {error.message = `${name}: ${error.message}`; throw error;}
|
||||
console.log(`PASS ${name}`);
|
||||
}
|
||||
const strings = new CppCodeExecutor('', null);
|
||||
await strings.executeSimpleStatement('string msg = "outer";');
|
||||
await strings.withVariableScope(async () => {
|
||||
await strings.executeSimpleStatement('string msg = "inner";');
|
||||
assert.strictEqual(strings.variables.get('msg'), 'inner');
|
||||
});
|
||||
assert.strictEqual(strings.variables.get('msg'), 'outer');
|
||||
console.log('PASS string block shadowing');
|
||||
|
||||
// Seed the interpreter's global environment to verify lexical lookup and writes.
|
||||
const globalExecutor = await run('void f() {\ng += 1;\nplayerMove(g);\n}\n' + main('int g = 9;\nf();\nplayerMove(g);'), {moves: [2, 9]}, executor => executor.variables.set('g', 1));
|
||||
assert.strictEqual(globalExecutor.variables.get('g'), 2);
|
||||
console.log('PASS global writes survive function calls; caller locals are not visible');
|
||||
|
||||
for (const stop of [false, true]) {
|
||||
const executor = new CppCodeExecutor('void f(int n) {\nfor (int i = 0; i < 2; i++) {\nplayerMove(i);\n}\n}\n' + main('f(2);'), null);
|
||||
executor.variables.set('n', 9);
|
||||
context.window = {isScriptRunOver: false, cppGameAPI: {playerMove: async () => {
|
||||
if (stop) context.window.isScriptRunOver = true;
|
||||
else throw new Error('game failure');
|
||||
}}};
|
||||
if (stop) await executor.run();
|
||||
else await assert.rejects(executor.run(), /game failure/);
|
||||
assert.strictEqual(executor.variables, executor.globalVariables);
|
||||
assert.strictEqual(executor.variables.get('n'), 9);
|
||||
assert.strictEqual(executor.variables.has('i'), false);
|
||||
assert.strictEqual(executor.callStack.length, 0);
|
||||
assert.strictEqual(executor.currentExecutionContext, null);
|
||||
console.log(`PASS scope cleanup on ${stop ? 'stop' : 'error'}`);
|
||||
}
|
||||
|
||||
const highlighted = [];
|
||||
context.window = {isScriptRunOver: false, setHighlightedLine: line => highlighted.push(line)};
|
||||
const manager = createCppExecutor();
|
||||
for (const status of [1, 0]) {
|
||||
assert.strictEqual(await manager.execute(main(`return ${status};`)), status);
|
||||
assert.strictEqual(manager.currentExecutor, null);
|
||||
assert.strictEqual(highlighted[highlighted.length - 1], null);
|
||||
}
|
||||
console.log('PASS editor executor manager resolves nonzero exit and can run again');
|
||||
|
||||
context.window.cppGameAPI = {playerMove: async () => {throw new Error('RETURN_INTERRUPT');}};
|
||||
await assert.rejects(manager.execute('void f() {\nplayerMove(1);\n}\n' + main('f();')), /RETURN_INTERRUPT/);
|
||||
assert.strictEqual(manager.currentExecutor, null);
|
||||
assert.strictEqual(highlighted[highlighted.length - 1], null);
|
||||
console.log('PASS real game errors are not mistaken for a return signal');
|
||||
|
||||
console.log(`${cases.length + 6} regression checks passed`);
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
166
scratch-gui/scripts/test-devtool-control.cjs
Normal file
@@ -0,0 +1,166 @@
|
||||
// Run: npm run test:devtool
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const babel = require('@babel/core');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const controlPath = 'src/playground/devtool-control.js';
|
||||
const compile = file => babel.transformFileSync(path.join(root, file), {
|
||||
configFile: false, babelrc: false, presets: ['@babel/preset-env', '@babel/preset-react']
|
||||
}).code;
|
||||
const controlCode = compile(controlPath);
|
||||
const homeCode = compile('src/playground/home.js');
|
||||
let checks = 0;
|
||||
function test(name, action) {
|
||||
action();
|
||||
checks++;
|
||||
console.log(`PASS ${name}`);
|
||||
}
|
||||
|
||||
function page(storage = new Map(), {enabled = true, brokenStorage = false, immediate = false} = {}) {
|
||||
const starts = [];
|
||||
const effects = [];
|
||||
function detector(options) {
|
||||
starts.push(options);
|
||||
// Simulate a library probe firing during initialization with F12 already open.
|
||||
if (immediate) options.ondevtoolopen('already-open');
|
||||
}
|
||||
detector.isSuspend = false;
|
||||
const window = {
|
||||
location: {href: '/editor.html'}, onbeforeunload: () => {},
|
||||
cppCodeStopRun: () => effects.push('stop C++'),
|
||||
loadCppProject: () => effects.push('clear C++'),
|
||||
pythonCodeStopRun: () => effects.push('stop Python'),
|
||||
loadPythonProject: () => effects.push('clear Python'),
|
||||
unityInstance: {Quit: () => effects.push('quit engine')}
|
||||
};
|
||||
const sessionStorage = {
|
||||
getItem(key) {if (brokenStorage) throw new Error('storage denied'); return storage.get(key) || null;},
|
||||
setItem(key, value) {if (brokenStorage) throw new Error('storage denied'); storage.set(key, value);}
|
||||
};
|
||||
const sandbox = {
|
||||
exports: {}, window, sessionStorage,
|
||||
localStorage: {getItem: () => '/index.html'},
|
||||
vm: {
|
||||
stopAll: () => effects.push('stop Scratch'),
|
||||
editingTarget: {blocks: {deleteAllBlocks: () => effects.push('clear Scratch')}},
|
||||
emitWorkspaceUpdate: () => effects.push('update workspace')
|
||||
},
|
||||
require(name) {
|
||||
if (name === 'disable-devtool') return detector;
|
||||
if (name === './config.js') return {disabledev: enabled};
|
||||
if (name === './devtool-control.js') return api;
|
||||
throw new Error(`Unexpected import: ${name}`);
|
||||
}
|
||||
};
|
||||
const context = vm.createContext(sandbox);
|
||||
vm.runInContext(controlCode, context, {filename: controlPath});
|
||||
const api = sandbox.exports;
|
||||
return {api, starts, effects, detector, window, context};
|
||||
}
|
||||
|
||||
// Use the actual options and destructive callback from each editor mode.
|
||||
function stageOptions(file) {
|
||||
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const ast = babel.parseSync(source, {configFile: false, babelrc: false, parserOpts: {plugins: ['jsx']}});
|
||||
let options;
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
assert.notStrictEqual(node.source.value, 'disable-devtool', `${file} bypasses shared control`);
|
||||
}
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'initDisableDevtool') options = node.arguments[0];
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach(visit);
|
||||
else if (value && typeof value === 'object') visit(value);
|
||||
}
|
||||
}
|
||||
visit(ast);
|
||||
assert(options, `${file} must initialize through shared control`);
|
||||
// Also verify the entire JSX file remains compatible with project Babel presets.
|
||||
compile(file);
|
||||
return source.slice(options.start, options.end);
|
||||
}
|
||||
|
||||
const storage = new Map();
|
||||
test('home exposes stopDev and persists it for navigation', () => {
|
||||
const home = page(storage);
|
||||
vm.runInContext(homeCode, home.context);
|
||||
home.window.stopDev();
|
||||
assert.strictEqual(storage.get('stopDev'), '1');
|
||||
assert.strictEqual(home.detector.isSuspend, true);
|
||||
});
|
||||
|
||||
for (const mode of ['stage-unity', 'stage-python-unity', 'stage-cpp-unity']) {
|
||||
const optionSource = stageOptions(`src/components/${mode}/stage-unity.jsx`);
|
||||
test(`${mode}: home -> editor with F12 open does not start detection or clear code`, () => {
|
||||
const editor = page(storage, {immediate: true});
|
||||
const options = vm.runInContext(`(${optionSource})`, editor.context);
|
||||
editor.api.initDisableDevtool(options);
|
||||
assert.strictEqual(editor.starts.length, 0);
|
||||
assert.strictEqual(editor.detector.isSuspend, true);
|
||||
assert.strictEqual(editor.window.location.href, '/editor.html');
|
||||
assert.deepStrictEqual(editor.effects, []);
|
||||
// A refresh or another mode initialization in this tab must stay stopped.
|
||||
editor.api.initDisableDevtool(options);
|
||||
assert.strictEqual(editor.starts.length, 0);
|
||||
});
|
||||
test(`${mode}: stopDev blocks an already queued destructive callback`, () => {
|
||||
const editor = page();
|
||||
const options = vm.runInContext(`(${optionSource})`, editor.context);
|
||||
editor.api.initDisableDevtool(options);
|
||||
assert.strictEqual(editor.starts.length, 1);
|
||||
const callback = editor.starts[0];
|
||||
editor.window.stopDev();
|
||||
assert.strictEqual(callback.ignore(), true);
|
||||
callback.ondevtoolopen('queued');
|
||||
assert.strictEqual(editor.window.location.href, '/editor.html');
|
||||
assert.deepStrictEqual(editor.effects, []);
|
||||
});
|
||||
}
|
||||
|
||||
test('normal session retains default detection and redirect', () => {
|
||||
const normal = page();
|
||||
normal.api.initDisableDevtool();
|
||||
assert.strictEqual(normal.starts.length, 1);
|
||||
assert.strictEqual(normal.starts[0].ignore(), false);
|
||||
normal.starts[0].ondevtoolopen();
|
||||
assert.strictEqual(normal.window.location.href, '/index.html');
|
||||
});
|
||||
test('custom options, ignore and callback arguments remain intact', () => {
|
||||
const normal = page();
|
||||
const seen = [];
|
||||
const next = () => {};
|
||||
normal.api.initDisableDevtool({interval: 123, ignore: () => true, ondevtoolopen: (...args) => seen.push(args)});
|
||||
assert.strictEqual(normal.starts[0].interval, 123);
|
||||
assert.strictEqual(normal.starts[0].ignore(), true);
|
||||
normal.starts[0].ondevtoolopen(7, next);
|
||||
assert.deepStrictEqual(seen, [[7, next]]);
|
||||
});
|
||||
test('disabled configuration does not initialize; stopDev remains available', () => {
|
||||
const development = page(new Map(), {enabled: false});
|
||||
development.api.initDisableDevtool();
|
||||
assert.strictEqual(development.starts.length, 0);
|
||||
assert.strictEqual(typeof development.window.stopDev, 'function');
|
||||
});
|
||||
test('blocked storage still permits immediate stop on current page', () => {
|
||||
const broken = page(new Map(), {brokenStorage: true});
|
||||
broken.api.initDisableDevtool();
|
||||
broken.window.stopDev();
|
||||
assert.strictEqual(broken.detector.isSuspend, true);
|
||||
assert.strictEqual(broken.starts[0].ignore(), true);
|
||||
broken.starts[0].ondevtoolopen();
|
||||
broken.api.initDisableDevtool();
|
||||
assert.strictEqual(broken.starts.length, 1);
|
||||
assert.strictEqual(broken.window.location.href, '/editor.html');
|
||||
});
|
||||
for (const file of ['src/playground/competition.js', 'src/playground/competition_sum.js']) {
|
||||
const optionsSource = stageOptions(file);
|
||||
test(`${file}: same stopDev state is honored`, () => {
|
||||
const competition = page(storage, {immediate: true});
|
||||
competition.api.initDisableDevtool(vm.runInContext(`(${optionsSource})`, competition.context));
|
||||
assert.strictEqual(competition.starts.length, 0);
|
||||
});
|
||||
}
|
||||
console.log(`${checks} devtool regression checks passed`);
|
||||
@@ -0,0 +1,49 @@
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
background: rgba(8, 8, 8, 0.92);
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
transition: opacity 0.28s ease-out, visibility 0.28s ease-out;
|
||||
}
|
||||
|
||||
.overlay.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #f2f2f2;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.barTrack {
|
||||
width: min(72%, 280px);
|
||||
height: 10px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.barFill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4fc3f7 0%, #29b6f6 100%);
|
||||
border-radius: 999px;
|
||||
transition: width 0.25s linear;
|
||||
}
|
||||
|
||||
.percent {
|
||||
color: #e0e0e0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './game-loading-overlay.css';
|
||||
|
||||
const GameLoadingOverlay = ({ visible, progress }) => {
|
||||
const pct = Math.max(0, Math.min(100, Math.round(Number(progress) || 0)));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.overlay}${visible ? '' : ` ${styles.hidden}`}`}
|
||||
aria-hidden={!visible}
|
||||
>
|
||||
<div className={styles.text}>正在加载中</div>
|
||||
<div className={styles.barTrack}>
|
||||
<div className={styles.barFill} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div className={styles.percent}>{pct}%</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GameLoadingOverlay.propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
progress: PropTypes.number
|
||||
};
|
||||
|
||||
GameLoadingOverlay.defaultProps = {
|
||||
visible: true,
|
||||
progress: 0
|
||||
};
|
||||
|
||||
export default GameLoadingOverlay;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const alreadyReady = () => typeof window !== 'undefined' && window.iscreate === 1;
|
||||
|
||||
const useGameLoadProgress = () => {
|
||||
const [progress, setProgress] = useState(() => (alreadyReady() ? 100 : 0));
|
||||
const [visible, setVisible] = useState(() => !alreadyReady());
|
||||
const hidingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return undefined;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (window.iscreate === 1) {
|
||||
if (!hidingRef.current) {
|
||||
hidingRef.current = true;
|
||||
setProgress(100);
|
||||
setTimeout(() => setVisible(false), 280);
|
||||
}
|
||||
clearInterval(timer);
|
||||
return;
|
||||
}
|
||||
setProgress(prev => (prev >= 90 && prev < 99 ? prev + 1 : prev));
|
||||
}, 300);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [visible]);
|
||||
|
||||
const onUnityProgress = useCallback(unityProgress => {
|
||||
const mapped = Math.round(Math.min(90, Number(unityProgress) * 90));
|
||||
if (!Number.isFinite(mapped)) return;
|
||||
setProgress(prev => (mapped > prev ? mapped : prev));
|
||||
}, []);
|
||||
|
||||
const onUnityInstanceCreated = useCallback(() => {
|
||||
setProgress(prev => Math.max(prev, 90));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
progress,
|
||||
visible,
|
||||
onUnityProgress,
|
||||
onUnityInstanceCreated
|
||||
};
|
||||
};
|
||||
|
||||
export default useGameLoadProgress;
|
||||
@@ -852,7 +852,7 @@ class MenuBar extends React.Component {
|
||||
let realLvId_tmp = '';
|
||||
// 发送消息给 Unity 实例,切换关卡
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv;
|
||||
realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv;
|
||||
} else {
|
||||
realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber);
|
||||
}
|
||||
@@ -902,7 +902,8 @@ class MenuBar extends React.Component {
|
||||
window.gamelvdataJson.gamelv,
|
||||
window.gamelvdataJson.gameid,
|
||||
window.gamelvdataJson.ismatch,
|
||||
window.gamelvdataJson.gameNumber
|
||||
window.gamelvdataJson.gameNumber,
|
||||
window.gamelvdataJson.gamemode
|
||||
) + (window.gamelvdata ? window.gamelvdata.currentlv : '');
|
||||
} else {
|
||||
realLvId_tmp = CONFIG.getRealLvId(
|
||||
|
||||
@@ -148,7 +148,7 @@ const CurrentLevelChoose = ({ onCancel }) => {
|
||||
console.warn('window.loadCppProject 未定义或不是函数');
|
||||
}
|
||||
}
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber);
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode);
|
||||
const realLvId = realLvId_tmp + cellIndex
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
|
||||
@@ -14,6 +14,8 @@ window.CONFIG = CONFIG;
|
||||
import { initDisableDevtool } from '../../playground/devtool-control.js';
|
||||
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
|
||||
import { debug } from 'scratch-vm/src/util/log.js';
|
||||
import GameLoadingOverlay from '../game-loading-overlay/game-loading-overlay.jsx';
|
||||
import useGameLoadProgress from '../game-loading-overlay/use-game-load-progress.js';
|
||||
|
||||
const MAX_LEVELS = 500; // 最大关卡数据数量
|
||||
const STORAGE_KEY = 'player_levels_data'; // 使用命名空间,防止键名冲突
|
||||
@@ -47,6 +49,16 @@ const UnityComponent = function (props) {
|
||||
const [getBlockNum, setBlockNum] = useState(0);
|
||||
const [testDal, setTestDal] = useState(-1);
|
||||
const canvasRef = useRef(null);
|
||||
const {
|
||||
progress: gameLoadProgress,
|
||||
visible: gameLoadVisible,
|
||||
onUnityProgress,
|
||||
onUnityInstanceCreated
|
||||
} = useGameLoadProgress();
|
||||
const onUnityProgressRef = useRef(onUnityProgress);
|
||||
const onUnityInstanceCreatedRef = useRef(onUnityInstanceCreated);
|
||||
onUnityProgressRef.current = onUnityProgress;
|
||||
onUnityInstanceCreatedRef.current = onUnityInstanceCreated;
|
||||
const [isGameOverVisible, setIsGameOverVisible] = useState(false);
|
||||
const [isGameOkVisible, setIsGameOkVisible] = useState(false);
|
||||
const [isLockNextButton, setIsLockNextButton] = useState(true);
|
||||
@@ -547,29 +559,39 @@ const UnityComponent = function (props) {
|
||||
const [hasAlerted, setHasAlerted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
if (CONFIG.disabledev) {
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
const originalOnBeforeUnload = window.onbeforeunload;
|
||||
window.onbeforeunload = null;
|
||||
window.cppCodeStopRun();
|
||||
window.loadCppProject("");
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
const originalOnBeforeUnload = window.onbeforeunload;
|
||||
window.onbeforeunload = null;
|
||||
window.cppCodeStopRun();
|
||||
window.loadCppProject("");
|
||||
|
||||
// 销毁 Unity 实例
|
||||
if (window.unityInstance) {
|
||||
window.unityInstance.Quit().then(() => {
|
||||
window.unityInstance = null;
|
||||
});
|
||||
}
|
||||
// 销毁游戏实例(Cocos 兼容:Quit 可能不存在)
|
||||
if (window.unityInstance) {
|
||||
const quit = window.unityInstance.Quit;
|
||||
if (typeof quit === 'function') {
|
||||
Promise.resolve(quit.call(window.unityInstance)).then(() => {
|
||||
window.unityInstance = null;
|
||||
});
|
||||
} else {
|
||||
window.unityInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟跳转到目标页面,确保销毁
|
||||
window.location.href = localStorage.getItem('returnUrl');
|
||||
// window.location.href = '/matchsum.html';
|
||||
// 恢复原始的 onbeforeunload 监听器
|
||||
window.onbeforeunload = originalOnBeforeUnload;
|
||||
}
|
||||
});
|
||||
// 延迟跳转到目标页面,确保销毁
|
||||
window.location.href = localStorage.getItem('returnUrl');
|
||||
// window.location.href = '/matchsum.html';
|
||||
// 恢复原始的 onbeforeunload 监听器
|
||||
window.onbeforeunload = originalOnBeforeUnload;
|
||||
},
|
||||
interval: 500, // 检测间隔时间
|
||||
clearLog: true, // 清除控制台的日志
|
||||
disableMenu: true, // 禁用右键菜单
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -583,7 +605,7 @@ const UnityComponent = function (props) {
|
||||
//计算真实关卡位置
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
}
|
||||
@@ -693,7 +715,7 @@ const UnityComponent = function (props) {
|
||||
window.iscreate = 1;
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber, gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber)
|
||||
}
|
||||
@@ -710,7 +732,8 @@ const UnityComponent = function (props) {
|
||||
// 延迟 500 毫秒后切换关卡,只执行一次
|
||||
setTimeout(() => {
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
} else {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
@@ -769,7 +792,8 @@ const UnityComponent = function (props) {
|
||||
|
||||
const savedProject = window.cppCode;
|
||||
setIsLockNextButton(true);
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
const trialCount = localStorage.getItem('trial_count');
|
||||
|
||||
if (localStorage.getItem('permission') === "0" && (realLvId - CONFIG.BEGINNING_REAL_LVID) >= trialCount && currentlv > 9) {
|
||||
@@ -1152,8 +1176,9 @@ const UnityComponent = function (props) {
|
||||
} else {
|
||||
setIsNextButtonComplete(false);
|
||||
}
|
||||
|
||||
window.gamelvdata.currentlv = window.gamelvdata.currentlv + 1
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
@@ -1210,7 +1235,8 @@ const UnityComponent = function (props) {
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
console.log("Retrying the current level...");
|
||||
};
|
||||
@@ -1218,46 +1244,87 @@ const UnityComponent = function (props) {
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
let cancelled = false;
|
||||
|
||||
const bootUnity = async () => {
|
||||
const config = await getUnityPlayerConfig();
|
||||
await loadUnityLoaderScript();
|
||||
if (cancelled || !canvas) {
|
||||
return;
|
||||
}
|
||||
// if (canvas && !window.unityInstance) {
|
||||
const unityUrl = "/unity";
|
||||
const buildUrl = "/unity/Build";
|
||||
const cdnRoot = CONFIG.usecdn ? getUnityCdnRoot() : unityUrl;
|
||||
const cdnBuildRoot = CONFIG.usecdn ? getUnityCdnBuildRoot() : buildUrl;
|
||||
let loaderUrl = CONFIG.usecdn
|
||||
? `${cdnBuildRoot}/mstest5.loader.js`
|
||||
: `${buildUrl}/mstest5.loader.js`;
|
||||
let config = {
|
||||
dataUrl: buildUrl + "/mstest5.data.br",
|
||||
frameworkUrl: buildUrl + "/mstest5.framework.js.br",
|
||||
codeUrl: buildUrl + "/mstest5.wasm.br",
|
||||
streamingAssetsUrl: unityUrl + "/StreamingAssets",
|
||||
companyName: "DefaultCompany",
|
||||
productName: "Web A",
|
||||
productVersion: "1.0",
|
||||
};
|
||||
if (CONFIG.usecdn) {
|
||||
config = {
|
||||
dataUrl: cdnBuildRoot + "/mstest5.data.br",
|
||||
frameworkUrl: cdnBuildRoot + "/mstest5.framework.js.br",
|
||||
codeUrl: cdnBuildRoot + "/mstest5.wasm.br",
|
||||
streamingAssetsUrl: cdnRoot + "/StreamingAssets",
|
||||
// dataUrl: buildUrl + "/Build.data.br",
|
||||
// frameworkUrl: buildUrl + "/Build.framework.js.br",
|
||||
// codeUrl: buildUrl + "/Build.wasm.br",
|
||||
// streamingAssetsUrl: unityUrl + "/StreamingAssets",
|
||||
companyName: "DefaultCompany",
|
||||
productName: "Web A",
|
||||
productVersion: "1.0",
|
||||
};
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = loaderUrl;
|
||||
|
||||
script.onload = () => {
|
||||
window.iscreate = 0;
|
||||
const unityInstance = await createUnityInstance(canvas, config, () => {});
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
window.unityInstance = unityInstance;
|
||||
window.waitForUnityStepFinish = waitForUnityStepFinish;
|
||||
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
|
||||
window.sleepForUnityStepFinish = sleepForUnityStepFinish;
|
||||
window.sleepByStepForUnityStepFinish = sleepByStepForUnityStepFinish;
|
||||
console.log("Unity 实例创建成功");
|
||||
window.processData = processData;
|
||||
window.processVehicleData = processVehicleData;
|
||||
window.externalResult = externalResult;
|
||||
window.externalLevelInfo = externalLevelInfo;
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const boot = () => {
|
||||
createUnityInstance(canvas, config, (progress) => {
|
||||
onUnityProgressRef.current(progress);
|
||||
}).then((unityInstance) => {
|
||||
onUnityInstanceCreatedRef.current();
|
||||
window.unityInstance = unityInstance;
|
||||
window.waitForUnityStepFinish = waitForUnityStepFinish;
|
||||
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
|
||||
window.sleepForUnityStepFinish = sleepForUnityStepFinish;
|
||||
window.sleepByStepForUnityStepFinish = sleepByStepForUnityStepFinish;
|
||||
console.log("Unity 实例创建成功");
|
||||
window.processData = processData;
|
||||
window.processVehicleData = processVehicleData;
|
||||
window.externalResult = externalResult;
|
||||
window.externalLevelInfo = externalLevelInfo;
|
||||
//计算真实关卡位置
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
}
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
}).catch((message) => {
|
||||
console.error(`Unity 实例创建失败: ${message}`);
|
||||
});
|
||||
};
|
||||
if (CONFIG.usecdn) {
|
||||
prefetchLevelsDatabase(cdnRoot).then(boot).catch((err) => {
|
||||
console.error('[unity-cdn] 关卡库预取失败', err);
|
||||
boot();
|
||||
});
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
boot();
|
||||
}
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
};
|
||||
|
||||
bootUnity().catch((message) => {
|
||||
console.error(`Unity 实例创建失败: ${message}`);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
// } else {
|
||||
// console.log("Unity 实例已经存在,跳过初始化");
|
||||
// }
|
||||
}, []);
|
||||
|
||||
// 修改 formatDateTime 函数
|
||||
@@ -1284,6 +1351,7 @@ const UnityComponent = function (props) {
|
||||
}}
|
||||
>
|
||||
<canvas id="unity-canvas" ref={canvasRef} width="960" height="600" style={{ width: '100%', height: '100%' }}></canvas>
|
||||
<GameLoadingOverlay visible={gameLoadVisible} progress={gameLoadProgress} />
|
||||
|
||||
|
||||
|
||||
@@ -1837,4 +1905,4 @@ UnityComponent.propTypes = {
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
export default UnityComponent;
|
||||
export default UnityComponent;
|
||||
|
||||
@@ -32,11 +32,16 @@ const StageWrapperCppClang = () => {
|
||||
const [wasmModule, setWasmModule] = useState(null);
|
||||
const [syntaxErrorModalVisible, setSyntaxErrorModalVisible] = useState(false);
|
||||
const [syntaxErrors, setSyntaxErrors] = useState([]);
|
||||
const [overlayActive, setOverlayActive] = useState(false);
|
||||
const [apiSelectedCategory, setApiSelectedCategory] = useState('syntax');
|
||||
const [apiSelectedCategory, setApiSelectedCategory] = useState('player');
|
||||
const [isApiPanelReady, setIsApiPanelReady] = useState(() => window.iscreate === 1);
|
||||
const [overlayActive, setOverlayActive] = useState(false);
|
||||
const MAX_CODE_LINES = 500;
|
||||
|
||||
// CodeMirror编辑器引用
|
||||
const editorRef = useRef(null);
|
||||
const lastValidCppCodeRef = useRef(cppCode);
|
||||
|
||||
// Unity 加载完成前,API 面板显示加载态并屏蔽点击(5 秒兜底自动解除)
|
||||
// 游戏引擎加载完成前,API 面板显示加载态并屏蔽点击(5 秒兜底自动解除)
|
||||
useEffect(() => {
|
||||
if (isApiPanelReady) return undefined;
|
||||
const timer = setInterval(() => {
|
||||
@@ -54,11 +59,6 @@ const StageWrapperCppClang = () => {
|
||||
clearTimeout(fallback);
|
||||
};
|
||||
}, [isApiPanelReady]);
|
||||
const MAX_CODE_LINES = 500;
|
||||
|
||||
// CodeMirror编辑器引用
|
||||
const editorRef = useRef(null);
|
||||
const lastValidCppCodeRef = useRef(cppCode);
|
||||
|
||||
// API参考数据
|
||||
const apiReference = {
|
||||
@@ -654,7 +654,7 @@ const StageWrapperCppClang = () => {
|
||||
if (window.unityInstance) {
|
||||
window.unityInstance.SendMessage("UIMain", "SetText", window.cmd_print_text);
|
||||
}
|
||||
|
||||
|
||||
setCompilationOutput(prev => prev + textStr + "\n");
|
||||
},
|
||||
|
||||
@@ -1112,7 +1112,7 @@ const StageWrapperCppClang = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// API参考面板
|
||||
// API参考面板(与 Python 一样常驻在编辑区左侧)
|
||||
const ApiDrawer = () => {
|
||||
const selectedCategory = apiSelectedCategory;
|
||||
const setSelectedCategory = setApiSelectedCategory;
|
||||
@@ -1120,12 +1120,10 @@ const StageWrapperCppClang = () => {
|
||||
return (
|
||||
<div className={styles.apiDrawer}>
|
||||
<div className={styles.apiDrawerHeader}>
|
||||
<h3>🔧 API参考</h3>
|
||||
<h3>🔧 C++ API参考</h3>
|
||||
</div>
|
||||
|
||||
{/* 抽屉内容 */}
|
||||
<div className={styles.apiDrawerContent}>
|
||||
{/* 分类导航 */}
|
||||
<div className={styles.apiDrawerCategories}>
|
||||
{Object.entries(apiReference).map(([key, category]) => (
|
||||
<button
|
||||
@@ -1139,7 +1137,6 @@ const StageWrapperCppClang = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* API列表 */}
|
||||
<div className={styles.apiDrawerList}>
|
||||
{apiReference[selectedCategory]?.apis.map((api, index) => (
|
||||
<div key={index} className={styles.apiDrawerItem}>
|
||||
@@ -1163,12 +1160,13 @@ const StageWrapperCppClang = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{!isApiPanelReady && (
|
||||
<div className={styles.apiDrawerLoading}>
|
||||
<div className={styles.apiDrawerLoadingSpinner}></div>
|
||||
<div className={styles.apiDrawerLoadingText}>游戏引擎加载中,完成后即可插入代码…</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${styles.apiDrawerLoading}${isApiPanelReady ? ` ${styles.apiDrawerLoadingHidden}` : ''}`}
|
||||
aria-hidden={isApiPanelReady}
|
||||
>
|
||||
<div className={styles.apiDrawerLoadingSpinner}></div>
|
||||
<div className={styles.apiDrawerLoadingText}>正在加载中,完成后即可插入代码…</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1189,6 +1187,12 @@ const StageWrapperCppClang = () => {
|
||||
// 移除snippet标记,转换为普通文本
|
||||
insertCode = code.replace(/#{([^}]*)}/g, '$1');
|
||||
}
|
||||
if (cursor > 0 && view.state.doc.sliceString(cursor - 1, cursor) !== '\n') {
|
||||
insertCode = '\n' + insertCode;
|
||||
}
|
||||
if (!insertCode.endsWith('\n')) {
|
||||
insertCode += '\n';
|
||||
}
|
||||
|
||||
// 插入代码
|
||||
view.dispatch({
|
||||
@@ -1202,7 +1206,6 @@ const StageWrapperCppClang = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 聚焦编辑器
|
||||
view.focus();
|
||||
}
|
||||
}
|
||||
@@ -1213,144 +1216,141 @@ const StageWrapperCppClang = () => {
|
||||
<ErrorModal />
|
||||
<CompilationModal />
|
||||
<SyntaxErrorModal />
|
||||
<div
|
||||
className={styles.editorWithApi}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ApiDrawer />
|
||||
<div className={styles.editorPane}>
|
||||
<CodeMirror
|
||||
ref={editorRef}
|
||||
<div className={styles.editorWithApi}>
|
||||
{ApiDrawer()}
|
||||
<div
|
||||
className={styles.editorPane}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CodeMirror
|
||||
ref={editorRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
backgroundColor: '#010101',
|
||||
overflow: 'auto',
|
||||
fontSize: '20px'
|
||||
}}
|
||||
value={cppCode}
|
||||
theme={abcdef}
|
||||
extensions={[
|
||||
cpp(),
|
||||
createCppCompletions(),
|
||||
keymap.of(completionKeymap),
|
||||
keymap.of(snippetKeymap),
|
||||
classname({
|
||||
add: (lineNumber) => {
|
||||
if (lineNumber === highlightedLine) {
|
||||
return 'highlighted-line';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
},
|
||||
'.cm-editor': {
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
},
|
||||
'.cm-scroller': {
|
||||
height: '100%',
|
||||
overflow: 'auto'
|
||||
},
|
||||
'.cm-content': {
|
||||
minHeight: '100%',
|
||||
padding: '10px'
|
||||
},
|
||||
'.highlighted-line': {
|
||||
backgroundColor: '#ffeb3b80',
|
||||
borderLeft: '4px solid #ffeb3b',
|
||||
borderRight: '2px solid #ffeb3b',
|
||||
boxShadow: '0 0 10px rgba(255, 235, 59, 0.3)',
|
||||
animation: 'pulse 1s ease-in-out'
|
||||
},
|
||||
'@keyframes pulse': {
|
||||
'0%': { backgroundColor: '#ffeb3b80' },
|
||||
'50%': { backgroundColor: '#ffeb3ba0' },
|
||||
'100%': { backgroundColor: '#ffeb3b80' }
|
||||
},
|
||||
fontSize: "20px"
|
||||
})
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: true,
|
||||
autocompletion: true
|
||||
}}
|
||||
onChange={(value) => {
|
||||
const lines = value.split(/\r\n|\r|\n/);
|
||||
if (lines.length > MAX_CODE_LINES) {
|
||||
setErrorMessage(`代码不能超过${MAX_CODE_LINES}行!当前已有${lines.length}行,请删除多余代码后再编辑。`);
|
||||
setErrorModalVisible(true);
|
||||
if (editorRef.current && editorRef.current.view) {
|
||||
const view = editorRef.current.view;
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: lastValidCppCodeRef.current
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastValidCppCodeRef.current = value;
|
||||
setCppCode(value);
|
||||
updateBlockCount(value);
|
||||
}}
|
||||
/>
|
||||
{overlayActive && (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: '#010101',
|
||||
fontSize: '20px'
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.25)',
|
||||
cursor: 'not-allowed',
|
||||
zIndex: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
value={cppCode}
|
||||
theme={abcdef}
|
||||
extensions={[
|
||||
cpp(),
|
||||
createCppCompletions(),
|
||||
keymap.of(completionKeymap),
|
||||
keymap.of(snippetKeymap),
|
||||
classname({
|
||||
add: (lineNumber) => {
|
||||
if (lineNumber === highlightedLine) {
|
||||
return 'highlighted-line';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
},
|
||||
'.cm-editor': {
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
},
|
||||
'.cm-scroller': {
|
||||
height: '100%',
|
||||
overflow: 'auto'
|
||||
},
|
||||
'.cm-content': {
|
||||
minHeight: '100%',
|
||||
padding: '10px'
|
||||
},
|
||||
'.highlighted-line': {
|
||||
backgroundColor: '#ffeb3b80',
|
||||
borderLeft: '4px solid #ffeb3b',
|
||||
borderRight: '2px solid #ffeb3b',
|
||||
boxShadow: '0 0 10px rgba(255, 235, 59, 0.3)',
|
||||
animation: 'pulse 1s ease-in-out'
|
||||
},
|
||||
'@keyframes pulse': {
|
||||
'0%': { backgroundColor: '#ffeb3b80' },
|
||||
'50%': { backgroundColor: '#ffeb3ba0' },
|
||||
'100%': { backgroundColor: '#ffeb3b80' }
|
||||
},
|
||||
fontSize: "20px"
|
||||
})
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: true,
|
||||
autocompletion: true
|
||||
}}
|
||||
onChange={(value) => {
|
||||
const lines = value.split(/\r\n|\r|\n/);
|
||||
if (lines.length > MAX_CODE_LINES) {
|
||||
setErrorMessage(`代码不能超过${MAX_CODE_LINES}行!当前已有${lines.length}行,请删除多余代码后再编辑。`);
|
||||
setErrorModalVisible(true);
|
||||
// 使用 CodeMirror dispatch 直接回滚编辑器内容
|
||||
if (editorRef.current && editorRef.current.view) {
|
||||
const view = editorRef.current.view;
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: lastValidCppCodeRef.current
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastValidCppCodeRef.current = value;
|
||||
setCppCode(value);
|
||||
// 计算代码行数并更新window.blockcount
|
||||
updateBlockCount(value);
|
||||
}}
|
||||
/>
|
||||
{overlayActive && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.25)',
|
||||
cursor: 'not-allowed',
|
||||
zIndex: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
onWheel={(e) => e.preventDefault()}
|
||||
onTouchMove={(e) => e.preventDefault()}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div style={{
|
||||
color: '#fff',
|
||||
fontSize: '28px',
|
||||
fontWeight: 'bold',
|
||||
textShadow: '0 2px 8px rgba(0,0,0,0.6)',
|
||||
letterSpacing: '4px',
|
||||
animation: 'blink 1.5s ease-in-out infinite'
|
||||
}}>
|
||||
⏳ 执行中...
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
`}</style>
|
||||
onWheel={(e) => e.preventDefault()}
|
||||
onTouchMove={(e) => e.preventDefault()}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div style={{
|
||||
color: '#fff',
|
||||
fontSize: '28px',
|
||||
fontWeight: 'bold',
|
||||
textShadow: '0 2px 8px rgba(0,0,0,0.6)',
|
||||
letterSpacing: '4px',
|
||||
animation: 'blink 1.5s ease-in-out infinite'
|
||||
}}>
|
||||
⏳ 执行中...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StageWrapperCppClang;
|
||||
export default StageWrapperCppClang;
|
||||
|
||||
@@ -378,7 +378,7 @@
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* API参考 + 编辑器并排:参考在左,编辑器靠右 */
|
||||
/* API参考 + 编辑器并排:参考在左,编辑器靠右(与 Python 一致) */
|
||||
.editorWithApi {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -392,9 +392,118 @@
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* API抽屉标签样式 */
|
||||
.apiDrawerTab {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
width: 45px;
|
||||
height: 90px;
|
||||
background: hsla(215, 100%, 65%, 1);
|
||||
border: 1px solid hsla(215, 100%, 65%, 1);
|
||||
border-right: none;
|
||||
border-radius: 12px 0 0 12px;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.25);
|
||||
color: #FFFFFF;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.apiDrawerTab:hover {
|
||||
background: hsla(215, 100%, 65%, 1);
|
||||
transform: translateY(-50%) translateX(-3px);
|
||||
box-shadow: -6px 0 18px rgba(0, 0, 0, 0.35);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.apiDrawerTab.apiDrawerTabOpen {
|
||||
background: linear-gradient(135deg, #D35400 0%, #E67E22 100%);
|
||||
border-color: #E67E22;
|
||||
right: 20vw;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.apiDrawerTab.apiDrawerTabOpen:hover {
|
||||
background: linear-gradient(135deg, #E67E22 0%, #F39C12 100%);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.apiDrawerTabIcon {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
color: #FFFFFF;
|
||||
opacity: 0.9;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.apiDrawerTab:hover .apiDrawerTabIcon {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.apiDrawerTabText {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
writing-mode: vertical-lr;
|
||||
text-orientation: mixed;
|
||||
line-height: 1;
|
||||
color: #FFFFFF;
|
||||
opacity: 0.8;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.apiDrawerTab:hover .apiDrawerTabText {
|
||||
opacity: 1;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* 响应式抽屉标签 */
|
||||
@media (max-width: 1200px) {
|
||||
.apiDrawerTab.apiDrawerTabOpen {
|
||||
right: 25vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.apiDrawerTab.apiDrawerTabOpen {
|
||||
right: 35vw;
|
||||
}
|
||||
|
||||
.apiDrawerTab {
|
||||
width: 35px;
|
||||
height: 75px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.apiDrawerTabIcon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.apiDrawerTabText {
|
||||
font-size: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.apiDrawerTab:hover .apiDrawerTabText {
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* API面板样式 */
|
||||
.apiDrawer {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
@@ -422,6 +531,10 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.apiDrawerLoadingHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.apiDrawerLoadingSpinner {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
@@ -429,6 +542,7 @@
|
||||
border-top-color: #ba68c8;
|
||||
border-radius: 50%;
|
||||
animation: apiDrawerLoadingSpin 0.9s linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.apiDrawerLoadingText {
|
||||
@@ -440,6 +554,9 @@
|
||||
}
|
||||
|
||||
@keyframes apiDrawerLoadingSpin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ const CurrentLevelChoose = ({ onCancel }) => {
|
||||
// 清除所有代码
|
||||
window.loadPythonProject(`` + `\n`.repeat(50));
|
||||
}
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber);
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode);
|
||||
const realLvId = realLvId_tmp + cellIndex
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
|
||||
@@ -14,6 +14,8 @@ window.CONFIG = CONFIG;
|
||||
import { initDisableDevtool } from '../../playground/devtool-control.js';
|
||||
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
|
||||
import { debug } from 'scratch-vm/src/util/log.js';
|
||||
import GameLoadingOverlay from '../game-loading-overlay/game-loading-overlay.jsx';
|
||||
import useGameLoadProgress from '../game-loading-overlay/use-game-load-progress.js';
|
||||
|
||||
const MAX_LEVELS = 500; // 最大关卡数据数量
|
||||
const STORAGE_KEY = 'player_levels_data'; // 使用命名空间,防止键名冲突
|
||||
@@ -47,6 +49,16 @@ const UnityComponent = function (props) {
|
||||
const [getBlockNum, setBlockNum] = useState(0);
|
||||
const [testDal, setTestDal] = useState(-1);
|
||||
const canvasRef = useRef(null);
|
||||
const {
|
||||
progress: gameLoadProgress,
|
||||
visible: gameLoadVisible,
|
||||
onUnityProgress,
|
||||
onUnityInstanceCreated
|
||||
} = useGameLoadProgress();
|
||||
const onUnityProgressRef = useRef(onUnityProgress);
|
||||
const onUnityInstanceCreatedRef = useRef(onUnityInstanceCreated);
|
||||
onUnityProgressRef.current = onUnityProgress;
|
||||
onUnityInstanceCreatedRef.current = onUnityInstanceCreated;
|
||||
const [isGameOverVisible, setIsGameOverVisible] = useState(false);
|
||||
const [isGameOkVisible, setIsGameOkVisible] = useState(false);
|
||||
const [isLockNextButton, setIsLockNextButton] = useState(true);
|
||||
@@ -547,29 +559,39 @@ const UnityComponent = function (props) {
|
||||
const [hasAlerted, setHasAlerted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
if (CONFIG.disabledev) {
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
const originalOnBeforeUnload = window.onbeforeunload;
|
||||
window.onbeforeunload = null;
|
||||
window.pythonCodeStopRun();
|
||||
window.loadPythonProject("");
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
const originalOnBeforeUnload = window.onbeforeunload;
|
||||
window.onbeforeunload = null;
|
||||
window.pythonCodeStopRun();
|
||||
window.loadPythonProject("");
|
||||
|
||||
// 销毁 Unity 实例
|
||||
if (window.unityInstance) {
|
||||
window.unityInstance.Quit().then(() => {
|
||||
window.unityInstance = null;
|
||||
});
|
||||
}
|
||||
// 销毁游戏实例(Cocos 兼容:Quit 可能不存在)
|
||||
if (window.unityInstance) {
|
||||
const quit = window.unityInstance.Quit;
|
||||
if (typeof quit === 'function') {
|
||||
Promise.resolve(quit.call(window.unityInstance)).then(() => {
|
||||
window.unityInstance = null;
|
||||
});
|
||||
} else {
|
||||
window.unityInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟跳转到目标页面,确保销毁
|
||||
window.location.href = localStorage.getItem('returnUrl');
|
||||
// window.location.href = '/matchsum.html';
|
||||
// 恢复原始的 onbeforeunload 监听器
|
||||
window.onbeforeunload = originalOnBeforeUnload;
|
||||
}
|
||||
});
|
||||
// 延迟跳转到目标页面,确保销毁
|
||||
window.location.href = localStorage.getItem('returnUrl');
|
||||
// window.location.href = '/matchsum.html';
|
||||
// 恢复原始的 onbeforeunload 监听器
|
||||
window.onbeforeunload = originalOnBeforeUnload;
|
||||
},
|
||||
interval: 500, // 检测间隔时间
|
||||
clearLog: true, // 清除控制台的日志
|
||||
disableMenu: true, // 禁用右键菜单
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -583,7 +605,7 @@ const UnityComponent = function (props) {
|
||||
//计算真实关卡位置
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
}
|
||||
@@ -693,7 +715,7 @@ const UnityComponent = function (props) {
|
||||
window.iscreate = 1;
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber, gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber)
|
||||
}
|
||||
@@ -710,7 +732,7 @@ const UnityComponent = function (props) {
|
||||
// 延迟 500 毫秒后切换关卡,只执行一次
|
||||
setTimeout(() => {
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
} else {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
@@ -767,7 +789,7 @@ const UnityComponent = function (props) {
|
||||
const savedProject = window.pythonCode;
|
||||
setIsLockNextButton(true);
|
||||
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
const trialCount = localStorage.getItem('trial_count');
|
||||
|
||||
if (localStorage.getItem('permission') === "0" && (realLvId - CONFIG.BEGINNING_REAL_LVID) >= trialCount && currentlv > 9) {
|
||||
@@ -1147,7 +1169,7 @@ const UnityComponent = function (props) {
|
||||
setIsNextButtonComplete(false);
|
||||
}
|
||||
window.gamelvdata.currentlv = window.gamelvdata.currentlv + 1
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
@@ -1199,7 +1221,7 @@ const UnityComponent = function (props) {
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
console.log("Retrying the current level...");
|
||||
};
|
||||
@@ -1207,46 +1229,87 @@ const UnityComponent = function (props) {
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
let cancelled = false;
|
||||
|
||||
const bootUnity = async () => {
|
||||
const config = await getUnityPlayerConfig();
|
||||
await loadUnityLoaderScript();
|
||||
if (cancelled || !canvas) {
|
||||
return;
|
||||
}
|
||||
// if (canvas && !window.unityInstance) {
|
||||
const unityUrl = "/unity";
|
||||
const buildUrl = "/unity/Build";
|
||||
const cdnRoot = CONFIG.usecdn ? getUnityCdnRoot() : unityUrl;
|
||||
const cdnBuildRoot = CONFIG.usecdn ? getUnityCdnBuildRoot() : buildUrl;
|
||||
let loaderUrl = CONFIG.usecdn
|
||||
? `${cdnBuildRoot}/mstest5.loader.js`
|
||||
: `${buildUrl}/mstest5.loader.js`;
|
||||
let config = {
|
||||
dataUrl: buildUrl + "/mstest5.data.br",
|
||||
frameworkUrl: buildUrl + "/mstest5.framework.js.br",
|
||||
codeUrl: buildUrl + "/mstest5.wasm.br",
|
||||
streamingAssetsUrl: unityUrl + "/StreamingAssets",
|
||||
companyName: "DefaultCompany",
|
||||
productName: "Web A",
|
||||
productVersion: "1.0",
|
||||
};
|
||||
if (CONFIG.usecdn) {
|
||||
config = {
|
||||
dataUrl: cdnBuildRoot + "/mstest5.data.br",
|
||||
frameworkUrl: cdnBuildRoot + "/mstest5.framework.js.br",
|
||||
codeUrl: cdnBuildRoot + "/mstest5.wasm.br",
|
||||
streamingAssetsUrl: cdnRoot + "/StreamingAssets",
|
||||
// dataUrl: buildUrl + "/Build.data.br",
|
||||
// frameworkUrl: buildUrl + "/Build.framework.js.br",
|
||||
// codeUrl: buildUrl + "/Build.wasm.br",
|
||||
// streamingAssetsUrl: unityUrl + "/StreamingAssets",
|
||||
companyName: "DefaultCompany",
|
||||
productName: "Web A",
|
||||
productVersion: "1.0",
|
||||
};
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = loaderUrl;
|
||||
|
||||
script.onload = () => {
|
||||
window.iscreate = 0;
|
||||
const unityInstance = await createUnityInstance(canvas, config, () => {});
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
window.unityInstance = unityInstance;
|
||||
window.waitForUnityStepFinish = waitForUnityStepFinish;
|
||||
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
|
||||
window.sleepForUnityStepFinish = sleepForUnityStepFinish;
|
||||
window.sleepByStepForUnityStepFinish = sleepByStepForUnityStepFinish;
|
||||
console.log("Unity 实例创建成功");
|
||||
window.processData = processData;
|
||||
window.processVehicleData = processVehicleData;
|
||||
window.externalResult = externalResult;
|
||||
window.externalLevelInfo = externalLevelInfo;
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const boot = () => {
|
||||
createUnityInstance(canvas, config, (progress) => {
|
||||
onUnityProgressRef.current(progress);
|
||||
}).then((unityInstance) => {
|
||||
onUnityInstanceCreatedRef.current();
|
||||
window.unityInstance = unityInstance;
|
||||
window.waitForUnityStepFinish = waitForUnityStepFinish;
|
||||
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
|
||||
window.sleepForUnityStepFinish = sleepForUnityStepFinish;
|
||||
window.sleepByStepForUnityStepFinish = sleepByStepForUnityStepFinish;
|
||||
console.log("Unity 实例创建成功");
|
||||
window.processData = processData;
|
||||
window.processVehicleData = processVehicleData;
|
||||
window.externalResult = externalResult;
|
||||
window.externalLevelInfo = externalLevelInfo;
|
||||
//计算真实关卡位置
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
}
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
}).catch((message) => {
|
||||
console.error(`Unity 实例创建失败: ${message}`);
|
||||
});
|
||||
};
|
||||
if (CONFIG.usecdn) {
|
||||
prefetchLevelsDatabase(cdnRoot).then(boot).catch((err) => {
|
||||
console.error('[unity-cdn] 关卡库预取失败', err);
|
||||
boot();
|
||||
});
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
boot();
|
||||
}
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
};
|
||||
|
||||
bootUnity().catch((message) => {
|
||||
console.error(`Unity 实例创建失败: ${message}`);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
// } else {
|
||||
// console.log("Unity 实例已经存在,跳过初始化");
|
||||
// }
|
||||
}, []);
|
||||
|
||||
// 修改 formatDateTime 函数
|
||||
@@ -1273,6 +1336,7 @@ const UnityComponent = function (props) {
|
||||
}}
|
||||
>
|
||||
<canvas id="unity-canvas" ref={canvasRef} width="960" height="600" style={{ width: '100%', height: '100%' }}></canvas>
|
||||
<GameLoadingOverlay visible={gameLoadVisible} progress={gameLoadProgress} />
|
||||
|
||||
|
||||
|
||||
@@ -1827,4 +1891,4 @@ UnityComponent.propTypes = {
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
export default UnityComponent;
|
||||
export default UnityComponent;
|
||||
|
||||
@@ -183,6 +183,10 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.apiDrawerLoadingHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.apiDrawerLoadingSpinner {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
@@ -190,6 +194,7 @@
|
||||
border-top-color: #ba68c8;
|
||||
border-radius: 50%;
|
||||
animation: apiDrawerLoadingSpin 0.9s linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.apiDrawerLoadingText {
|
||||
@@ -201,6 +206,9 @@
|
||||
}
|
||||
|
||||
@keyframes apiDrawerLoadingSpin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
@@ -1287,12 +1287,13 @@ sys.settrace(trace_func)`;
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{!isApiPanelReady && (
|
||||
<div className={styles.apiDrawerLoading}>
|
||||
<div className={styles.apiDrawerLoadingSpinner}></div>
|
||||
<div className={styles.apiDrawerLoadingText}>游戏引擎加载中,完成后即可插入代码…</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${styles.apiDrawerLoading}${isApiPanelReady ? ` ${styles.apiDrawerLoadingHidden}` : ''}`}
|
||||
aria-hidden={isApiPanelReady}
|
||||
>
|
||||
<div className={styles.apiDrawerLoadingSpinner}></div>
|
||||
<div className={styles.apiDrawerLoadingText}>正在加载中,完成后即可插入代码…</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1305,12 +1306,22 @@ sys.settrace(trace_func)`;
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
const cursor = selection.main.head;
|
||||
const withSafeNewlines = (text) => {
|
||||
let next = text;
|
||||
if (cursor > 0 && view.state.doc.sliceString(cursor - 1, cursor) !== '\n') {
|
||||
next = '\n' + next;
|
||||
}
|
||||
if (!next.endsWith('\n')) {
|
||||
next += '\n';
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
// 检查代码是否包含snippet占位符
|
||||
if (code.includes('#{')) {
|
||||
// 使用snippet功能插入代码
|
||||
try {
|
||||
const snippetTemplate = snippet(code);
|
||||
const snippetTemplate = snippet(withSafeNewlines(code));
|
||||
const completion = snippetTemplate(view, {
|
||||
from: cursor,
|
||||
to: cursor
|
||||
@@ -1320,7 +1331,7 @@ sys.settrace(trace_func)`;
|
||||
view.dispatch(completion);
|
||||
} else {
|
||||
// 如果snippet创建失败,使用简化的插入方式
|
||||
const cleanCode = code.replace(/#{([^}]*)}/g, '$1');
|
||||
const cleanCode = withSafeNewlines(code.replace(/#{([^}]*)}/g, '$1'));
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: cursor,
|
||||
@@ -1332,7 +1343,7 @@ sys.settrace(trace_func)`;
|
||||
} catch (error) {
|
||||
console.warn('Snippet insertion failed, falling back to plain text:', error);
|
||||
// 回退到普通插入,移除snippet标记
|
||||
const cleanCode = code.replace(/#{([^}]*)}/g, '$1');
|
||||
const cleanCode = withSafeNewlines(code.replace(/#{([^}]*)}/g, '$1'));
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: cursor,
|
||||
@@ -1342,15 +1353,15 @@ sys.settrace(trace_func)`;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 普通代码插入
|
||||
const insertText = withSafeNewlines(code);
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: cursor,
|
||||
to: cursor,
|
||||
insert: code
|
||||
insert: insertText
|
||||
},
|
||||
selection: {
|
||||
anchor: cursor + code.length
|
||||
anchor: cursor + insertText.length
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1365,7 +1376,7 @@ sys.settrace(trace_func)`;
|
||||
<>
|
||||
<ErrorModal />
|
||||
<div className={styles.editorWithApi}>
|
||||
<ApiDrawer />
|
||||
{ApiDrawer()}
|
||||
<div
|
||||
className={styles.editorPane}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -150,7 +150,7 @@ const CurrentLevelChoose = ({ onCancel }) => {
|
||||
}
|
||||
// window.handleClickNewProjectWithUnity();
|
||||
}
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber);
|
||||
const realLvId_tmp = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode);
|
||||
const realLvId = realLvId_tmp + cellIndex
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
|
||||
@@ -14,6 +14,8 @@ window.CONFIG = CONFIG;
|
||||
import { initDisableDevtool } from '../../playground/devtool-control.js';
|
||||
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
|
||||
import { debug } from 'scratch-vm/src/util/log.js';
|
||||
import GameLoadingOverlay from '../game-loading-overlay/game-loading-overlay.jsx';
|
||||
import useGameLoadProgress from '../game-loading-overlay/use-game-load-progress.js';
|
||||
|
||||
const MAX_LEVELS = 500; // 最大关卡数据数量
|
||||
const STORAGE_KEY = 'player_levels_data'; // 使用命名空间,防止键名冲突
|
||||
@@ -247,6 +249,16 @@ const UnityComponent = function (props) {
|
||||
const [getBlockNum, setBlockNum] = useState(0);
|
||||
const [testDal, setTestDal] = useState(-1);
|
||||
const canvasRef = useRef(null);
|
||||
const {
|
||||
progress: gameLoadProgress,
|
||||
visible: gameLoadVisible,
|
||||
onUnityProgress,
|
||||
onUnityInstanceCreated
|
||||
} = useGameLoadProgress();
|
||||
const onUnityProgressRef = useRef(onUnityProgress);
|
||||
const onUnityInstanceCreatedRef = useRef(onUnityInstanceCreated);
|
||||
onUnityProgressRef.current = onUnityProgress;
|
||||
onUnityInstanceCreatedRef.current = onUnityInstanceCreated;
|
||||
const [isGameOverVisible, setIsGameOverVisible] = useState(false);
|
||||
const [isGameOkVisible, setIsGameOkVisible] = useState(false);
|
||||
const [isLockNextButton, setIsLockNextButton] = useState(true);
|
||||
@@ -741,33 +753,43 @@ const UnityComponent = function (props) {
|
||||
const [hasAlerted, setHasAlerted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
if (CONFIG.disabledev) {
|
||||
initDisableDevtool({
|
||||
ondevtoolopen: () => {
|
||||
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
const originalOnBeforeUnload = window.onbeforeunload;
|
||||
window.onbeforeunload = null;
|
||||
// 临时禁用 onbeforeunload 事件监听,以防止弹出确认离开弹窗
|
||||
const originalOnBeforeUnload = window.onbeforeunload;
|
||||
window.onbeforeunload = null;
|
||||
|
||||
// 停止 Scratch VM 的所有活动
|
||||
vm.stopAll();
|
||||
// 停止 Scratch VM 的所有活动
|
||||
vm.stopAll();
|
||||
|
||||
// 清空积木
|
||||
vm.editingTarget.blocks.deleteAllBlocks();
|
||||
vm.emitWorkspaceUpdate();
|
||||
// 清空积木
|
||||
vm.editingTarget.blocks.deleteAllBlocks();
|
||||
vm.emitWorkspaceUpdate();
|
||||
|
||||
// 销毁 Unity 实例
|
||||
if (window.unityInstance) {
|
||||
window.unityInstance.Quit().then(() => {
|
||||
window.unityInstance = null;
|
||||
});
|
||||
}
|
||||
// 销毁游戏实例(Cocos 兼容:Quit 可能不存在)
|
||||
if (window.unityInstance) {
|
||||
const quit = window.unityInstance.Quit;
|
||||
if (typeof quit === 'function') {
|
||||
Promise.resolve(quit.call(window.unityInstance)).then(() => {
|
||||
window.unityInstance = null;
|
||||
});
|
||||
} else {
|
||||
window.unityInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟跳转到目标页面,确保销毁
|
||||
window.location.href = localStorage.getItem('returnUrl')
|
||||
// 恢复原始的 onbeforeunload 监听器
|
||||
window.onbeforeunload = originalOnBeforeUnload;
|
||||
}
|
||||
});
|
||||
// 延迟跳转到目标页面,确保销毁
|
||||
window.location.href = localStorage.getItem('returnUrl')
|
||||
// 恢复原始的 onbeforeunload 监听器
|
||||
window.onbeforeunload = originalOnBeforeUnload;
|
||||
},
|
||||
interval: 500, // 检测间隔时间
|
||||
clearLog: true, // 清除控制台的日志
|
||||
disableMenu: true, // 禁用右键菜单
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -781,7 +803,7 @@ const UnityComponent = function (props) {
|
||||
//计算真实关卡位置
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
}
|
||||
@@ -905,7 +927,7 @@ const UnityComponent = function (props) {
|
||||
window.iscreate = 1;
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber, gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(gamelvdataJson.gamelv, gamelvdataJson.gameid, gamelvdataJson.ismatch, gamelvdataJson.gameNumber)
|
||||
}
|
||||
@@ -922,7 +944,7 @@ const UnityComponent = function (props) {
|
||||
// 延迟 500 毫秒后切换关卡,只执行一次
|
||||
setTimeout(() => {
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
} else {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
@@ -984,7 +1006,7 @@ const UnityComponent = function (props) {
|
||||
}
|
||||
setIsLockNextButton(true);
|
||||
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
const trialCount = localStorage.getItem('trial_count');
|
||||
|
||||
if (localStorage.getItem('permission') === "0" && (realLvId - CONFIG.BEGINNING_REAL_LVID) >= trialCount && currentlv > 9) {
|
||||
@@ -1403,7 +1425,7 @@ const UnityComponent = function (props) {
|
||||
setIsNextButtonComplete(false);
|
||||
}
|
||||
window.gamelvdata.currentlv = window.gamelvdata.currentlv + 1
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
console.log(window.gamelvdata.currentlv, window.gamelvdataJson.gameNumber)
|
||||
removeGlobalKeyboardShield();
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
@@ -1461,7 +1483,7 @@ const UnityComponent = function (props) {
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
const realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
removeGlobalKeyboardShield();
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
console.log("Retrying the current level...realLvId...", realLvId);
|
||||
@@ -1470,47 +1492,88 @@ const UnityComponent = function (props) {
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
let cancelled = false;
|
||||
|
||||
const bootUnity = async () => {
|
||||
const config = await getUnityPlayerConfig();
|
||||
await loadUnityLoaderScript();
|
||||
if (cancelled || !canvas) {
|
||||
return;
|
||||
}
|
||||
// if (canvas && !window.unityInstance) {
|
||||
const unityUrl = "/unity";
|
||||
const buildUrl = "/unity/Build";
|
||||
const cdnRoot = CONFIG.usecdn ? getUnityCdnRoot() : unityUrl;
|
||||
const cdnBuildRoot = CONFIG.usecdn ? getUnityCdnBuildRoot() : buildUrl;
|
||||
let loaderUrl = CONFIG.usecdn
|
||||
? `${cdnBuildRoot}/mstest5.loader.js`
|
||||
: `${buildUrl}/mstest5.loader.js`;
|
||||
let config = {
|
||||
dataUrl: buildUrl + "/mstest5.data.br",
|
||||
frameworkUrl: buildUrl + "/mstest5.framework.js.br",
|
||||
codeUrl: buildUrl + "/mstest5.wasm.br",
|
||||
streamingAssetsUrl: unityUrl + "/StreamingAssets",
|
||||
companyName: "DefaultCompany",
|
||||
productName: "Web A",
|
||||
productVersion: "1.0",
|
||||
};
|
||||
if (CONFIG.usecdn) {
|
||||
config = {
|
||||
dataUrl: cdnBuildRoot + "/mstest5.data.br",
|
||||
frameworkUrl: cdnBuildRoot + "/mstest5.framework.js.br",
|
||||
codeUrl: cdnBuildRoot + "/mstest5.wasm.br",
|
||||
streamingAssetsUrl: cdnRoot + "/StreamingAssets",
|
||||
// dataUrl: buildUrl + "/Build.data.br",
|
||||
// frameworkUrl: buildUrl + "/Build.framework.js.br",
|
||||
// codeUrl: buildUrl + "/Build.wasm.br",
|
||||
// streamingAssetsUrl: unityUrl + "/StreamingAssets",
|
||||
companyName: "DefaultCompany",
|
||||
productName: "Web A",
|
||||
productVersion: "1.0",
|
||||
};
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = loaderUrl;
|
||||
|
||||
script.onload = () => {
|
||||
window.iscreate = 0;
|
||||
const unityInstance = await createUnityInstance(canvas, config, () => {});
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
window.unityInstance = unityInstance;
|
||||
window.waitForUnityStepFinish = waitForUnityStepFinish;
|
||||
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
|
||||
window.sleepForUnityStepFinish = sleepForUnityStepFinish;
|
||||
window.sleepByStepForUnityStepFinish = sleepByStepForUnityStepFinish;
|
||||
console.log("Unity 实例创建成功");
|
||||
window.processData = processData;
|
||||
window.processVehicleData = processVehicleData;
|
||||
window.externalResult = externalResult;
|
||||
window.externalLevelInfo = externalLevelInfo;
|
||||
const boot = () => {
|
||||
createUnityInstance(canvas, config, (progress) => {
|
||||
onUnityProgressRef.current(progress);
|
||||
}).then((unityInstance) => {
|
||||
onUnityInstanceCreatedRef.current();
|
||||
window.unityInstance = unityInstance;
|
||||
window.waitForUnityStepFinish = waitForUnityStepFinish;
|
||||
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
|
||||
window.sleepForUnityStepFinish = sleepForUnityStepFinish;
|
||||
window.sleepByStepForUnityStepFinish = sleepByStepForUnityStepFinish;
|
||||
console.log("Unity 实例创建成功");
|
||||
window.processData = processData;
|
||||
window.processVehicleData = processVehicleData;
|
||||
window.externalResult = externalResult;
|
||||
window.externalLevelInfo = externalLevelInfo;
|
||||
//计算真实关卡位置
|
||||
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber) + window.gamelvdata.currentlv
|
||||
let realLvId = 0;
|
||||
if (localStorage.getItem('platformMode') === 'normal') {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber, window.gamelvdataJson.gamemode) + window.gamelvdata.currentlv
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
}
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
}).catch((message) => {
|
||||
console.error(`Unity 实例创建失败: ${message}`);
|
||||
});
|
||||
};
|
||||
if (CONFIG.usecdn) {
|
||||
prefetchLevelsDatabase(cdnRoot).then(boot).catch((err) => {
|
||||
console.error('[unity-cdn] 关卡库预取失败', err);
|
||||
boot();
|
||||
});
|
||||
} else {
|
||||
realLvId = CONFIG.getRealLvId(window.gamelvdataJson.gamelv, window.gamelvdataJson.gameid, window.gamelvdataJson.ismatch, window.gamelvdataJson.gameNumber)
|
||||
boot();
|
||||
}
|
||||
CONFIG.checkGameStyle(realLvId);
|
||||
window.unityInstance.SendMessage("GameController", "SwitchLevel", realLvId);
|
||||
};
|
||||
|
||||
bootUnity().catch((message) => {
|
||||
console.error(`Unity 实例创建失败: ${message}`);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
// } else {
|
||||
// console.log("Unity 实例已经存在,跳过初始化");
|
||||
// }
|
||||
}, []);
|
||||
|
||||
const formatDateTime = (dateStr) => {
|
||||
@@ -1536,6 +1599,7 @@ const UnityComponent = function (props) {
|
||||
}}
|
||||
>
|
||||
<canvas id="unity-canvas" ref={canvasRef} width="960" height="600" style={{ width: '100%', height: '100%' }}></canvas>
|
||||
<GameLoadingOverlay visible={gameLoadVisible} progress={gameLoadProgress} />
|
||||
|
||||
<section
|
||||
className="u-align-center u-black u-clearfix u-container-align-center u-container-style u-dialog-block u-opacity u-opacity-50 u-valign-middle-md u-valign-middle-xl u-dialog-section-8"
|
||||
@@ -2097,4 +2161,4 @@ UnityComponent.propTypes = {
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
export default UnityComponent;
|
||||
export default UnityComponent;
|
||||
|
||||
0
scratch-gui/src/lib/libraries/decks/steps/add-effects.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/animate-char-add-sound.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/animate-char-change-color.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/animate-char-jump.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/animate-char-move.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/animate-char-say-something.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/animate-char-talk.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/change-size.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/chase-game-change-score.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/chase-game-move-randomly.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/chase-game-play-sound.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/chase-game-right-left.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/chase-game-up-down.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/cn-backdrop.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/cn-collect.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/cn-glide.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/cn-say.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/cn-score.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-01-say-something.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-02-animate.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-03-select-different-character.LTR.png
Normal file → Executable file
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-04-use-minus-sign.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-05-grow-shrink.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-06-select-another-different-character.LTR.png
Normal file → Executable file
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-07-jump.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-08-change-scenes.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-09-glide-around.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-10-change-costumes.es.png
Normal file → Executable file
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/code-cartoon-11-choose-more-characters.LTR.png
Normal file → Executable file
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
0
scratch-gui/src/lib/libraries/decks/steps/fly-choose-character.LTR.png
Normal file → Executable file
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |