diff --git a/.gitignore b/.gitignore
index 3dcd1e4..115e0d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,12 @@ research/
.playwright-mcp/
cache/
star-raid/.env
+
+# Star Raid Android App
+starraid-android/build/
+starraid-android/app/build/
+starraid-android/.gradle/
+starraid-android/local.properties
+starraid-android/app/release/
+starraid-android/.keystore-pass
+*.apk
diff --git a/AGENTS.md b/AGENTS.md
index 14ad23c..405eb51 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -31,8 +31,9 @@ git add -A && git commit -m "提交信息"
## 验证规则(最高优先级,违反会被用户骂)
1. **游戏视觉/手感禁止 Agent 验收**——用户亲自验收。但**账号/大厅/排行榜/结算上报等功能流允许 playwright 测试**(用户 2026-08-06 明确授权):注册/登陆/数据断言可跑,只是不许用截图评判游戏画面效果。
-2. 交付时必须诚实标注验证状态。
-3. 完成了操作 ≠ 任务完成,未经验证不得说“修复了/完成了”。
+2. **游戏玩法改动(武装配置/Boss机制/手感/数值等)默认由用户亲自测试**(用户 2026-08-06 补充):Agent 不主动跑 playwright 验收这类改动,只做语法检查等静态校验,交付时标注“未做功能验证,待用户验收”;用户明确要求时才跑功能流测试。
+3. 交付时必须诚实标注验证状态。
+4. 完成了操作 ≠ 任务完成,未经验证不得说“修复了/完成了”。
## 项目结构
diff --git a/deploy_starraid.sh b/deploy_starraid.sh
new file mode 100755
index 0000000..343e47e
--- /dev/null
+++ b/deploy_starraid.sh
@@ -0,0 +1,106 @@
+#!/bin/bash
+set -euo pipefail
+
+# ============================================================================
+# deploy_starraid.sh — Star Raid 一键部署(2026-08-06)
+# ----------------------------------------------------------------------------
+# 参考项目本体 agents 的 deploy_agents.sh / refresh_agents.sh 双脚本模式:
+# 本地:git push → rsync 上传 → SSH 触发服务器 refresh_starraid.sh
+# 服务器:/opt/starraid/refresh_starraid.sh(重启 + 健康检查 + nginx reload)
+#
+# 用法:
+# bash deploy_starraid.sh # 默认:push main + rsync + 触发刷新
+# bash deploy_starraid.sh --no-push # 跳过 git push(例如已手动 push)
+# REMOTE_HOST=xxx bash deploy_starraid.sh # 覆盖远程主机
+#
+# 说明:
+# - rsync 排除 .env(生产密钥)、demo/、.DS_Store
+# - Star Raid 是纯静态 + node 服务,无前端构建步骤
+# - 游戏服务器目录 /opt/starraid 无 git,采用 rsync 直传(与项目本体 git pull 模式不同)
+# ============================================================================
+
+# ── 配置(可通过环境变量覆盖) ──
+REMOTE_HOST="${REMOTE_HOST:-59.110.19.30}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_DIR="${REMOTE_DIR:-/opt/starraid}"
+GIT_BRANCH="${GIT_BRANCH:-main}"
+
+LOCAL_ROOT="$(cd "$(dirname "$0")" && pwd)"
+GAME_DIR="$LOCAL_ROOT/star-raid"
+SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+
+log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
+
+need_cmd() {
+ if ! command -v "$1" >/dev/null 2>&1; then
+ log "缺少命令 $1,请先安装后重试"
+ exit 1
+ fi
+}
+
+ssh_exec() { ssh -p "$REMOTE_PORT" $SSH_OPTS "$REMOTE_USER@$REMOTE_HOST" "$@"; }
+
+# ── 参数解析 ──
+SKIP_PUSH=false
+for arg in "$@"; do
+ case "$arg" in
+ --no-push) SKIP_PUSH=true ;;
+ -h|--help)
+ echo "用法: bash deploy_starraid.sh [--no-push]"
+ echo " --no-push 跳过 git push(仅 rsync 上传 + 服务器刷新)"
+ exit 0
+ ;;
+ *) log "未知参数: $arg(支持 --no-push)"; exit 1 ;;
+ esac
+done
+
+main() {
+ need_cmd ssh
+ need_cmd rsync
+
+ # ── 0. 前置检查 ──
+ if [[ ! -d "$GAME_DIR" ]]; then
+ log "错误: 未找到游戏目录 $GAME_DIR"
+ exit 1
+ fi
+
+ # ── 1. Git Push ──
+ if $SKIP_PUSH; then
+ log "1/3 跳过 git push(--no-push)"
+ else
+ log "1/3 git push → origin/$GIT_BRANCH"
+ cd "$LOCAL_ROOT"
+ if ! git diff --quiet --exit-code 2>/dev/null || ! git diff --cached --quiet --exit-code 2>/dev/null; then
+ log "警告:有未提交的改动,请先 commit 或 stash"
+ log " - 查看: git status"
+ log " - 提交: git add -A && git commit -m '...'"
+ log " - 暂存: git stash push -m 'WIP'"
+ log "若仍要部署,请使用 --no-push 跳过此步骤"
+ exit 1
+ fi
+ git push origin "$GIT_BRANCH"
+ log "push 完成"
+ fi
+
+ # ── 2. rsync 上传 ──
+ log "2/3 rsync 上传 → $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR"
+ # 注意:不用 --delete——服务器端 refresh_starraid.sh 等本地没有的文件必须保留;
+ # 且避免本地临时缺文件时连带删除服务器文件(与手动 rsync 命令保持一致)
+ rsync -az \
+ --exclude='.env' \
+ --exclude='.env.example' \
+ --exclude='.DS_Store' \
+ --exclude='demo/' \
+ --exclude='*.log' \
+ --exclude='refresh_starraid.sh' \
+ "$GAME_DIR/" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
+ log "上传完成"
+
+ # ── 3. 触发服务器刷新 ──
+ log "3/3 触发服务器 refresh_starraid.sh"
+ ssh_exec "bash $REMOTE_DIR/refresh_starraid.sh"
+ log "全部完成 🚀 https://starraid.cyjai.com"
+}
+
+main "$@"
diff --git a/star-raid/css/style.css b/star-raid/css/style.css
index 8eb6d58..c3aea78 100644
--- a/star-raid/css/style.css
+++ b/star-raid/css/style.css
@@ -134,6 +134,11 @@ h1.logo{font-size:clamp(20px,5.4vw,44px);color:#fff;letter-spacing:4px;line-heig
.cx-item .nm{font-size:clamp(6px,1.1vw,8px);margin-top:4px;letter-spacing:1px}
.cx-item.lock{opacity:.35;cursor:default}
.cx-item.lock:hover{transform:none;border-color:var(--line)}
+/* 自选武装(Boss挑战):选中高亮 + 槽位计数 */
+.cx-item.sel{border-color:var(--cyan);box-shadow:0 0 12px rgba(65,230,255,.4);background:rgba(65,230,255,.08)}
+.cnt{color:var(--gold);font-size:clamp(8px,1.4vw,11px)}
+.cx-grid.full .cx-item:not(.sel){opacity:.55}
+.cx-grid.full .cx-item:not(.sel):hover{border-color:var(--line);transform:none}
.cx-sec{font-size:clamp(8px,1.5vw,10px);color:var(--cyan);margin:10px 0 4px;letter-spacing:3px}
/* ---------- 登陆/注册 ---------- */
diff --git a/star-raid/index.html b/star-raid/index.html
index d409855..808b6f2 100644
--- a/star-raid/index.html
+++ b/star-raid/index.html
@@ -102,7 +102,7 @@
— 机库 · 强化 —
diff --git a/star-raid/js/enemies.js b/star-raid/js/enemies.js
index 1fb8ebf..3b74b51 100644
--- a/star-raid/js/enemies.js
+++ b/star-raid/js/enemies.js
@@ -282,7 +282,13 @@ function updateBoss(){
if(np>b.phase){b.phase=np;b.transT=70;b.r=(def.rr&&def.rr[np])||b.r;b.ry=(def.ryy&&def.ryy[np])||undefined;
G.eb.length=0;G.bbeams.length=0;b.keys=null;SFX.warn();G.shake=Math.max(G.shake,5);
if(b.idx===6){G.funnels.length=0;b.form=null;if(np===1){spawnFunnels(b,4);b.plT=Math.round(300/b.fireM);}} // 夺权者X二阶段:增至4架僚机
- banner(b.name+" — 第"+["一","二","三"][np]+"形态","#ff5566",90);}
+ banner(b.name+" — 第"+["一","二","三"][np]+"形态","#ff5566",90);
+ if(G.mode==="rush"){ // Boss挑战:通过一个阶段恢复50%血量
+ const heal=Math.ceil(G.maxHp/2);
+ G.hp=Math.min(G.maxHp,G.hp+heal);
+ G.texts.push({x:G.p.x,y:G.p.y-16,t:"+50% 生命恢复",col:"#5cff9d",life:70});
+ SFX.graze();
+ }}
if(b.transT>0){b.transT--;b.dashTrail=null; // 变形期间清冲撞残影
if(b.transT===0){explodeFx(b.x,b.y,"dark",1.3);G.shake=Math.max(G.shake,6);
for(let i=0;i<10;i++)ebul(b.x,b.y,i/10*TAU,1.5,"orbP",12);} // 变形完成爆发环弹
diff --git a/star-raid/js/ui.js b/star-raid/js/ui.js
index a33da7f..f92b9c0 100644
--- a/star-raid/js/ui.js
+++ b/star-raid/js/ui.js
@@ -1,6 +1,6 @@
"use strict";
/* ================= 界面流转 ================= */
-const OVLS=["o-auth","o-home","o-title","o-setup","o-hangar","o-cards","o-pause","o-over","o-codex","o-demo","o-rush","o-slot","o-unsaved"];
+const OVLS=["o-auth","o-home","o-title","o-setup","o-hangar","o-cards","o-pause","o-over","o-codex","o-demo","o-rush","o-slot","o-unsaved","o-rushmode","o-rushpick"];
function show(id){for(const o of OVLS)$(o).classList.toggle("hide",o!==id);}
const SETUP={mode:"endless",ship:"falcon",diff:0}; // 出击默认无尽模式
function bestLine(){
@@ -359,21 +359,96 @@ function buildRush(){
if(row.children.length){grp.appendChild(row);el.appendChild(grp);}
}
}
-function rollRushBuild(){ // 随机武装:3主武Lv3 + 3副武Lv3 + 2僚机Lv2
+function rollRushBuild(){ // 随机武装:3主武Lv4 + 3副武Lv4 + 2僚机Lv3
const sh=a=>{const x=a.slice();for(let i=x.length-1;i>0;i--){const j=rndi(0,i),t2=x[i];x[i]=x[j];x[j]=t2;}return x;};
return {m:sh(Object.keys(WEAPONS)).slice(0,3),s:sh(Object.keys(SUBS)).slice(0,3),d:sh(Object.keys(DRONES)).slice(0,2)};
}
-function startRush(i){
+function startRush(i){ // 点Boss卡片:弹窗选择 随机出击/自选武器
AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
- const build=rollRushBuild();
- if(SAVE.rushAnim)playSlot(build,()=>startRushGo(i,build)); // 老虎机动画抽取
- else startRushGo(i,build); // 直接随机
+ openRushMode(i);
+}
+/* ---- 出击方式弹窗:随机 / 自选 ---- */
+let rushPickIdx=0;
+function openRushMode(i){
+ rushPickIdx=i;
+ const b=BOSSES[i],tierName=["简单","中等","困难","地狱"][b.tier]||"";
+ const nm=$("rushmode-name");nm.textContent=b.name+(tierName?" · "+tierName:"");nm.style.color=b.col;
+ $("b-rushmode-random").onclick=()=>{
+ show("o-rush");
+ const build=rollRushBuild();
+ if(SAVE.rushAnim)playSlot(build,()=>startRushGo(i,build)); // 老虎机动画抽取
+ else startRushGo(i,build); // 直接随机
+ };
+ $("b-rushmode-pick").onclick=()=>openRushPick(i);
+ $("b-rushmode-cancel").onclick=()=>show("o-rush");
+ show("o-rushmode");
+}
+/* ---- 自选武装面板:从图鉴已解锁中挑 3主+3副+2僚(等级与随机同级 Lv4/4/3) ---- */
+const rushPick={m:[null,null,null],s:[null,null,null],d:[null,null,null]}; // 槽位:null=未选
+function openRushPick(i){
+ rushPickIdx=i;
+ const b=BOSSES[i];
+ $("rushpick-title").textContent=b.name+" — 从已解锁图鉴中选择";
+ for(const k of ["m","s","d"])rushPick[k].fill(null);
+ const mk=(label,keys,dict)=>{
+ const box=$("rushpick-"+label);box.innerHTML="";
+ for(const k of keys){
+ const def=dict[k],owned=!!SAVE.codex[k]||codexTestAll; // 测试模式:全部可选
+ const d=document.createElement("div");
+ d.className="cx-item"+(owned?"":" lock");
+ const img=def.icon?ICONS[def.icon]:SPR[def.spr];
+ d.appendChild(iconCanvas(img,28,!owned));
+ d.insertAdjacentHTML("beforeend",`
${owned?def.name:"???"}
`);
+ d.dataset.key=k;
+ if(owned)d.onclick=()=>toggleRushPick(d,label);
+ box.appendChild(d);
+ }
+ };
+ mk("m",MAINS,WEAPONS);
+ mk("s",SUB_KEYS,SUBS);
+ mk("d",DRONE_KEYS,DRONES);
+ updRushPickCaps();
+ show("o-rushpick");
+}
+function toggleRushPick(el,label){
+ const cap=label==="d"?2:3,slots=rushPick[label];
+ const i=slots.indexOf(el.dataset.key);
+ if(i>=0){slots[i]=null;el.classList.remove("sel");} // 取消已选
+ else{
+ if(slots.filter(Boolean).length>=cap)return; // 槽位已满:忽略点击
+ const empty=slots.indexOf(null);if(empty<0)return;
+ slots[empty]=el.dataset.key;el.classList.add("sel");
+ SFX.pick();
+ }
+ updRushPickCaps();
+}
+function updRushPickCaps(){
+ const cap={m:3,s:3,d:2};
+ for(const k of ["m","s","d"]){
+ const n=rushPick[k].filter(Boolean).length;
+ const c=$("rushpick-cnt-"+k);if(c)c.textContent=n+"/"+cap[k];
+ $("rushpick-"+k).classList.toggle("full",n>=cap[k]);
+ }
+}
+function rushPickGo(){ // 未选满的槽位:从已解锁池随机补齐(图鉴全空时兜底全池)
+ const owned=kk=>!!SAVE.codex[kk]||codexTestAll;
+ const pools={m:MAINS,s:SUB_KEYS,d:DRONE_KEYS};
+ const build={m:[],s:[],d:[]};
+ for(const k of ["m","s","d"]){
+ const cap=k==="d"?2:3;
+ const ownedPool=pools[k].filter(owned);
+ const src=ownedPool.length?ownedPool:pools[k]; // 兜底
+ build[k]=rushPick[k].filter(Boolean).slice(0,cap);
+ const rest=src.filter(x=>!build[k].includes(x));
+ while(build[k].length
G.weapons[k]=3);G.loadout=build.m.slice();
- G.subs={};build.s.forEach(k=>G.subs[k]=3);G.subLoadout=build.s.slice();
- G.drones={};build.d.forEach(k=>G.drones[k]=2);G.droneLoadout=build.d.slice();
+ G.weapons={};build.m.forEach(k=>G.weapons[k]=4);G.loadout=build.m.slice();
+ G.subs={};build.s.forEach(k=>G.subs[k]=4);G.subLoadout=build.s.slice();
+ G.drones={};build.d.forEach(k=>G.drones[k]=3);G.droneLoadout=build.d.slice();
G.xpNeed=1e9; // 挑战模式无升级打断
G.rushIdx=i;G.rushT0=0;
G.stage=i; // 排行榜统计:rush 局 stage 记录 Boss 索引(0~10)
@@ -384,6 +459,8 @@ function startRushGo(i,build){
G.rushT0=G.time;
startBoss();
}
+$("b-rushpick-go").onclick=()=>{show("o-rush");rushPickGo();};
+$("b-rushpick-back").onclick=()=>show("o-rush");
/* ---- 老虎机抽奖动画 ---- */
let slotTimers=[];
function playSlot(build,cb){
diff --git a/starraid-android/.gitignore b/starraid-android/.gitignore
new file mode 100644
index 0000000..1741fc1
--- /dev/null
+++ b/starraid-android/.gitignore
@@ -0,0 +1,25 @@
+# macOS
+.DS_Store
+
+# Local Android/IDE state
+local.properties
+.idea/
+.gradle/
+
+# Build outputs
+build/
+**/build/
+
+# Signing keys and secrets
+*.jks
+*.keystore
+
+# Logs
+*.log
+
+# Release artifacts
+*.apk
+*.aab
+*.apks
+*.dm
+app/release/
diff --git a/starraid-android/APP_CHANGELOG.md b/starraid-android/APP_CHANGELOG.md
new file mode 100644
index 0000000..e8fabaa
--- /dev/null
+++ b/starraid-android/APP_CHANGELOG.md
@@ -0,0 +1,7 @@
+# Star Raid Android 更新说明
+
+## 1.0.0(2026-08-06)
+- 首个版本:WebView 壳加载 https://starraid.cyjai.com
+- 锁定竖屏 + 沉浸式全屏 + 游戏期间屏幕常亮
+- 像素风启动画面与 App 图标(初始飞机「猎鹰」)
+- 账号 / 排行榜 / 云存档与 Web 版完全互通
diff --git a/starraid-android/README.md b/starraid-android/README.md
new file mode 100644
index 0000000..1c050c3
--- /dev/null
+++ b/starraid-android/README.md
@@ -0,0 +1,34 @@
+# Star Raid Android 客户端
+
+星尘突击队(Star Raid)安卓客户端,WebView 壳工程。
+
+- 游戏线上地址:https://starraid.cyjai.com
+- 客户端仅承载 WebView,游戏/账号/排行榜/云存档全部走线上服务
+- 锁定竖屏、沉浸式全屏、游戏期间屏幕常亮
+
+## 构建
+
+```bash
+# 需要 Android SDK(local.properties 或 ANDROID_HOME)与 JDK 17
+
+# 签名参数(也可用同名环境变量)
+./gradlew assembleRelease \
+ -PANDROID_KEYSTORE_PATH=/path/to/starraid-release.jks \
+ -PANDROID_KEYSTORE_PASSWORD=xxx \
+ -PANDROID_KEY_ALIAS=starraid \
+ -PANDROID_KEY_PASSWORD=xxx
+```
+
+产物:`app/build/outputs/apk/release/app-release.apk`
+
+## 可改项
+
+- 包名:`app/build.gradle.kts`(namespace/applicationId)与 `MainActivity.kt` 的 `package`
+- 应用名:`app/src/main/res/values/strings.xml`
+- 首页地址:`MainActivity.kt` 中 `HOME_URL`
+- 图标:`tools/gen_icons.py`(像素数据取自游戏 `js/sprites.js` 的 ship0,重新生成后需在 Android Studio 或 gradle 里 rebuild)
+
+## 版本发布
+
+- 递增 `app/build.gradle.kts` 的 `versionCode` / `versionName`
+- 顶部更新 `APP_CHANGELOG.md`
diff --git a/starraid-android/app/build.gradle.kts b/starraid-android/app/build.gradle.kts
new file mode 100644
index 0000000..e4b4b54
--- /dev/null
+++ b/starraid-android/app/build.gradle.kts
@@ -0,0 +1,66 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+fun resolveConfig(name: String): String? {
+ return (project.findProperty(name) as String?)?.takeIf { it.isNotBlank() }
+ ?: System.getenv(name)?.takeIf { it.isNotBlank() }
+}
+
+android {
+ namespace = "com.cyjai.starraid"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "com.cyjai.starraid"
+ minSdk = 24
+ targetSdk = 35
+ versionCode = 1
+ versionName = "1.0.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ signingConfigs {
+ create("release") {
+ val storeFilePath = resolveConfig("ANDROID_KEYSTORE_PATH")
+ val storePass = resolveConfig("ANDROID_KEYSTORE_PASSWORD")
+ val keyAliasValue = resolveConfig("ANDROID_KEY_ALIAS")
+ val keyPass = resolveConfig("ANDROID_KEY_PASSWORD")
+
+ if (!storeFilePath.isNullOrBlank()) {
+ storeFile = file(storeFilePath)
+ }
+ storePassword = storePass
+ keyAlias = keyAliasValue
+ keyPassword = keyPass
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ signingConfig = signingConfigs.getByName("release")
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.13.1")
+ implementation("androidx.appcompat:appcompat:1.7.0")
+ implementation("com.google.android.material:material:1.12.0")
+ implementation("androidx.activity:activity-ktx:1.9.2")
+}
diff --git a/starraid-android/app/proguard-rules.pro b/starraid-android/app/proguard-rules.pro
new file mode 100644
index 0000000..2cb8d49
--- /dev/null
+++ b/starraid-android/app/proguard-rules.pro
@@ -0,0 +1 @@
+# Intentionally minimal for WebView shell app
diff --git a/starraid-android/app/src/main/AndroidManifest.xml b/starraid-android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..626935c
--- /dev/null
+++ b/starraid-android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/starraid-android/app/src/main/java/com/cyjai/starraid/MainActivity.kt b/starraid-android/app/src/main/java/com/cyjai/starraid/MainActivity.kt
new file mode 100644
index 0000000..4aa5282
--- /dev/null
+++ b/starraid-android/app/src/main/java/com/cyjai/starraid/MainActivity.kt
@@ -0,0 +1,120 @@
+package com.cyjai.starraid
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import android.webkit.CookieManager
+import android.webkit.WebResourceRequest
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+
+/**
+ * Star Raid(星尘突击队)安卓客户端
+ * WebView 壳加载线上游戏,账号/排行榜/云存档与 Web 版完全互通。
+ */
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var webView: WebView
+ private var lastBackPressedAt = 0L
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // 沉浸式全屏:隐藏状态栏/导航栏,滑动手势可临时唤出
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ WindowInsetsControllerCompat(window, window.decorView).apply {
+ hide(WindowInsetsCompat.Type.systemBars())
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+ // 游戏期间屏幕常亮
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+ webView = WebView(this)
+ webView.isVerticalScrollBarEnabled = false
+ webView.isHorizontalScrollBarEnabled = false
+ webView.overScrollMode = View.OVER_SCROLL_NEVER
+ setContentView(webView)
+
+ CookieManager.getInstance().apply {
+ setAcceptCookie(true)
+ setAcceptThirdPartyCookies(webView, true)
+ }
+
+ webView.settings.apply {
+ javaScriptEnabled = true
+ domStorageEnabled = true
+ databaseEnabled = true
+ mediaPlaybackRequiresUserGesture = false
+ // 服务器静态资源为 immutable 长缓存;App 侧强制走网络,确保游戏更新即时生效
+ cacheMode = WebSettings.LOAD_NO_CACHE
+ useWideViewPort = true
+ loadWithOverviewMode = true
+ setSupportZoom(false)
+ builtInZoomControls = false
+ displayZoomControls = false
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ safeBrowsingEnabled = true
+ }
+ }
+ webView.clearCache(true)
+
+ webView.webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
+ val url = request?.url?.toString() ?: return false
+ if (url.startsWith(HOME_URL)) return false
+ // 站外链接交给系统浏览器
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+ } catch (_: Exception) {
+ // 忽略无浏览器可用的极端情况
+ }
+ return true
+ }
+ }
+
+ // 返回键:2 秒内再按一次退出,防误触
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ val now = System.currentTimeMillis()
+ if (now - lastBackPressedAt < 2000) {
+ finish()
+ } else {
+ lastBackPressedAt = now
+ Toast.makeText(this@MainActivity, "再按一次退出游戏", Toast.LENGTH_SHORT).show()
+ }
+ }
+ })
+
+ if (savedInstanceState == null) {
+ webView.loadUrl(HOME_URL)
+ } else {
+ webView.restoreState(savedInstanceState)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ webView.saveState(outState)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onDestroy() {
+ webView.destroy()
+ super.onDestroy()
+ }
+
+ companion object {
+ private const val HOME_URL = "https://starraid.cyjai.com"
+ }
+}
diff --git a/starraid-android/app/src/main/res/drawable/ic_launcher_fg.png b/starraid-android/app/src/main/res/drawable/ic_launcher_fg.png
new file mode 100644
index 0000000..553f0cd
Binary files /dev/null and b/starraid-android/app/src/main/res/drawable/ic_launcher_fg.png differ
diff --git a/starraid-android/app/src/main/res/drawable/ic_launcher_full.png b/starraid-android/app/src/main/res/drawable/ic_launcher_full.png
new file mode 100644
index 0000000..46d3af0
Binary files /dev/null and b/starraid-android/app/src/main/res/drawable/ic_launcher_full.png differ
diff --git a/starraid-android/app/src/main/res/drawable/ic_launcher_mono.png b/starraid-android/app/src/main/res/drawable/ic_launcher_mono.png
new file mode 100644
index 0000000..c756048
Binary files /dev/null and b/starraid-android/app/src/main/res/drawable/ic_launcher_mono.png differ
diff --git a/starraid-android/app/src/main/res/drawable/splash_bg.xml b/starraid-android/app/src/main/res/drawable/splash_bg.xml
new file mode 100644
index 0000000..619ef81
--- /dev/null
+++ b/starraid-android/app/src/main/res/drawable/splash_bg.xml
@@ -0,0 +1,11 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
diff --git a/starraid-android/app/src/main/res/drawable/splash_logo.png b/starraid-android/app/src/main/res/drawable/splash_logo.png
new file mode 100644
index 0000000..394ea01
Binary files /dev/null and b/starraid-android/app/src/main/res/drawable/splash_logo.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/starraid-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..b420871
--- /dev/null
+++ b/starraid-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/starraid-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/starraid-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..b420871
--- /dev/null
+++ b/starraid-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/starraid-android/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/starraid-android/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml
new file mode 100644
index 0000000..b6a0d17
--- /dev/null
+++ b/starraid-android/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/starraid-android/app/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml b/starraid-android/app/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml
new file mode 100644
index 0000000..b6a0d17
--- /dev/null
+++ b/starraid-android/app/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/starraid-android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/starraid-android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..b6bba39
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/starraid-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b6bba39
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/starraid-android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..513bf1b
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/starraid-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..513bf1b
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/starraid-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..cfa836e
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/starraid-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..cfa836e
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/starraid-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..6911b0d
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/starraid-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..6911b0d
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/starraid-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..543c821
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/starraid-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/starraid-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..543c821
Binary files /dev/null and b/starraid-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/starraid-android/app/src/main/res/values/colors.xml b/starraid-android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..84f84b8
--- /dev/null
+++ b/starraid-android/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #0A0A18
+ #0A0A18
+
diff --git a/starraid-android/app/src/main/res/values/strings.xml b/starraid-android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..0b0f291
--- /dev/null
+++ b/starraid-android/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ 星尘突击队
+
diff --git a/starraid-android/app/src/main/res/values/themes.xml b/starraid-android/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..cbed99a
--- /dev/null
+++ b/starraid-android/app/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/starraid-android/app/src/main/res/xml/network_security_config.xml b/starraid-android/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..66ef456
--- /dev/null
+++ b/starraid-android/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,7 @@
+
+
+
+
+ starraid.cyjai.com
+
+
diff --git a/starraid-android/build.gradle.kts b/starraid-android/build.gradle.kts
new file mode 100644
index 0000000..017d909
--- /dev/null
+++ b/starraid-android/build.gradle.kts
@@ -0,0 +1,4 @@
+plugins {
+ id("com.android.application") version "8.5.2" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.24" apply false
+}
diff --git a/starraid-android/gradle.properties b/starraid-android/gradle.properties
new file mode 100644
index 0000000..f0a2e55
--- /dev/null
+++ b/starraid-android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+kotlin.code.style=official
+android.nonTransitiveRClass=true
diff --git a/starraid-android/gradle/wrapper/gradle-wrapper.jar b/starraid-android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..980502d
Binary files /dev/null and b/starraid-android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/starraid-android/gradle/wrapper/gradle-wrapper.properties b/starraid-android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..128196a
--- /dev/null
+++ b/starraid-android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/starraid-android/gradlew b/starraid-android/gradlew
new file mode 100755
index 0000000..faf9300
--- /dev/null
+++ b/starraid-android/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/starraid-android/gradlew.bat b/starraid-android/gradlew.bat
new file mode 100644
index 0000000..9b42019
--- /dev/null
+++ b/starraid-android/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/starraid-android/settings.gradle.kts b/starraid-android/settings.gradle.kts
new file mode 100644
index 0000000..0287b35
--- /dev/null
+++ b/starraid-android/settings.gradle.kts
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "StarRaidApp"
+include(":app")
diff --git a/starraid-android/tools/gen_icons.py b/starraid-android/tools/gen_icons.py
new file mode 100644
index 0000000..ec5e402
--- /dev/null
+++ b/starraid-android/tools/gen_icons.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+"""Star Raid Android 图标生成:使用游戏初始飞机 falcon(ship0) 像素数据"""
+import os
+import random
+from PIL import Image, ImageDraw
+
+BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+RES = os.path.join(BASE, "app", "src", "main", "res")
+
+# —— 初始飞机 falcon (ship0) 像素数据(来自 star-raid/js/sprites.js)——
+PAL = {"K": "#14142b", "W": "#f2f0e6", "C": "#43e8ff", "R": "#e0393e", "D": "#8f1d2c", "O": "#ff9a3d"}
+SHIP = [
+ "......K......", ".....KWK.....", ".....KWK.....", ".....KCK.....",
+ "....KWWWK....", "....KRCRK....", "...KWRRRWK...", "..KWKRDRKWK..",
+ "..KWRRRRRWK..", ".KWKRRRRRKWK.", ".KRRKWWWKRRK.", "KKRDKWKWKDRKK",
+ "KD..KWKWK..DK", "K...KOKOK...K", "....KOKOK....",
+]
+BG_COLOR = (10, 10, 24) # 深空蓝黑
+
+def hex2rgb(h):
+ h = h.lstrip("#")
+ return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
+
+def render_ship(scale, mono=False):
+ w, h = len(SHIP[0]), len(SHIP)
+ img = Image.new("RGBA", (w * scale, h * scale), (0, 0, 0, 0))
+ d = ImageDraw.Draw(img)
+ for y, row in enumerate(SHIP):
+ for x, ch in enumerate(row):
+ if ch != ".":
+ c = (255, 255, 255) if mono else hex2rgb(PAL[ch])
+ d.rectangle([x * scale, y * scale, (x + 1) * scale - 1, (y + 1) * scale - 1], fill=c + (255,))
+ return img
+
+def stars_bg(size, seed=7, n=120):
+ rnd = random.Random(seed)
+ img = Image.new("RGB", (size, size), BG_COLOR)
+ d = ImageDraw.Draw(img)
+ for _ in range(n):
+ x, y = rnd.randint(0, size - 1), rnd.randint(0, size - 1)
+ r = rnd.choice([1, 1, 1, 2])
+ col = rnd.choice([(242, 240, 230), (67, 232, 255), (255, 255, 255), (150, 170, 220)])
+ d.rectangle([x, y, x + r, y + r], fill=col)
+ for _ in range(6):
+ x, y = rnd.randint(20, size - 20), rnd.randint(20, size - 20)
+ d.rectangle([x - 1, y, x + 1, y], fill=(242, 240, 230))
+ d.rectangle([x, y - 1, x, y + 1], fill=(242, 240, 230))
+ return img
+
+def paste_center(canvas, layer, scale):
+ fg = render_ship(scale)
+ x = (canvas.width - fg.width) // 2
+ y = (canvas.height - fg.height) // 2
+ canvas.alpha_composite(fg, (x, y))
+
+def save_png(img, path):
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ img.save(path, "PNG")
+ print("saved", path, img.size)
+
+# 1) adaptive icon foreground:512 透明底 + 飞机 16x(176x240,居中,安全区内)
+fg = Image.new("RGBA", (512, 512), (0, 0, 0, 0))
+paste_center(fg, render_ship, 16)
+save_png(fg, os.path.join(RES, "drawable", "ic_launcher_fg.png"))
+
+# 2) mono(Android 13 主题图标):白色飞机
+mono = Image.new("RGBA", (512, 512), (0, 0, 0, 0))
+mono_ship = render_ship(16, mono=True)
+mono.alpha_composite(mono_ship, ((512 - mono_ship.width) // 2, (512 - mono_ship.height) // 2))
+save_png(mono, os.path.join(RES, "drawable", "ic_launcher_mono.png"))
+
+# 3) 完整图标(背景 + 飞机):用于 legacy mipmap
+full = stars_bg(512).convert("RGBA")
+paste_center(full, render_ship, 16)
+save_png(full, os.path.join(RES, "drawable", "ic_launcher_full.png"))
+
+# 4) legacy mipmap 各尺寸
+mipmap_sizes = {"mdpi": 48, "hdpi": 72, "xhdpi": 96, "xxhdpi": 144, "xxxhdpi": 192}
+for dpi, size in mipmap_sizes.items():
+ icon = full.resize((size, size), Image.NEAREST)
+ for name in ("ic_launcher", "ic_launcher_round"):
+ save_png(icon, os.path.join(RES, "mipmap-" + dpi, name + ".png"))
+
+# 5) splash logo:透明底飞机大图(windowBackground layer-list 用)
+splash = Image.new("RGBA", (512, 512), (0, 0, 0, 0))
+paste_center(splash, render_ship, 24)
+save_png(splash, os.path.join(RES, "drawable", "splash_logo.png"))
+
+print("DONE")