Pārlūkot izejas kodu

backup gameplay polish

wenxianchao 1 mēnesi atpakaļ
vecāks
revīzija
8d59ad46bb

BIN
assets/resources/sprites/imagegen/frames/boss_core_enraged.png


BIN
assets/resources/sprites/imagegen/frames/obstacle_block.png


+ 42 - 0
assets/resources/sprites/imagegen/frames/obstacle_block.png.meta

@@ -0,0 +1,42 @@
+{
+  "ver": "1.0.27",
+  "importer": "image",
+  "imported": true,
+  "uuid": "3d156591-0d17-4d81-8869-0f37aaf79db5",
+  "files": [
+    ".json",
+    ".png"
+  ],
+  "subMetas": {
+    "6c48a": {
+      "importer": "texture",
+      "uuid": "3d156591-0d17-4d81-8869-0f37aaf79db5@6c48a",
+      "displayName": "obstacle_block",
+      "id": "6c48a",
+      "name": "texture",
+      "userData": {
+        "wrapModeS": "repeat",
+        "wrapModeT": "repeat",
+        "minfilter": "linear",
+        "magfilter": "linear",
+        "mipfilter": "none",
+        "anisotropy": 0,
+        "isUuid": true,
+        "imageUuidOrDatabaseUri": "3d156591-0d17-4d81-8869-0f37aaf79db5",
+        "visible": false
+      },
+      "ver": "1.0.22",
+      "imported": true,
+      "files": [
+        ".json"
+      ],
+      "subMetas": {}
+    }
+  },
+  "userData": {
+    "type": "texture",
+    "fixAlphaTransparencyArtifacts": false,
+    "hasAlpha": true,
+    "redirect": "3d156591-0d17-4d81-8869-0f37aaf79db5@6c48a"
+  }
+}

BIN
assets/resources/sprites/imagegen/frames/spore_idle.png


BIN
assets/resources/sprites/imagegen/frames/spore_walk_1.png


BIN
assets/resources/sprites/imagegen/frames/spore_walk_2.png


+ 241 - 66
assets/scripts/SteelAssaultGame.ts

@@ -52,6 +52,13 @@ interface Obstacle {
   height: number;
 }
 
