tkunity 2026.10.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tkunity/export.py ADDED
@@ -0,0 +1,740 @@
1
+ # -*- coding: utf-8 -*-
2
+ """APK 导出器:把 .pyu 项目字典生成一个可直接构建的 Android Studio/Gradle 工程。
3
+
4
+ 产物特点:
5
+ - 纯 Java + 系统自带 org.json / android.graphics,零第三方依赖;
6
+ - minSdk 26 / targetSdk 34,JDK 17,AGP 8.2.2,Gradle 8.5;
7
+ - 内置动作与桌面预览一致(BUILTIN_ACTIONS:move-* / bounce / stop / print);
8
+ - 自定义 Python 表达式只用于桌面预览,APK 不支持(文档已注明)。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+
15
+ from . import __version__
16
+ from .pyu import MAGIC
17
+
18
+ __all__ = ["build", "DEFAULT_PACKAGE"]
19
+
20
+ DEFAULT_PACKAGE = "com.oiiaio.tkunity"
21
+ APP_NAME = "TkUnityGame"
22
+
23
+
24
+ def build(project, outdir, package=DEFAULT_PACKAGE, app_name=None):
25
+ """生成 Android 工程到 outdir,返回生成的文件路径列表。"""
26
+ meta = project.get("meta", {})
27
+ name = app_name or meta.get("name", APP_NAME)
28
+ files = []
29
+ paths = {
30
+ "settings": os.path.join(outdir, "settings.gradle"),
31
+ "root_build": os.path.join(outdir, "build.gradle"),
32
+ "gradle_props": os.path.join(outdir, "gradle.properties"),
33
+ "app_build": os.path.join(outdir, "app", "build.gradle"),
34
+ "manifest": os.path.join(outdir, "app", "src", "main", "AndroidManifest.xml"),
35
+ "strings": os.path.join(outdir, "app", "src", "main", "res", "values", "strings.xml"),
36
+ "pyu": os.path.join(outdir, "app", "src", "main", "assets", "game.pyu"),
37
+ "activity": os.path.join(outdir, "app", "src", "main", "java", package.replace(".", "/"), "MainActivity.java"),
38
+ "gameview": os.path.join(outdir, "app", "src", "main", "java", package.replace(".", "/"), "GameView.java"),
39
+ "bat": os.path.join(outdir, "build_apk.bat"),
40
+ "sh": os.path.join(outdir, "build_apk.sh"),
41
+ "readme": os.path.join(outdir, "README_BUILD.md"),
42
+ "local_props": os.path.join(outdir, "local.properties.example"),
43
+ }
44
+
45
+ for path in paths.values():
46
+ os.makedirs(os.path.dirname(path), exist_ok=True)
47
+
48
+ def write(key, content):
49
+ with open(paths[key], "w", encoding="utf-8", newline="\n") as fh:
50
+ fh.write(content)
51
+ files.append(paths[key])
52
+
53
+ write("settings", _settings_gradle())
54
+ write("root_build", _root_build_gradle())
55
+ write("gradle_props", _gradle_properties())
56
+ write("app_build", _app_build_gradle(package, name))
57
+ write("manifest", _manifest(package))
58
+ write("strings", _strings(name))
59
+ write("pyu", _pyu_blob(project))
60
+ write("activity", _main_activity(package))
61
+ write("gameview", _game_view(package, project))
62
+ write("bat", _build_bat())
63
+ write("sh", _build_sh())
64
+ write("readme", _build_readme(outdir, name))
65
+ write("local_props", "# 复制为 local.properties 并填写你的 Android SDK 路径\n# sdk.dir=C:\\\\Users\\\\you\\\\AppData\\\\Local\\\\Android\\\\Sdk\nsdk.dir=/path/to/your/android/sdk\n")
66
+ return files
67
+
68
+
69
+ # ---------------------------------------------------------------- 内容生成
70
+ def _pyu_blob(project):
71
+ blob = json.dumps(project, ensure_ascii=False, indent=1,
72
+ separators=(",", ": ")).encode("utf-8")
73
+ return (MAGIC + blob).decode("utf-8")
74
+
75
+
76
+ def _settings_gradle():
77
+ return """pluginManagement {
78
+ repositories {
79
+ google()
80
+ mavenCentral()
81
+ gradlePluginPortal()
82
+ }
83
+ }
84
+ dependencyResolutionManagement {
85
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
86
+ repositories {
87
+ google()
88
+ mavenCentral()
89
+ }
90
+ }
91
+ rootProject.name = "TkUnityGame"
92
+ include ':app'
93
+ """
94
+
95
+
96
+ def _root_build_gradle():
97
+ return """plugins {
98
+ id 'com.android.application' version '8.2.2' apply false
99
+ }
100
+ """
101
+
102
+
103
+ def _gradle_properties():
104
+ return """org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
105
+ android.useAndroidX=false
106
+ android.enableJetifier=false
107
+ """
108
+
109
+
110
+ def _app_build_gradle(package, name):
111
+ return """plugins {
112
+ id 'com.android.application'
113
+ }
114
+
115
+ android {
116
+ namespace '%s'
117
+ compileSdk 34
118
+
119
+ defaultConfig {
120
+ applicationId "%s"
121
+ minSdk 26
122
+ targetSdk 34
123
+ versionCode 1
124
+ versionName "%s"
125
+ }
126
+
127
+ compileOptions {
128
+ sourceCompatibility JavaVersion.VERSION_17
129
+ targetCompatibility JavaVersion.VERSION_17
130
+ }
131
+
132
+ buildTypes {
133
+ release {
134
+ minifyEnabled false
135
+ }
136
+ }
137
+ }
138
+
139
+ // tkunity 游戏:零第三方依赖,无需任何 dependencies 块
140
+ """ % (package, package, __version__)
141
+
142
+
143
+ def _manifest(package):
144
+ return """<?xml version="1.0" encoding="utf-8"?>
145
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
146
+
147
+ <application
148
+ android:label="@string/app_name"
149
+ android:allowBackup="true"
150
+ android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen">
151
+
152
+ <activity
153
+ android:name=".MainActivity"
154
+ android:exported="true"
155
+ android:screenOrientation="landscape"
156
+ android:configChanges="orientation|screenSize|keyboardHidden">
157
+ <intent-filter>
158
+ <action android:name="android.intent.action.MAIN" />
159
+ <category android:name="android.intent.category.LAUNCHER" />
160
+ </intent-filter>
161
+ </activity>
162
+ </application>
163
+
164
+ </manifest>
165
+ """
166
+
167
+
168
+ def _strings(name):
169
+ return """<resources>
170
+ <string name="app_name">%s</string>
171
+ </resources>
172
+ """ % name
173
+
174
+
175
+ def _main_activity(package):
176
+ return """package %s;
177
+
178
+ import android.app.Activity;
179
+ import android.os.Bundle;
180
+ import android.view.WindowManager;
181
+
182
+ public class MainActivity extends Activity {
183
+ @Override
184
+ protected void onCreate(Bundle savedInstanceState) {
185
+ super.onCreate(savedInstanceState);
186
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
187
+ setContentView(new GameView(this));
188
+ }
189
+ }
190
+ """ % package
191
+
192
+
193
+ def _build_readme(outdir, name):
194
+ return """# %s - Android 构建说明
195
+
196
+ 本目录由 tkunity %s 导出,是一个 **零第三方依赖** 的纯 Java Android 工程。
197
+
198
+ ## 构建要求
199
+ - JDK 17
200
+ - Android SDK(compileSdk 34)
201
+ - 设置环境变量 ANDROID_HOME,或把 local.properties.example 复制为 local.properties 并填写 sdk.dir
202
+ - 无需 Android Studio,命令行即可构建
203
+
204
+ ## 构建
205
+ Windows:
206
+ build_apk.bat
207
+ Linux/macOS:
208
+ ./build_apk.sh
209
+
210
+ 脚本会自动下载 Gradle 8.5(若本机未安装 gradle)。
211
+
212
+ ## 产物
213
+ app/build/outputs/apk/debug/app-debug.apk
214
+
215
+ ## 说明
216
+ - 游戏数据在 app/src/main/assets/game.pyu(.pyu 格式,图片 base64 内嵌)
217
+ - 渲染器 GameView.java 使用 android.graphics.Canvas 实现,支持:
218
+ 网格背景 / rect / oval / 图片 / 文本 / 层级 / 触摸拖动 / 点击动作 / 定时器
219
+ - 动作支持(与桌面预览通用):move-left move-right move-up move-down bounce stop print(...)
220
+ - 自定义 Python 表达式仅用于桌面预览;如需更多玩法,直接修改 GameView.java 扩展。
221
+ """ % (name, __version__)
222
+
223
+
224
+ def _build_bat():
225
+ return """@echo off
226
+ setlocal enabledelayedexpansion
227
+ set "GRADLE_VERSION=8.5"
228
+ set "DIR=%~dp0"
229
+ set "TOOL_DIR=%DIR%.gradle-tool"
230
+ set "GRADLE_HOME=%TOOL_DIR%\\gradle-%GRADLE_VERSION%"
231
+
232
+ set "GRADLE_CMD="
233
+ where gradle >nul 2>nul && set "GRADLE_CMD=gradle"
234
+ if not defined GRADLE_CMD (
235
+ if not exist "%GRADLE_HOME%\\bin\\gradle.bat" (
236
+ echo [tkunity] 正在下载 Gradle %GRADLE_VERSION% ...
237
+ if not exist "%TOOL_DIR%" mkdir "%TOOL_DIR%"
238
+ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
239
+ "$u='https://services.gradle.org/distributions/gradle-%GRADLE_VERSION%-bin.zip';" ^
240
+ "$z='%TOOL_DIR%\\gradle.zip';" ^
241
+ "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;" ^
242
+ "Invoke-WebRequest -Uri $u -OutFile $z;" ^
243
+ "Expand-Archive -Path $z -DestinationPath '%TOOL_DIR%' -Force"
244
+ if errorlevel 1 (
245
+ echo [tkunity] Gradle 下载失败,请手动下载后重试:
246
+ echo https://services.gradle.org/distributions/gradle-%GRADLE_VERSION%-bin.zip
247
+ exit /b 1
248
+ )
249
+ )
250
+ set "GRADLE_CMD=%GRADLE_HOME%\\bin\\gradle.bat"
251
+ )
252
+
253
+ if not defined ANDROID_HOME (
254
+ if not exist "%DIR%local.properties" (
255
+ echo [tkunity] 未设置 ANDROID_HOME 且缺少 local.properties
256
+ echo 请创建 %DIR%local.properties,内容: sdk.dir=C:\\\\你的Android\\\\SDK\\\\路径
257
+ exit /b 1
258
+ )
259
+ )
260
+
261
+ cd /d "%DIR%"
262
+ call "%GRADLE_CMD%" assembleDebug
263
+ if errorlevel 1 (
264
+ echo [tkunity] 构建失败
265
+ exit /b 1
266
+ )
267
+ echo.
268
+ echo APK: %DIR%app\\build\\outputs\\apk\\debug\\app-debug.apk
269
+ """
270
+
271
+
272
+ def _build_sh():
273
+ return """#!/usr/bin/env bash
274
+ set -e
275
+ GRADLE_VERSION=8.5
276
+ DIR="$(cd "$(dirname "$0")" && pwd)"
277
+ TOOL_DIR="$DIR/.gradle-tool"
278
+ GRADLE_HOME="$TOOL_DIR/gradle-$GRADLE_VERSION"
279
+
280
+ if command -v gradle >/dev/null 2>&1; then
281
+ GRADLE_CMD=gradle
282
+ else
283
+ if [ ! -x "$GRADLE_HOME/bin/gradle" ]; then
284
+ echo "[tkunity] downloading Gradle $GRADLE_VERSION ..."
285
+ mkdir -p "$TOOL_DIR"
286
+ (curl -fsSL -o "$TOOL_DIR/gradle.zip" \\
287
+ "https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip") \\
288
+ || (wget -q -O "$TOOL_DIR/gradle.zip" \\
289
+ "https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip")
290
+ unzip -q -o "$TOOL_DIR/gradle.zip" -d "$TOOL_DIR"
291
+ fi
292
+ GRADLE_CMD="$GRADLE_HOME/bin/gradle"
293
+ fi
294
+
295
+ if [ -z "$ANDROID_HOME" ] && [ ! -f "$DIR/local.properties" ]; then
296
+ echo "[tkunity] ANDROID_HOME not set and no local.properties found."
297
+ echo " Create $DIR/local.properties with: sdk.dir=/path/to/your/android/sdk"
298
+ exit 1
299
+ fi
300
+
301
+ cd "$DIR"
302
+ "$GRADLE_CMD" assembleDebug
303
+ echo
304
+ echo "APK: $DIR/app/build/outputs/apk/debug/app-debug.apk"
305
+ """
306
+
307
+
308
+ def _game_view(package, project):
309
+ """生成 GameView.java 渲染器(含场景内置动作执行逻辑)。"""
310
+ return _GAME_VIEW_TEMPLATE.replace("@PACKAGE@", package)
311
+
312
+
313
+ _GAME_VIEW_TEMPLATE = """package @PACKAGE@;
314
+
315
+ import android.content.Context;
316
+ import android.graphics.Bitmap;
317
+ import android.graphics.BitmapFactory;
318
+ import android.graphics.Canvas;
319
+ import android.graphics.Color;
320
+ import android.graphics.Paint;
321
+ import android.graphics.Rect;
322
+ import android.graphics.Typeface;
323
+ import android.os.Handler;
324
+ import android.os.Looper;
325
+ import android.util.AttributeSet;
326
+ import android.util.Base64;
327
+ import android.util.Log;
328
+ import android.view.MotionEvent;
329
+ import android.view.View;
330
+
331
+ import org.json.JSONArray;
332
+ import org.json.JSONObject;
333
+
334
+ import java.io.ByteArrayOutputStream;
335
+ import java.io.InputStream;
336
+ import java.nio.charset.StandardCharsets;
337
+ import java.util.ArrayList;
338
+ import java.util.Collections;
339
+ import java.util.Comparator;
340
+ import java.util.HashMap;
341
+ import java.util.List;
342
+ import java.util.Map;
343
+
344
+ /**
345
+ * tkunity 游戏渲染器:纯 Android 系统 API,零第三方依赖。
346
+ * 读取 assets/game.pyu(.pyu 格式),渲染场景并驱动动作。
347
+ */
348
+ public class GameView extends View {
349
+ private static final String TAG = "tkunity";
350
+ private static final int GRID = 20;
351
+ private static final int TICK_MS = 16;
352
+
353
+ private JSONObject project;
354
+ private final List<Entity> entities = new ArrayList<>();
355
+ private final Map<String, Bitmap> bitmaps = new HashMap<>();
356
+ private final Handler handler = new Handler(Looper.getMainLooper());
357
+ private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
358
+
359
+ private Entity dragging;
360
+ private float dragDX, dragDY;
361
+ private int sceneW = 960;
362
+ private int sceneH = 640;
363
+ private int bgColor = Color.rgb(30, 30, 46);
364
+ private int offsetX = 0;
365
+ private int offsetY = 0;
366
+ private long startTime;
367
+
368
+ private final Runnable ticker = new Runnable() {
369
+ @Override
370
+ public void run() {
371
+ long now = System.currentTimeMillis();
372
+ long elapsed = now - startTime;
373
+ for (Entity e : entities) {
374
+ e.tick(elapsed, sceneW, sceneH);
375
+ }
376
+ invalidate();
377
+ handler.postDelayed(this, TICK_MS);
378
+ }
379
+ };
380
+
381
+ public GameView(Context context) {
382
+ super(context);
383
+ init();
384
+ }
385
+
386
+ public GameView(Context context, AttributeSet attrs) {
387
+ super(context, attrs);
388
+ init();
389
+ }
390
+
391
+ public GameView(Context context, AttributeSet attrs, int defStyle) {
392
+ super(context, attrs, defStyle);
393
+ init();
394
+ }
395
+
396
+ private void init() {
397
+ loadProject();
398
+ startTime = System.currentTimeMillis();
399
+ handler.post(ticker);
400
+ }
401
+
402
+ private void loadProject() {
403
+ try {
404
+ InputStream in = getContext().getAssets().open("game.pyu");
405
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
406
+ byte[] buf = new byte[8192];
407
+ int n;
408
+ while ((n = in.read(buf)) != -1) {
409
+ bos.write(buf, 0, n);
410
+ }
411
+ in.close();
412
+ String text = new String(bos.toByteArray(), StandardCharsets.UTF_8);
413
+ int idx = text.indexOf('{');
414
+ project = new JSONObject(text.substring(idx));
415
+
416
+ JSONObject meta = project.optJSONObject("meta");
417
+ if (meta != null) {
418
+ sceneW = meta.optInt("scene_width", 960);
419
+ sceneH = meta.optInt("scene_height", 640);
420
+ bgColor = parseColor(meta.optString("background", "#1e1e2e"));
421
+ }
422
+ JSONArray arr = project.optJSONArray("entities");
423
+ if (arr == null) {
424
+ return;
425
+ }
426
+ for (int i = 0; i < arr.length(); i++) {
427
+ entities.add(Entity.fromJson(arr.optJSONObject(i)));
428
+ }
429
+ } catch (Exception e) {
430
+ Log.e(TAG, "load project failed", e);
431
+ }
432
+ }
433
+
434
+ @Override
435
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
436
+ super.onSizeChanged(w, h, oldw, oldh);
437
+ offsetX = Math.max(0, (w - sceneW) / 2);
438
+ offsetY = Math.max(0, (h - sceneH) / 2);
439
+ }
440
+
441
+ @Override
442
+ protected void onDraw(Canvas canvas) {
443
+ super.onDraw(canvas);
444
+ canvas.drawColor(Color.rgb(32, 34, 40));
445
+ canvas.translate(offsetX, offsetY);
446
+
447
+ // 场景背景
448
+ canvas.drawRect(0, 0, sceneW, sceneH, paint);
449
+
450
+ // 网格
451
+ paint.setStyle(Paint.Style.STROKE);
452
+ paint.setStrokeWidth(1);
453
+ paint.setColor(Color.rgb(58, 58, 58));
454
+ for (int x = 0; x <= sceneW; x += GRID) {
455
+ canvas.drawLine(x, 0, x, sceneH, paint);
456
+ }
457
+ for (int y = 0; y <= sceneH; y += GRID) {
458
+ canvas.drawLine(0, y, sceneW, y, paint);
459
+ }
460
+
461
+ // 实体按 layer 升序绘制
462
+ List<Entity> sorted = new ArrayList<>(entities);
463
+ Collections.sort(sorted, new Comparator<Entity>() {
464
+ @Override
465
+ public int compare(Entity a, Entity b) {
466
+ int c = Integer.compare(a.layer, b.layer);
467
+ return c != 0 ? c : Integer.compare(a.id, b.id);
468
+ }
469
+ });
470
+ for (Entity e : sorted) {
471
+ drawEntity(canvas, e);
472
+ }
473
+ }
474
+
475
+ private void drawEntity(Canvas canvas, Entity e) {
476
+ if ("image".equals(e.shape) && !e.image.isEmpty()) {
477
+ Bitmap bmp = getBitmap(e.image);
478
+ if (bmp != null) {
479
+ canvas.drawBitmap(bmp, null,
480
+ new Rect(e.x, e.y, e.x + e.width, e.y + e.height), paint);
481
+ }
482
+ } else if ("oval".equals(e.shape)) {
483
+ paint.setStyle(Paint.Style.FILL);
484
+ paint.setColor(e.fill);
485
+ canvas.drawOval(e.x, e.y, e.x + e.width, e.y + e.height, paint);
486
+ paint.setStyle(Paint.Style.STROKE);
487
+ paint.setStrokeWidth(2);
488
+ paint.setColor(e.outline);
489
+ canvas.drawOval(e.x, e.y, e.x + e.width, e.y + e.height, paint);
490
+ } else {
491
+ paint.setStyle(Paint.Style.FILL);
492
+ paint.setColor(e.fill);
493
+ canvas.drawRect(e.x, e.y, e.x + e.width, e.y + e.height, paint);
494
+ paint.setStyle(Paint.Style.STROKE);
495
+ paint.setStrokeWidth(2);
496
+ paint.setColor(e.outline);
497
+ canvas.drawRect(e.x, e.y, e.x + e.width, e.y + e.height, paint);
498
+ }
499
+
500
+ if (e.labelText != null && !e.labelText.isEmpty()) {
501
+ paint.setStyle(Paint.Style.FILL);
502
+ paint.setColor(e.labelColor);
503
+ paint.setTextSize(e.fontSize * getResources().getDisplayMetrics().density);
504
+ paint.setTypeface(e.bold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
505
+ Paint.Align oldAlign = paint.getTextAlign();
506
+ paint.setTextAlign(Paint.Align.CENTER);
507
+ Paint.FontMetrics fm = paint.getFontMetrics();
508
+ float cy = e.y + e.height / 2f - (fm.ascent + fm.descent) / 2f;
509
+ canvas.drawText(e.labelText, e.x + e.width / 2f, cy, paint);
510
+ paint.setTextAlign(oldAlign);
511
+ }
512
+ }
513
+
514
+ private Bitmap getBitmap(String assetName) {
515
+ if (bitmaps.containsKey(assetName)) {
516
+ return bitmaps.get(assetName);
517
+ }
518
+ try {
519
+ JSONObject assets = project.optJSONObject("assets");
520
+ if (assets == null) {
521
+ return null;
522
+ }
523
+ String b64 = assets.optString(assetName, "");
524
+ if (b64.isEmpty()) {
525
+ return null;
526
+ }
527
+ byte[] data = Base64.decode(b64, Base64.DEFAULT);
528
+ Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
529
+ bitmaps.put(assetName, bmp);
530
+ return bmp;
531
+ } catch (Exception e) {
532
+ Log.e(TAG, "decode image failed: " + assetName, e);
533
+ return null;
534
+ }
535
+ }
536
+
537
+ // -------------------------------------------------- 触摸
538
+ @Override
539
+ public boolean onTouchEvent(MotionEvent event) {
540
+ float x = event.getX() - offsetX;
541
+ float y = event.getY() - offsetY;
542
+ switch (event.getActionMasked()) {
543
+ case MotionEvent.ACTION_DOWN: {
544
+ Entity hit = hitTest(x, y);
545
+ if (hit != null) {
546
+ dragging = hit;
547
+ dragDX = x - hit.x;
548
+ dragDY = y - hit.y;
549
+ hit.runAction(hit.tapAction);
550
+ }
551
+ return true;
552
+ }
553
+ case MotionEvent.ACTION_MOVE: {
554
+ if (dragging != null) {
555
+ dragging.x = Math.round(x - dragDX);
556
+ dragging.y = Math.round(y - dragDY);
557
+ invalidate();
558
+ }
559
+ return true;
560
+ }
561
+ case MotionEvent.ACTION_UP:
562
+ case MotionEvent.ACTION_CANCEL:
563
+ dragging = null;
564
+ return true;
565
+ default:
566
+ return super.onTouchEvent(event);
567
+ }
568
+ }
569
+
570
+ private Entity hitTest(float x, float y) {
571
+ List<Entity> sorted = new ArrayList<>(entities);
572
+ Collections.sort(sorted, new Comparator<Entity>() {
573
+ @Override
574
+ public int compare(Entity a, Entity b) {
575
+ int c = Integer.compare(b.layer, a.layer);
576
+ return c != 0 ? c : Integer.compare(b.id, a.id);
577
+ }
578
+ });
579
+ for (Entity e : sorted) {
580
+ if (x >= e.x && x <= e.x + e.width && y >= e.y && y <= e.y + e.height) {
581
+ return e;
582
+ }
583
+ }
584
+ return null;
585
+ }
586
+
587
+ // -------------------------------------------------- 工具
588
+ static int parseColor(String hex) {
589
+ try {
590
+ if (hex == null || hex.isEmpty()) {
591
+ return Color.BLACK;
592
+ }
593
+ hex = hex.trim();
594
+ if (hex.startsWith("#")) {
595
+ hex = hex.substring(1);
596
+ }
597
+ if (hex.length() == 6) {
598
+ return Color.rgb(Integer.parseInt(hex.substring(0, 2), 16),
599
+ Integer.parseInt(hex.substring(2, 4), 16),
600
+ Integer.parseInt(hex.substring(4, 6), 16));
601
+ }
602
+ if (hex.length() == 8) {
603
+ return Color.argb(Integer.parseInt(hex.substring(6, 8), 16),
604
+ Integer.parseInt(hex.substring(0, 2), 16),
605
+ Integer.parseInt(hex.substring(2, 4), 16),
606
+ Integer.parseInt(hex.substring(4, 6), 16));
607
+ }
608
+ } catch (Exception ignored) {
609
+ // fall through
610
+ }
611
+ return Color.BLACK;
612
+ }
613
+
614
+ // -------------------------------------------------- 实体
615
+ public static class Entity {
616
+ int id;
617
+ String name = "obj";
618
+ int x, y, width = 80, height = 60, layer;
619
+ float rotation;
620
+ String shape = "rect";
621
+ int fill = Color.rgb(76, 139, 245);
622
+ int outline = Color.rgb(18, 52, 86);
623
+ String image = "";
624
+ String labelText = "";
625
+ int fontSize = 18;
626
+ int labelColor = Color.BLACK;
627
+ boolean bold = false;
628
+ String tapAction = "none";
629
+ String timerAction = "none";
630
+ int intervalMs = 1000;
631
+ long lastTick = 0;
632
+ long lastMove = 0;
633
+ int vx = 0;
634
+ int vy = 0;
635
+
636
+ static Entity fromJson(JSONObject o) throws Exception {
637
+ Entity e = new Entity();
638
+ e.id = o.optInt("id");
639
+ e.name = o.optString("name", "obj");
640
+ JSONObject comps = o.optJSONObject("components");
641
+ if (comps == null) {
642
+ return e;
643
+ }
644
+ JSONObject t = comps.optJSONObject("Transform");
645
+ if (t != null) {
646
+ e.x = t.optInt("x");
647
+ e.y = t.optInt("y");
648
+ e.width = t.optInt("width", 80);
649
+ e.height = t.optInt("height", 60);
650
+ e.layer = t.optInt("layer", 0);
651
+ e.rotation = (float) t.optDouble("rotation", 0.0);
652
+ }
653
+ JSONObject s = comps.optJSONObject("Sprite");
654
+ if (s != null) {
655
+ e.shape = s.optString("shape", "rect");
656
+ e.fill = parseColor(s.optString("fill", "#4C8BF5"));
657
+ e.outline = parseColor(s.optString("outline", "#123456"));
658
+ e.image = s.optString("image", "");
659
+ }
660
+ JSONObject l = comps.optJSONObject("Label");
661
+ if (l != null) {
662
+ e.labelText = l.optString("text", "");
663
+ e.fontSize = l.optInt("font_size", 18);
664
+ e.labelColor = parseColor(l.optString("color", "#000000"));
665
+ e.bold = l.optBoolean("bold", false);
666
+ }
667
+ JSONObject tap = comps.optJSONObject("TapHandler");
668
+ if (tap != null) {
669
+ e.tapAction = tap.optString("action", "none");
670
+ }
671
+ JSONObject tm = comps.optJSONObject("Timer");
672
+ if (tm != null) {
673
+ e.timerAction = tm.optString("action", "none");
674
+ e.intervalMs = tm.optInt("interval_ms", 1000);
675
+ }
676
+ return e;
677
+ }
678
+
679
+ void tick(long elapsed, int sceneW, int sceneH) {
680
+ if ((vx != 0 || vy != 0) && elapsed - lastMove >= TICK_MS) {
681
+ lastMove = elapsed;
682
+ x += vx;
683
+ y += vy;
684
+ if (x < 0) {
685
+ x = 0;
686
+ vx = -vx;
687
+ }
688
+ if (x + width > sceneW) {
689
+ x = sceneW - width;
690
+ vx = -vx;
691
+ }
692
+ if (y < 0) {
693
+ y = 0;
694
+ vy = -vy;
695
+ }
696
+ if (y + height > sceneH) {
697
+ y = sceneH - height;
698
+ vy = -vy;
699
+ }
700
+ }
701
+ if (!"none".equals(timerAction) && intervalMs > 0) {
702
+ if (elapsed - lastTick >= intervalMs) {
703
+ lastTick = elapsed;
704
+ runAction(timerAction);
705
+ }
706
+ }
707
+ }
708
+
709
+ void runAction(String action) {
710
+ if (action == null) {
711
+ return;
712
+ }
713
+ action = action.trim();
714
+ if (action.isEmpty() || "none".equals(action)) {
715
+ return;
716
+ }
717
+ if ("move-left".equals(action)) {
718
+ vx = -4; vy = 0;
719
+ } else if ("move-right".equals(action)) {
720
+ vx = 4; vy = 0;
721
+ } else if ("move-up".equals(action)) {
722
+ vx = 0; vy = -4;
723
+ } else if ("move-down".equals(action)) {
724
+ vx = 0; vy = 4;
725
+ } else if ("bounce".equals(action)) {
726
+ if (vx == 0 && vy == 0) {
727
+ vx = 4;
728
+ vy = 4;
729
+ }
730
+ } else if ("stop".equals(action)) {
731
+ vx = 0; vy = 0;
732
+ } else if (action.startsWith("print(")) {
733
+ Log.i(TAG, action);
734
+ }
735
+ // 其余自定义 Python 表达式仅在桌面预览中执行
736
+ }
737
+ }
738
+ }
739
+ }
740
+ """