+interface UiRect {
+  x: number;
+  y: number;
+  width: number;
+  height: number;
+}
+
 interface Bullet {
   x: number;
   y: number;
@@ -63,6 +70,7 @@ interface Bullet {
   pierce: boolean;
   kind: WeaponId | 'enemy';
   homing?: boolean;
+  hitEnemies?: Set<Enemy>;
 }
 
 const W = 1280;
@@ -77,11 +85,14 @@ const END_X = U(3320);
 const ACTOR_RENDER_Y_OFFSET = 0;
 const VISUAL_LANE_Y = LANE_Y + ACTOR_RENDER_Y_OFFSET;
 const ENEMY_BULLET_HIT_RADIUS = U(24);
+const BOSS_MAX_HP = 300;
+const BOSS_PHASE_TWO_RATIO = 0.6;
+const BOSS_ENRAGE_RATIO = 0.2;
 
 const WEAPONS = {
   rifle: { name: 'RIFLE', cd: 0.11, damage: 4, speed: U(880) },
   flame: { name: 'FLAME', cd: 0.055, damage: 1.4, speed: U(520) },
-  rail: { name: 'RAIL', cd: 0.58, damage: 18, speed: U(1250) }
+  rail: { name: 'RAIL', cd: 0.58, damage: 5, speed: U(1250) }
 };
 
 function enemyVisualProjectileOffset(type: EnemyType) {
@@ -100,8 +111,10 @@ export class SteelAssaultGame extends Component {
   private spriteLayer = new Node('RuntimeSprites');
   private propLayer = new Node('RuntimeProps');
   private uiArtLayer = new Node('RuntimeUIArt');
+  private outcomeButtonLayer = new Node('RuntimeOutcomeButtons');
   private hudNode = new Node('RuntimeHUD');
   private graphics!: Graphics;
+  private outcomeButtonGraphics!: Graphics;
   private hud!: Label;
   private hpHud!: Label;
   private scoreHud!: Label;
@@ -109,6 +122,8 @@ export class SteelAssaultGame extends Component {
   private outcomeTitle!: Label;
   private outcomeStats!: Label;
   private outcomeAction!: Label;
+  private outcomeNextAction!: Label;
+  private outcomeRestartAction!: Label;
   private keys = new Set<KeyCode>();
   private weapon: WeaponId = 'rifle';
   private player = { x: U(-360), y: LANE_Y, vx: 0, vy: 0, hp: 6, face: 1, grounded: true };
@@ -128,6 +143,7 @@ export class SteelAssaultGame extends Component {
   private heroSprite: Sprite | null = null;
   private enemySprites = new Map<Enemy, Sprite>();
   private bulletSprites = new Map<Bullet, Sprite>();
+  private obstacleSprites = new Map<Obstacle, Sprite>();
   private heroFrames: SpriteFrame[] = [];
   private sporeFrames: SpriteFrame[] = [];
   private wingFrames: SpriteFrame[] = [];
@@ -182,10 +198,11 @@ export class SteelAssaultGame extends Component {
     { x: U(520), y: LANE_Y, width: U(118), height: U(58) },
     { x: U(1120), y: LANE_Y, width: U(138), height: U(72) },
     { x: U(2220), y: LANE_Y, width: U(132), height: U(64) },
-    { x: U(2650), y: LANE_Y, width: U(122), height: U(70) },
-    { x: U(2860), y: LANE_Y, width: U(112), height: U(58) },
-    { x: U(3025), y: LANE_Y, width: U(132), height: U(92) },
-    { x: U(3160), y: LANE_Y, width: U(112), height: U(126) }
+    { x: U(2600), y: LANE_Y + U(8), width: U(132), height: U(58) },
+    { x: U(2760), y: LANE_Y + U(78), width: U(126), height: U(56) },
+    { x: U(2940), y: LANE_Y + U(48), width: U(132), height: U(58) },
+    { x: U(3100), y: LANE_Y + U(132), width: U(138), height: U(56) },
+    { x: U(3190), y: LANE_Y + U(82), width: U(132), height: U(72) }
   ];
 
   onLoad() {
@@ -283,6 +300,12 @@ export class SteelAssaultGame extends Component {
     this.uiArtLayer.layer = Layers.Enum.UI_2D;
     this.uiArtLayer.addComponent(UITransform).setContentSize(this.viewportWidth, H);
 
+    this.node.addChild(this.outcomeButtonLayer);
+    this.outcomeButtonLayer.layer = Layers.Enum.UI_2D;
+    this.outcomeButtonLayer.addComponent(UITransform).setContentSize(this.viewportWidth, H);
+    this.outcomeButtonGraphics = this.outcomeButtonLayer.addComponent(Graphics);
+    this.bindOutcomeRestartNode(this.outcomeButtonLayer);
+
     this.node.addChild(this.hudNode);
     this.hudNode.layer = Layers.Enum.UI_2D;
     this.hudNode.addComponent(UITransform).setContentSize(W, U(64));
@@ -354,6 +377,26 @@ export class SteelAssaultGame extends Component {
     this.outcomeAction.lineHeight = 36;
     this.outcomeAction.color = new Color(14, 29, 36, 255);
 
+    const outcomeNextNode = new Node('OutcomeNextAction');
+    outcomeNextNode.layer = Layers.Enum.UI_2D;
+    this.node.addChild(outcomeNextNode);
+    outcomeNextNode.addComponent(UITransform).setContentSize(U(220), U(64));
+    outcomeNextNode.setPosition(U(-126), U(-100), 30);
+    this.outcomeNextAction = outcomeNextNode.addComponent(Label);
+    this.outcomeNextAction.fontSize = 26;
+    this.outcomeNextAction.lineHeight = 32;
+    this.outcomeNextAction.color = new Color(23, 32, 38, 255);
+
+    const outcomeRestartNode = new Node('OutcomeRestartAction');
+    outcomeRestartNode.layer = Layers.Enum.UI_2D;
+    this.node.addChild(outcomeRestartNode);
+    outcomeRestartNode.addComponent(UITransform).setContentSize(U(220), U(64));
+    outcomeRestartNode.setPosition(U(126), U(-100), 30);
+    this.outcomeRestartAction = outcomeRestartNode.addComponent(Label);
+    this.outcomeRestartAction.fontSize = 26;
+    this.outcomeRestartAction.lineHeight = 32;
+    this.outcomeRestartAction.color = new Color(235, 252, 255, 255);
+
     view.resizeWithBrowserSize(true);
 
   this.applyFullscreenLandscapeLayout();
@@ -553,6 +596,7 @@ if (!this.backgroundSprite) return;
     for (const name of ui) {
       this.loadFrame(`sprites/imagegen/ui_scene/ui/${name}`, (frame) => this.uiFrames.set(name, frame));
     }
+    this.loadFrame('sprites/imagegen/frames/obstacle_block', (frame) => this.uiFrames.set('obstacle_block', frame));
   }
 
   private loadFrame(path: string, use: (frame: SpriteFrame) => void) {
@@ -661,10 +705,8 @@ if (!this.backgroundSprite) return;
     }
   }
 
-  private randomEnemyType(seed: number): Exclude<EnemyType, 'boss'> {
-    const types: Array<Exclude<EnemyType, 'boss'>> = ['spore', 'wing', 'turret'];
-    const value = Math.abs(Math.sin(seed * 12.9898 + this.score * 0.01 + this.player.x * 0.017));
-    return types[Math.floor(value * types.length) % types.length];
+  private randomEnemyType(_seed: number): Exclude<EnemyType, 'boss'> {
+    return 'spore';
   }
 
   private updateObstacleHits() {
@@ -726,7 +768,8 @@ if (!this.backgroundSprite) return;
         this.bossTime += dt;
         enemy.vx = Math.sin(this.bossTime * 0.7) * U(28);
         enemy.y = BOSS_Y + Math.sin(this.bossTime * 1.1) * U(10);
-        const phase = enemy.hp > 115 ? 1 : enemy.hp > 55 ? 2 : 3;
+        const hpRatio = enemy.hp / enemy.maxHp;
+        const phase = hpRatio <= BOSS_ENRAGE_RATIO ? 3 : hpRatio <= BOSS_PHASE_TWO_RATIO ? 2 : 1;
         if (enemy.cd <= 0 && this.canEnemyShoot(enemy, U(980))) {
           const shotX = enemy.x - U(115);
           const targetX = this.player.x;
@@ -734,8 +777,8 @@ if (!this.backgroundSprite) return;
           this.enemyShotAt(shotX, enemy.y + this.enemyShotOffset(enemy) + U(46), targetX, targetY, phase >= 3 ? U(255) : U(220), 2.6);
           if (phase >= 2) this.enemyShotAt(shotX, enemy.y + this.enemyShotOffset(enemy) - U(18), targetX, targetY + U(34), U(205), 2.45);
           if (phase >= 3) this.enemyShotAt(shotX, enemy.y + this.enemyShotOffset(enemy) + U(88), targetX, targetY - U(28), U(190), 2.35);
-          if (phase >= 3 && this.enemies.filter((e) => e.type === 'spore').length < 4) this.spawnEnemy('spore', enemy.x - U(140), 0);
-          enemy.cd = phase === 1 ? 1.75 : phase === 2 ? 1.45 : 1.18;
+          if (phase >= 3 && this.enemies.filter((e) => e.type === 'spore').length < 5) this.spawnEnemy('spore', enemy.x - U(180), 0);
+          enemy.cd = phase === 1 ? 1.65 : phase === 2 ? 1.28 : 0.95;
         }
       }
       enemy.x += enemy.vx * dt;
@@ -767,9 +810,15 @@ if (!this.backgroundSprite) return;
       if (bullet.player) {
         for (const enemy of this.enemies) {
           const radius = enemy.type === 'boss' ? U(155) : U(44);
+          if (bullet.pierce && bullet.hitEnemies?.has(enemy)) continue;
           if (enemy.hp > 0 && this.hit(bullet.x, bullet.y, enemy.x, enemy.y, radius)) {
             enemy.hp -= bullet.damage;
-            if (!bullet.pierce) bullet.life = 0;
+            if (bullet.pierce) {
+              if (!bullet.hitEnemies) bullet.hitEnemies = new Set<Enemy>();
+              bullet.hitEnemies.add(enemy);
+            } else {
+              bullet.life = 0;
+            }
           }
         }
       } else if (this.hit(bullet.x, bullet.y, this.player.x, this.player.y, ENEMY_BULLET_HIT_RADIUS)) {
@@ -831,7 +880,7 @@ if (!this.backgroundSprite) return;
       player: false,
       pierce: false,
       kind: 'enemy',
-      homing: true
+      homing: false
     });
   }
 
@@ -849,7 +898,7 @@ if (!this.backgroundSprite) return;
   }
 
   private spawnBoss() {
-    const boss = { type: 'boss' as const, x: U(3290), y: BOSS_Y, hp: 180, maxHp: 180, vx: 0, cd: 0.5 };
+    const boss = { type: 'boss' as const, x: U(3290), y: BOSS_Y, hp: BOSS_MAX_HP, maxHp: BOSS_MAX_HP, vx: 0, cd: 0.5 };
     this.boss = boss;
     this.enemies.push(boss);
     this.playSfx('boss_alert', 0.95);
@@ -896,17 +945,17 @@ if (!this.backgroundSprite) return;
     }
     const half = this.visibleWorldWidth() / 2;
     const hpX = -half + 18;
-    const scoreX = half - 246;
+    const scorePanel = this.scoreWeaponPanelRect();
     g.fillColor = new Color(5, 12, 22, 205);
     g.fillRect(hpX, H / 2 - U(54), 232, 56);
-    g.fillRect(scoreX, H / 2 - U(54), 228, 56);
+    g.fillRect(scorePanel.x, scorePanel.y, scorePanel.width, scorePanel.height);
     g.fillColor = new Color(255, 207, 72, 230);
     g.fillRect(hpX, H / 2 - U(54), 4, 56);
-    g.fillRect(scoreX, H / 2 - U(54), 4, 56);
+    g.fillRect(scorePanel.x, scorePanel.y, 4, scorePanel.height);
     g.strokeColor = new Color(64, 238, 255, 190);
     g.lineWidth = 2;
     g.rect(hpX, H / 2 - U(54), 232, 56);
-    g.rect(scoreX, H / 2 - U(54), 228, 56);
+    g.rect(scorePanel.x, scorePanel.y, scorePanel.width, scorePanel.height);
     g.stroke();
   }
 
@@ -969,7 +1018,16 @@ if (!this.backgroundSprite) return;
   }
 
   private drawObstacleSolids(g: Graphics) {
-    if (this.state !== 'playing') return;
+    if (this.state !== 'playing') {
+      this.hideObstacleSprites();
+      return;
+    }
+    const frame = this.uiFrames.get('obstacle_block');
+    if (frame) {
+      this.syncObstacleSprites(frame);
+      return;
+    }
+    this.hideObstacleSprites();
     for (const obstacle of this.obstacles) {
       const x = obstacle.x - this.cameraX;
       const half = this.visibleWorldWidth() / 2;
@@ -999,31 +1057,51 @@ if (!this.backgroundSprite) return;
     }
   }
 
+  private syncObstacleSprites(frame: SpriteFrame) {
+    const half = this.visibleWorldWidth() / 2;
+    for (const obstacle of this.obstacles) {
+      let sprite = this.obstacleSprites.get(obstacle);
+      if (!sprite) {
+        sprite = this.createSprite('ObstacleBlock', this.propLayer);
+        sprite.sizeMode = Sprite.SizeMode.CUSTOM;
+        this.obstacleSprites.set(obstacle, sprite);
+      }
+      const x = obstacle.x - this.cameraX;
+      const visible = x >= -half - U(180) && x <= half + U(180);
+      sprite.spriteFrame = frame;
+      sprite.node.getComponent(UITransform)?.setContentSize(obstacle.width, obstacle.height);
+      sprite.node.setPosition(x, obstacle.y + obstacle.height / 2 + ACTOR_RENDER_Y_OFFSET, 2);
+      sprite.node.active = visible && this.state === 'playing';
+    }
+  }
+
+  private hideObstacleSprites() {
+    for (const sprite of this.obstacleSprites.values()) sprite.node.active = false;
+  }
+
   private drawControlButtons(g: Graphics) {
     if (this.state !== 'playing') return;
-    const x = this.visibleWorldWidth() / 2 - 228;
-    const y = H / 2 - 144;
-    const slotWidth = 60;
-    const slotGap = 7;
-    g.fillColor = new Color(8, 21, 29, 220);
-    g.fillRect(x, y, 204, 72);
-    g.strokeColor = new Color(64, 238, 255, 230);
-    g.lineWidth = 3;
-    g.rect(x, y, 204, 72);
-    g.stroke();
+    const panel = this.scoreWeaponPanelRect();
+    const slotWidth = U(54);
+    const slotHeight = U(36);
+    const y = panel.y + U(18);
     (Object.keys(WEAPONS) as WeaponId[]).forEach((id, index) => {
-      const sx = x + 14 + index * (slotWidth + slotGap);
+      const sx = panel.x + U(68) + index * U(64) - slotWidth / 2;
       const active = this.weapon === id;
       g.fillColor = active ? this.weaponColor(id, 235) : new Color(17, 34, 42, 225);
-      g.fillRect(sx, y + 14, slotWidth, 42);
+      g.fillRect(sx, y, slotWidth, slotHeight);
       g.strokeColor = active ? new Color(255, 255, 255, 245) : new Color(64, 238, 255, 120);
       g.lineWidth = active ? 4 : 2;
-      g.rect(sx, y + 14, slotWidth, 42);
+      g.rect(sx, y, slotWidth, slotHeight);
       g.stroke();
     });
   }
 
   private drawActors(g: Graphics) {
+    if (this.isOutcomeState()) {
+      this.hideActorSprites();
+      return;
+    }
     if (this.spritesReady) {
       this.drawSpriteActors(g);
       return;
@@ -1115,6 +1193,13 @@ if (!this.backgroundSprite) return;
     }
   }
 
+  private hideActorSprites() {
+    if (this.heroSprite) this.heroSprite.node.active = false;
+    if (this.heroWeaponSprite) this.heroWeaponSprite.node.active = false;
+    for (const sprite of this.enemySprites.values()) sprite.node.active = false;
+    for (const sprite of this.bulletSprites.values()) sprite.node.active = false;
+  }
+
   private syncHeroSprite() {
     if (!this.heroSprite) this.heroSprite = this.createSprite('HeroSprite');
     const frame = this.getHeroFrame();
@@ -1161,7 +1246,9 @@ if (!this.backgroundSprite) return;
       const frame = this.getEnemyFrame(enemy);
       if (frame) sprite.spriteFrame = frame;
       sprite.node.setPosition(enemy.x - this.cameraX, this.enemyRenderY(enemy), 0);
-      const face = enemy.x > this.player.x ? -1 : 1;
+      const face = enemy.type === 'spore'
+        ? (enemy.x > this.player.x ? 1 : -1)
+        : (enemy.x > this.player.x ? -1 : 1);
       const scale = enemy.type === 'boss' ? US(1.06) : enemy.type === 'turret' ? US(0.44) : enemy.type === 'wing' ? US(0.41) : US(0.51);
       sprite.node.setScale(new Vec3(scale * face, scale, 1));
       sprite.node.active = true;
@@ -1209,6 +1296,7 @@ if (!this.backgroundSprite) return;
     this.syncWeaponArt();
     this.syncTouchControls();
     this.syncOutcomePanelArt();
+    this.syncOutcomeButtons();
     this.syncRetryArt();
     this.syncOutcomeText();
     if (this.failOverlaySprite) this.failOverlaySprite.node.active = this.state === 'lose';
@@ -1216,12 +1304,23 @@ if (!this.backgroundSprite) return;
 
   private syncResponsiveUiLayout() {
     const half = this.visibleWorldWidth() / 2;
+    const scorePanel = this.scoreWeaponPanelRect();
     this.hpHud.node.setPosition(-half + U(112), H / 2 - U(30), 8);
-    this.scoreHud.node.setPosition(half - U(210), H / 2 - U(38), 8);
-    this.weaponHud.node.setPosition(half - U(210), H / 2 - U(70), 24);
+    this.scoreHud.node.setPosition(scorePanel.x + scorePanel.width / 2, scorePanel.y + scorePanel.height - U(34), 8);
+    this.weaponHud.node.setPosition(scorePanel.x + scorePanel.width / 2, scorePanel.y + U(70), 24);
     if (this.hpPanelSprite) this.hpPanelSprite.node.setPosition(-half + U(112), H / 2 - U(30), 6);
   }
 
+  private scoreWeaponPanelRect(): UiRect {
+    const half = this.visibleWorldWidth() / 2;
+    return {
+      x: half - U(430),
+      y: H / 2 - U(150),
+      width: U(306),
+      height: U(112)
+    };
+  }
+
   private syncTouchControls() {
     const visible = this.state === 'playing';
     this.touchLeftSprite = this.syncMobileButton(
@@ -1315,6 +1414,7 @@ if (!this.backgroundSprite) return;
     if (!frame) return sprite;
     if (!sprite) {
       sprite = this.createSprite(name, this.uiArtLayer);
+      sprite.sizeMode = Sprite.SizeMode.CUSTOM;
       this.bindTouchControlNode(sprite.node, onPress, onRelease, repeatOnMove);
     }
     sprite.spriteFrame = frame;
@@ -1364,8 +1464,7 @@ if (!this.backgroundSprite) return;
     const restart = (event: EventTouch | EventMouse) => {
       if (!this.isOutcomeState()) return;
       this.unlockAudio();
-      this.reset();
-      this.stopTouchEvent(event);
+      if (this.handleOutcomeAction(event)) this.stopTouchEvent(event);
     };
     node.on(Node.EventType.TOUCH_START, restart, this);
     node.on(Node.EventType.MOUSE_DOWN, restart, this);
@@ -1377,6 +1476,31 @@ if (!this.backgroundSprite) return;
     stoppable.propagationStopped = true;
   }
 
+  private handleOutcomeAction(event: EventTouch | EventMouse) {
+    const { x, y } = this.pointerToWorldPoint(event);
+    if (this.state === 'win') {
+      if (this.pointInRect(x, y, this.outcomeNextRect())) {
+        this.state = 'next';
+        this.clearMobileControls();
+        return true;
+      }
+      if (this.pointInRect(x, y, this.outcomeRestartRect())) {
+        this.reset();
+        return true;
+      }
+      return false;
+    }
+    if ((this.state === 'lose' || this.state === 'next') && this.pointInRect(x, y, this.outcomeSingleRestartRect())) {
+      this.reset();
+      return true;
+    }
+    return false;
+  }
+
+  private pointInRect(x: number, y: number, rect: UiRect) {
+    return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
+  }
+
   private setMobileDirectionFromTouchPoint(event: EventTouch | EventMouse) {
     this.clearMobileMove();
     const action = this.resolveDirectionalButtonFromPoints(this.pointerWorldPointCandidates(event));
@@ -1415,6 +1539,7 @@ if (!this.backgroundSprite) return;
   private syncWeaponArt() {
     if (this.weaponSprite) this.weaponSprite.node.active = false;
     const ids = Object.keys(WEAPONS) as WeaponId[];
+    const panel = this.scoreWeaponPanelRect();
     for (let index = 0; index < ids.length; index++) {
       const id = ids[index];
       const frame = this.uiFrames.get(`weapon_${id}`);
@@ -1426,7 +1551,7 @@ if (!this.backgroundSprite) return;
       }
       const active = id === this.weapon;
       sprite.spriteFrame = frame;
-      sprite.node.setPosition(this.visibleWorldWidth() / 2 - U(184) + index * U(67), H / 2 - U(110) + (active ? 3 : 0), 20);
+      sprite.node.setPosition(panel.x + U(68) + index * U(64), panel.y + U(36) + (active ? 3 : 0), 20);
       sprite.node.setScale(new Vec3(active ? US(0.146) : US(0.125), active ? US(0.146) : US(0.125), 1));
       sprite.node.active = this.state === 'playing';
     }
@@ -1470,7 +1595,7 @@ if (!this.backgroundSprite) return;
     if (enemy.type === 'spore') return this.sporeFrames[Math.floor(this.elapsed * 7) % 3];
     if (enemy.type === 'wing') return this.wingFrames[Math.floor(this.elapsed * 10) % 3];
     if (enemy.type === 'turret') return this.turretFrames[enemy.cd > 0.82 ? 2 : Math.floor(this.elapsed * 2) % 2];
-    if (enemy.hp < enemy.maxHp * 0.33) return this.bossFrames[3];
+    if (enemy.hp <= enemy.maxHp * BOSS_ENRAGE_RATIO) return this.bossFrames[3];
     if (enemy.cd > 0.72) return this.bossFrames[2];
     return this.bossFrames[Math.floor(this.elapsed * 2) % 2];
   }
@@ -1529,33 +1654,89 @@ if (!this.backgroundSprite) return;
     g.fillRect(-330, -158, 660, 6);
   }
 
+  private syncOutcomeButtons() {
+    if (!this.outcomeButtonGraphics) return;
+    const g = this.outcomeButtonGraphics;
+    g.clear();
+    const visible = this.isOutcomeState();
+    this.outcomeButtonLayer.active = visible;
+    if (!visible) return;
+
+    if (this.state === 'win') {
+      this.drawOutcomeButton(g, this.outcomeNextRect(), new Color(255, 211, 88, 238), new Color(255, 248, 190, 255), new Color(24, 136, 169, 210));
+      this.drawOutcomeButton(g, this.outcomeRestartRect(), new Color(17, 48, 66, 238), new Color(86, 230, 255, 235), new Color(255, 185, 82, 215));
+      return;
+    }
+
+    const restartColor = this.state === 'lose'
+      ? new Color(116, 25, 31, 238)
+      : new Color(17, 48, 66, 238);
+    const accentColor = this.state === 'lose'
+      ? new Color(255, 86, 72, 235)
+      : new Color(255, 185, 82, 215);
+    this.drawOutcomeButton(g, this.outcomeSingleRestartRect(), restartColor, new Color(255, 226, 205, 235), accentColor);
+  }
+
+  private drawOutcomeButton(g: Graphics, rect: UiRect, fill: Color, stroke: Color, accent: Color) {
+    g.fillColor = fill;
+    g.fillRect(rect.x, rect.y, rect.width, rect.height);
+    g.fillColor = accent;
+    g.fillRect(rect.x, rect.y, U(7), rect.height);
+    g.fillRect(rect.x + rect.width - U(7), rect.y, U(7), rect.height);
+    g.strokeColor = stroke;
+    g.lineWidth = 3;
+    g.rect(rect.x, rect.y, rect.width, rect.height);
+    g.stroke();
+  }
+
+  private outcomeNextRect(): UiRect {
+    return { x: U(-226), y: U(-130), width: U(196), height: U(58) };
+  }
+
+  private outcomeRestartRect(): UiRect {
+    return { x: U(30), y: U(-130), width: U(196), height: U(58) };
+  }
+
+  private outcomeSingleRestartRect(): UiRect {
+    return { x: U(-118), y: U(-130), width: U(236), height: U(58) };
+  }
+
   private syncOutcomeText() {
     const visible = this.isOutcomeState();
+    const win = this.state === 'win';
     this.outcomeTitle.node.active = visible;
     this.outcomeStats.node.active = visible;
-    this.outcomeAction.node.active = visible;
+    this.outcomeAction.node.active = visible && !win;
+    this.outcomeNextAction.node.active = visible && win;
+    this.outcomeRestartAction.node.active = visible && win;
     if (!visible) return;
     if (this.state === 'win') {
-      this.outcomeTitle.string = 'MISSION CLEAR';
-      this.outcomeStats.string = `SCORE ${this.score.toString().padStart(5, '0')}    HP ${Math.max(0, this.player.hp)}/6`;
-      this.outcomeAction.string = 'TAP TO RESTART';
+      this.outcomeTitle.string = '通关成功';
+      this.outcomeStats.string = `得分 ${this.score.toString().padStart(5, '0')}    生命 ${Math.max(0, this.player.hp)}/6`;
+      this.outcomeAction.string = '';
+      this.outcomeNextAction.string = '选择下一关';
+      this.outcomeRestartAction.string = '重新开始';
+      this.outcomeNextAction.color = new Color(22, 31, 36, 255);
+      this.outcomeRestartAction.color = new Color(232, 252, 255, 255);
       this.outcomeTitle.color = new Color(255, 232, 126, 255);
       this.outcomeStats.color = new Color(225, 250, 255, 255);
-      this.outcomeAction.color = new Color(14, 29, 36, 255);
+      this.outcomeAction.color = new Color(255, 238, 154, 255);
       return;
     }
     if (this.state === 'lose') {
-      this.outcomeTitle.string = 'MISSION FAILED';
-      this.outcomeStats.string = `SCORE ${this.score.toString().padStart(5, '0')}`;
-      this.outcomeAction.string = 'TAP TO RESTART';
+      this.outcomeTitle.string = '挑战失败';
+      this.outcomeStats.string = `得分 ${this.score.toString().padStart(5, '0')}`;
+      this.outcomeAction.string = '重新开始';
+      this.outcomeAction.node.setPosition(0, U(-100), 28);
       this.outcomeTitle.color = new Color(255, 92, 82, 255);
       this.outcomeStats.color = new Color(225, 250, 255, 255);
       this.outcomeAction.color = new Color(255, 226, 205, 255);
       return;
     }
-    this.outcomeTitle.string = 'READY';
-    this.outcomeStats.string = 'LEVEL 1';
-    this.outcomeAction.string = 'TAP TO RESTART';
+    this.outcomeTitle.string = '下一关筹备中';
+    this.outcomeStats.string = '请先返回第一关继续体验';
+    this.outcomeAction.string = '重新开始';
+    this.outcomeAction.node.setPosition(0, U(-100), 28);
   }
 
   private isOutcomeState() {
@@ -1563,7 +1744,7 @@ if (!this.backgroundSprite) return;
   }
 
   private drawHud() {
-    let status = this.boss && this.boss.hp > 0 ? `BOSS ${Math.ceil(this.boss.hp)}/180` : '';
+    let status = this.boss && this.boss.hp > 0 ? `BOSS ${Math.ceil(this.boss.hp)}/${this.boss.maxHp}` : '';
     if (this.isOutcomeState()) status = '';
     this.hud.string = status;
     this.hudNode.setPosition(0, this.state === 'lose' ? -40 : H / 2 - U(34), 12);
@@ -1572,9 +1753,7 @@ if (!this.backgroundSprite) return;
     this.hud.color = this.state === 'lose' ? new Color(255, 240, 180, 255) : new Color(255, 255, 255, 255);
     this.scoreHud.string = this.state === 'playing' ? `得分 ${this.score.toString().padStart(5, '0')}` : '';
     this.hpHud.string = this.state === 'playing' ? `生命 ${Math.max(0, this.player.hp)}/6` : '';
-    this.weaponHud.string = this.state === 'playing'
-      ? '按Q切换'
-      : '';
+    this.weaponHud.string = '';
   }
 
   private hit(ax: number, ay: number, bx: number, by: number, radius: number) {
@@ -1595,16 +1774,12 @@ if (!this.backgroundSprite) return;
 
   private onTouchStart(event: EventTouch) {
     this.unlockAudio();
-    const { x, y } = this.touchToGamePoint(event);
     if (this.state === 'playing') {
       this.handlePointerStart(this.pointerIdFromTouch(event), event);
       return;
     }
     if (!this.isOutcomeState()) return;
-    const inOutcome = Math.abs(x) < U(410) && y > U(-240) && y < U(190);
-    if (inOutcome) {
-      this.reset();
-    }
+    if (this.handleOutcomeAction(event)) this.stopTouchEvent(event);
   }
 
   private onTouchMove(event: EventTouch) {
@@ -1673,9 +1848,9 @@ if (!this.backgroundSprite) return;
   }
 
   private resolveDirectionalButtonFromPoints(points: Array<{ x: number; y: number }>): Extract<MobileAction, 'left' | 'right' | 'up'> | null {
-    if (this.closestPointInCircle(points, this.jumpButtonCenter(), U(58))) return 'up';
-    if (this.closestPointInCircle(points, this.leftButtonCenter(), U(58))) return 'left';
-    if (this.closestPointInCircle(points, this.rightButtonCenter(), U(58))) return 'right';
+    if (this.closestPointInCircle(points, this.jumpButtonCenter(), U(64))) return 'up';
+    if (this.closestPointInCircle(points, this.leftButtonCenter(), U(64))) return 'left';
+    if (this.closestPointInCircle(points, this.rightButtonCenter(), U(64))) return 'right';
     return null;
   }
 
@@ -1791,17 +1966,17 @@ if (!this.backgroundSprite) return;
 
   private leftButtonCenter() {
   const half = this.visibleWorldWidth() / 2;
-  return { x: -half + U(96), y: U(-188) };
+  return { x: -half + U(72), y: U(-208) };
 }
 
  private rightButtonCenter() {
   const half = this.visibleWorldWidth() / 2;
-  return { x: -half + U(190), y: U(-188) };
+  return { x: -half + U(214), y: U(-208) };
 }
 
   private jumpButtonCenter() {
   const half = this.visibleWorldWidth() / 2;
-  return { x: -half + U(143), y: U(-88) };
+  return { x: -half + U(143), y: U(-60) };
 }
 
  private fireButtonCenter() {

+ 6 - 11
profiles/v2/packages/builder.json

@@ -18,14 +18,14 @@
   },
   "BuildTaskManager": {
     "taskMap": {
-      "1779335866186": {
+      "1779350314340": {
         "type": "build",
-        "id": "1779335866186",
+        "id": "1779350314340",
         "progress": 1,
         "state": "success",
         "stage": "build",
-        "message": "2026-5-21 11:57:59 build -> run success in 13 s!",
-        "detailMessage": "// ---- build task wechatgame:onAfterBuild ----\r",
+        "message": "2026-5-21 15:58:46 build success in 12 s!",
+        "detailMessage": "// ---- build task 整理静态模板文件 ----\r",
         "options": {
           "name": "steel-assault-rift",
           "server": "",
@@ -86,16 +86,11 @@
             "threshold": 16,
             "enable": false
           },
-          "buildStageGroup": {
-            "build": [
-              "run"
-            ]
-          },
           "packages": {},
           "__version__": "1.3.9",
-          "logDest": "project://temp/builder/log/wechatgame2026-5-21 11-57.log"
+          "logDest": "project://temp/builder/log/wechatgame2026-5-21 15-58.log"
         },
-        "time": "2026-5-21 11:57:46",
+        "time": "2026-5-21 15:58:34",
         "dirty": false
       }
     }

+ 33 - 6
profiles/v2/packages/wechatgame.json

@@ -9,12 +9,7 @@
       "buildPath": "project://build",
       "outputName": "wechatgame",
       "mainBundleCompressionType": "merge_dep",
-      "platform": "wechatgame",
-      "buildStageGroup": {
-        "build": [
-          "run"
-        ]
-      }
+      "platform": "wechatgame"
     },
     "options": {
       "wechatgame": {
@@ -266,6 +261,38 @@
         "separateEngine": true,
         "highPerformanceMode": false,
         "__version__": "1.0.4"
+      },
+      "1779345436148": {
+        "orientation": "landscapeRight",
+        "appid": "wxb9f2d78e6d0cf8cb",
+        "buildOpenDataContextTemplate": "",
+        "separateEngine": true,
+        "highPerformanceMode": false,
+        "__version__": "1.0.4"
+      },
+      "1779345802262": {
+        "orientation": "landscapeRight",
+        "appid": "wxb9f2d78e6d0cf8cb",
+        "buildOpenDataContextTemplate": "",
+        "separateEngine": true,
+        "highPerformanceMode": false,
+        "__version__": "1.0.4"
+      },
+      "1779347067582": {
+        "orientation": "landscapeRight",
+        "appid": "wxb9f2d78e6d0cf8cb",
+        "buildOpenDataContextTemplate": "",
+        "separateEngine": true,
+        "highPerformanceMode": false,
+        "__version__": "1.0.4"
+      },
+      "1779350314340": {
+        "orientation": "landscapeRight",
+        "appid": "wxb9f2d78e6d0cf8cb",
+        "buildOpenDataContextTemplate": "",
+        "separateEngine": true,
+        "highPerformanceMode": false,
+        "__version__": "1.0.4"
       }
     },
     "__version__": "1.3.9"