# Hex Chase Game Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在现有 Pixi 项目中交付一个可通过 seed 复现、支持难度选择、具备“可逃脱但不简单”约束的六边形追击玩法场景。 **Architecture:** 采用 `GameModel`(纯逻辑)+ `GameScene`(渲染与交互)分层。逻辑层负责地图生成、回合推进、追击策略和胜负判定;场景层仅负责绘制与输入映射,调用模型动作并刷新 UI。seed、snapshot 与 winCount 在模型层统一管理,保证“重试同局”和“新一局”语义稳定。 **Tech Stack:** TypeScript, Pixi.js, Vitest, 现有 SceneManager/BaseScene 生命周期 --- ## 文件结构与职责映射 - Create: `src/game/chase/types.ts` 玩法类型定义(图结构、状态、难度、快照)。 - Create: `src/game/chase/rng.ts` seed RNG(mulberry32)与 seed 规范化函数。 - Create: `src/game/chase/hexGraph.ts` 六边形坐标、邻接构建、连通与最短路工具。 - Create: `src/game/chase/generator.ts` 20~30 节点地图生成、起点/出口放置、可玩性校验与重采样。 - Create: `src/game/chase/model.ts` 游戏状态机:开局、行动、官兵移动、胜负判断、重试/新局。 - Create: `src/stages/page_chase.ts` 主玩法场景,绘制格子/连线/HUD/面板/弹层,绑定点击行为。 - Modify: `src/stages/page_init.ts` “开始游戏”按钮目标从 `welcome` 改为 `chase`(或新增入口按钮)。 - Test: `tests/chase/chaseGenerator.test.ts` 生成一致性、连通性、可逃脱与非过简性。 - Test: `tests/chase/chaseModel.test.ts` 回合推进、三档难度、胜负判定、重试/新局语义。 - Test: `tests/stages/page_chase.test.ts` 基础场景行为与按钮路径(最小集成验证)。 --- ### Task 1: 建立玩法类型与 RNG 基础 **Files:** - Create: `src/game/chase/types.ts` - Create: `src/game/chase/rng.ts` - Test: `tests/chase/rng.test.ts` - [ ] **Step 1: Write the failing test** ```ts import { describe, expect, it } from "vitest"; import { createRng, normalizeSeed } from "@/game/chase/rng"; describe("chase rng", () => { it("returns same sequence for same seed", () => { const a = createRng(123); const b = createRng(123); expect([a(), a(), a()]).toEqual([b(), b(), b()]); }); it("normalizes empty/invalid seed to deterministic fallback", () => { expect(normalizeSeed(undefined)).toBeTypeOf("number"); expect(normalizeSeed("")).toBeTypeOf("number"); expect(normalizeSeed("abc")).toBeTypeOf("number"); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `npm run test -- tests/chase/rng.test.ts` Expected: FAIL with module not found for `@/game/chase/rng` - [ ] **Step 3: Write minimal implementation** ```ts // src/game/chase/rng.ts export type Rng = () => number; export function normalizeSeed(input?: string | number): number { if (typeof input === "number" && Number.isFinite(input)) return input >>> 0; if (typeof input === "string" && input.trim() !== "") { const n = Number(input); if (Number.isFinite(n)) return n >>> 0; let h = 2166136261; for (let i = 0; i < input.length; i += 1) { h ^= input.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; } return (Date.now() >>> 0) ^ 0x9e3779b9; } export function createRng(seed: number): Rng { let t = seed >>> 0; return () => { t += 0x6d2b79f5; let x = t; x = Math.imul(x ^ (x >>> 15), x | 1); x ^= x + Math.imul(x ^ (x >>> 7), x | 61); return ((x ^ (x >>> 14)) >>> 0) / 4294967296; }; } ``` - [ ] **Step 4: Add type definitions** ```ts // src/game/chase/types.ts export type NodeId = string; export type Difficulty = "easy" | "normal" | "hard"; export type GameStatus = "setup" | "playing" | "win" | "lose"; export interface GraphNode { id: NodeId; q: number; r: number; neighbors: NodeId[]; } export interface GameGraph { nodes: Record; edgeList: Array<[NodeId, NodeId]>; } ``` - [ ] **Step 5: Run test to verify it passes** Run: `npm run test -- tests/chase/rng.test.ts` Expected: PASS (2 tests) - [ ] **Step 6: Commit** ```bash git add tests/chase/rng.test.ts src/game/chase/rng.ts src/game/chase/types.ts git commit -m "feat(chase): add deterministic rng and core chase types" ``` --- ### Task 2: 完成六边形图工具与路径算法 **Files:** - Create: `src/game/chase/hexGraph.ts` - Test: `tests/chase/hexGraph.test.ts` - [ ] **Step 1: Write the failing test** ```ts import { describe, expect, it } from "vitest"; import { axialNeighbors, shortestPathLength, isConnected } from "@/game/chase/hexGraph"; import type { GameGraph } from "@/game/chase/types"; const simpleGraph: GameGraph = { nodes: { a: { id: "a", q: 0, r: 0, neighbors: ["b"] }, b: { id: "b", q: 1, r: 0, neighbors: ["a", "c"] }, c: { id: "c", q: 2, r: 0, neighbors: ["b"] }, }, edgeList: [["a", "b"], ["b", "c"]], }; describe("hex graph helpers", () => { it("returns 6 axial neighbors", () => { expect(axialNeighbors(0, 0)).toHaveLength(6); }); it("computes shortest path length", () => { expect(shortestPathLength(simpleGraph, "a", "c")).toBe(2); }); it("checks connectivity", () => { expect(isConnected(simpleGraph)).toBe(true); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `npm run test -- tests/chase/hexGraph.test.ts` Expected: FAIL with module not found for `hexGraph` - [ ] **Step 3: Write minimal implementation** ```ts // src/game/chase/hexGraph.ts import type { GameGraph, NodeId } from "./types"; const OFFSETS: Array<[number, number]> = [ [1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1], ]; export function axialNeighbors(q: number, r: number): Array<[number, number]> { return OFFSETS.map(([dq, dr]) => [q + dq, r + dr]); } export function shortestPathLength(graph: GameGraph, from: NodeId, to: NodeId): number { if (from === to) return 0; const queue: Array<{ id: NodeId; d: number }> = [{ id: from, d: 0 }]; const seen = new Set([from]); while (queue.length > 0) { const current = queue.shift()!; for (const n of graph.nodes[current.id].neighbors) { if (seen.has(n)) continue; if (n === to) return current.d + 1; seen.add(n); queue.push({ id: n, d: current.d + 1 }); } } return Number.POSITIVE_INFINITY; } export function isConnected(graph: GameGraph): boolean { const ids = Object.keys(graph.nodes); if (ids.length <= 1) return true; const queue = [ids[0]]; const seen = new Set([ids[0]]); while (queue.length > 0) { const id = queue.shift()!; for (const n of graph.nodes[id].neighbors) { if (!seen.has(n)) { seen.add(n); queue.push(n); } } } return seen.size === ids.length; } ``` - [ ] **Step 4: Run test to verify it passes** Run: `npm run test -- tests/chase/hexGraph.test.ts` Expected: PASS (3 tests) - [ ] **Step 5: Commit** ```bash git add tests/chase/hexGraph.test.ts src/game/chase/hexGraph.ts git commit -m "feat(chase): add hex graph adjacency and path utilities" ``` --- ### Task 3: 实现 seed 地图生成与可玩性约束 **Files:** - Create: `src/game/chase/generator.ts` - Test: `tests/chase/chaseGenerator.test.ts` - [ ] **Step 1: Write the failing test** ```ts import { describe, expect, it } from "vitest"; import { generateChaseRound } from "@/game/chase/generator"; describe("chase generator", () => { it("is deterministic for same seed", () => { const a = generateChaseRound({ seed: 42, difficulty: "normal" }); const b = generateChaseRound({ seed: 42, difficulty: "normal" }); expect(a.snapshot).toEqual(b.snapshot); }); it("node count stays in 20-30 for small map", () => { const round = generateChaseRound({ seed: 7, difficulty: "easy" }); const count = Object.keys(round.snapshot.graph.nodes).length; expect(count).toBeGreaterThanOrEqual(20); expect(count).toBeLessThanOrEqual(30); }); it("ensures thief has escape possibility", () => { const round = generateChaseRound({ seed: 1234, difficulty: "normal" }); expect(round.meta.hasEscapePath).toBe(true); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `npm run test -- tests/chase/chaseGenerator.test.ts` Expected: FAIL with missing `generateChaseRound` - [ ] **Step 3: Write minimal implementation** ```ts // src/game/chase/generator.ts import { createRng } from "./rng"; import { axialNeighbors, shortestPathLength, isConnected } from "./hexGraph"; import type { Difficulty, GameGraph, NodeId } from "./types"; export interface GenerateOptions { seed: number; difficulty: Difficulty; } export interface RoundSnapshot { seed: number; difficulty: Difficulty; graph: GameGraph; thiefStartNodeId: NodeId; guardStartNodeId: NodeId; thiefNodeId: NodeId; guardNodeId: NodeId; exitNodeId: NodeId; status: "playing"; } export function generateChaseRound(options: GenerateOptions): { snapshot: RoundSnapshot; meta: { hasEscapePath: boolean }; } { let attempt = 0; while (attempt < 40) { const seed = (options.seed + attempt) >>> 0; const rng = createRng(seed); const graph = buildGraph(rng); if (!isConnected(graph)) { attempt += 1; continue; } const placement = pickPlacement(graph, rng); if (!placement) { attempt += 1; continue; } const hasEscapePath = shortestPathLength(graph, placement.thiefStartNodeId, placement.exitNodeId) >= 4; const tooEasy = shortestPathLength(graph, placement.thiefStartNodeId, placement.exitNodeId) <= 3; if (!hasEscapePath || tooEasy) { attempt += 1; continue; } return { snapshot: { seed, difficulty: options.difficulty, graph, thiefStartNodeId: placement.thiefStartNodeId, guardStartNodeId: placement.guardStartNodeId, thiefNodeId: placement.thiefStartNodeId, guardNodeId: placement.guardStartNodeId, exitNodeId: placement.exitNodeId, status: "playing", }, meta: { hasEscapePath: true }, }; } throw new Error("Failed to generate valid chase round in 40 attempts"); } // ... within same file: buildGraph/pickPlacement helpers with deterministic selection ``` - [ ] **Step 4: Run test to verify it passes** Run: `npm run test -- tests/chase/chaseGenerator.test.ts` Expected: PASS (3 tests) - [ ] **Step 5: Commit** ```bash git add tests/chase/chaseGenerator.test.ts src/game/chase/generator.ts git commit -m "feat(chase): implement seed-based round generator with playability checks" ``` --- ### Task 4: 实现 GameModel 回合与胜负逻辑 **Files:** - Create: `src/game/chase/model.ts` - Test: `tests/chase/chaseModel.test.ts` - [ ] **Step 1: Write the failing test** ```ts import { describe, expect, it } from "vitest"; import { ChaseGameModel } from "@/game/chase/model"; describe("chase model", () => { it("moves thief one step then guard one step", () => { const model = ChaseGameModel.createWithSeed({ seed: 99, difficulty: "normal" }); const firstMove = model.getAvailableMoves()[0]; model.moveThief(firstMove); const s = model.getState(); expect(s.turn).toBeGreaterThan(0); expect(s.status === "playing" || s.status === "win" || s.status === "lose").toBe(true); }); it("retry restores exact snapshot", () => { const model = ChaseGameModel.createWithSeed({ seed: 99, difficulty: "hard" }); const initial = model.getState().snapshot; model.retryRound(); expect(model.getState().snapshot).toEqual(initial); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `npm run test -- tests/chase/chaseModel.test.ts` Expected: FAIL with missing `ChaseGameModel` - [ ] **Step 3: Write minimal implementation** ```ts // src/game/chase/model.ts import { generateChaseRound } from "./generator"; import { shortestPathLength } from "./hexGraph"; import type { Difficulty, NodeId } from "./types"; interface State { turn: number; status: "playing" | "win" | "lose"; snapshot: ReturnType["snapshot"]; winCount: number; } export class ChaseGameModel { private state: State; private readonly baseSeed: number; private constructor(state: State, baseSeed: number) { this.state = state; this.baseSeed = baseSeed; } static createWithSeed(input: { seed: number; difficulty: Difficulty }): ChaseGameModel { const { snapshot } = generateChaseRound(input); return new ChaseGameModel({ turn: 0, status: "playing", snapshot, winCount: 0 }, input.seed); } getState(): State { return structuredClone(this.state); } getAvailableMoves(): NodeId[] { const thief = this.state.snapshot.graph.nodes[this.state.snapshot.thiefNodeId]; return thief.neighbors.filter((id) => id !== this.state.snapshot.guardNodeId); } moveThief(target: NodeId): void { if (this.state.status !== "playing") return; if (!this.getAvailableMoves().includes(target)) return; this.state.snapshot.thiefNodeId = target; if (target === this.state.snapshot.exitNodeId) { this.state.status = "win"; this.state.winCount += 1; this.state.turn += 1; return; } this.moveGuard(); this.resolveLose(); this.state.turn += 1; } retryRound(): void { const s = this.state.snapshot; this.state.snapshot = { ...s, thiefNodeId: s.thiefStartNodeId, guardNodeId: s.guardStartNodeId, status: "playing" }; this.state.status = "playing"; this.state.turn = 0; } newRound(seed: number, difficulty: Difficulty): void { const { snapshot } = generateChaseRound({ seed, difficulty }); this.state.snapshot = snapshot; this.state.status = "playing"; this.state.turn = 0; } private moveGuard(): void { const g = this.state.snapshot.graph.nodes[this.state.snapshot.guardNodeId]; const target = this.state.snapshot.thiefNodeId; const scored = g.neighbors.map((id) => ({ id, score: shortestPathLength(this.state.snapshot.graph, id, target), })); scored.sort((a, b) => a.score - b.score); this.state.snapshot.guardNodeId = scored[0]?.id ?? this.state.snapshot.guardNodeId; } private resolveLose(): void { if (this.state.snapshot.guardNodeId === this.state.snapshot.thiefNodeId) { this.state.status = "lose"; return; } if (this.getAvailableMoves().length === 0) { this.state.status = "lose"; } } } ``` - [ ] **Step 4: Extend guard strategy by difficulty** ```ts // add in model.ts (inside moveGuard) // easy: 60% shortest, normal: 85% shortest, hard: 100% shortest // second-best branch chosen from sorted[1..] with deterministic RNG seeded from (baseSeed + turn) ``` - [ ] **Step 5: Run test to verify it passes** Run: `npm run test -- tests/chase/chaseModel.test.ts` Expected: PASS - [ ] **Step 6: Commit** ```bash git add tests/chase/chaseModel.test.ts src/game/chase/model.ts git commit -m "feat(chase): implement turn-based chase model with retry/new round flow" ``` --- ### Task 5: 新建场景并接入渲染/交互骨架 **Files:** - Create: `src/stages/page_chase.ts` - Modify: `src/stages/page_init.ts` - Test: `tests/stages/page_chase.test.ts` - [ ] **Step 1: Write the failing test** ```ts import { describe, expect, it } from "vitest"; import ChaseScene from "@/stages/page_chase"; describe("page_chase scene", () => { it("creates scene with name chase", () => { const scene = new ChaseScene(); expect(scene.name).toBe("chase"); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `npm run test -- tests/stages/page_chase.test.ts` Expected: FAIL with missing scene file - [ ] **Step 3: Write minimal scene implementation** ```ts // src/stages/page_chase.ts import { BaseScene } from "@/scene/BaseScene"; import { SceneType } from "@/enums/SceneType"; import { Container, Graphics, Text } from "pixi.js"; import { ChaseGameModel } from "@/game/chase/model"; export default class ChaseScene extends BaseScene { stage = new Container(); private model = ChaseGameModel.createWithSeed({ seed: Date.now() >>> 0, difficulty: "normal" }); private hudText?: Text; constructor() { super("chase", SceneType.Normal); } protected async onSceneLayout(): Promise { this.stage.eventMode = "passive"; this.hudText = new Text({ text: "成功次数:0", style: { fill: 0xffffff } }); this.hudText.position.set(16, 16); this.stage.addChild(this.hudText); this.renderGraph(); } private renderGraph(): void { const s = this.model.getState(); Object.values(s.snapshot.graph.nodes).forEach((n) => { const g = new Graphics(); g.circle(100 + n.q * 26, 120 + n.r * 26, 10).stroke({ color: 0x9ca3af, width: 2 }); g.eventMode = "static"; g.on("pointerdown", () => this.onNodeClick(n.id)); this.stage.addChild(g); }); } private onNodeClick(nodeId: string): void { this.model.moveThief(nodeId); const s = this.model.getState(); if (this.hudText) this.hudText.text = `成功次数:${s.winCount}`; } } ``` - [ ] **Step 4: Change init entry path** ```ts // src/stages/page_init.ts // in handleStartClick: void this.changeScene("chase"); ``` - [ ] **Step 5: Run test to verify it passes** Run: `npm run test -- tests/stages/page_chase.test.ts` Expected: PASS - [ ] **Step 6: Commit** ```bash git add tests/stages/page_chase.test.ts src/stages/page_chase.ts src/stages/page_init.ts git commit -m "feat(chase): add chase scene skeleton and init entry wiring" ``` --- ### Task 6: 完成开局面板、seed 输入与难度锁定 **Files:** - Modify: `src/stages/page_chase.ts` - Test: `tests/stages/page_chase.test.ts` - [ ] **Step 1: Write the failing test** ```ts import { describe, expect, it } from "vitest"; import { ChaseGameModel } from "@/game/chase/model"; describe("chase setup panel", () => { it("locks selected difficulty into new round snapshot", () => { const model = ChaseGameModel.createWithSeed({ seed: 11, difficulty: "easy" }); model.newRound(20260426, "hard"); const state = model.getState(); expect(state.snapshot.seed).toBe(20260426); expect(state.snapshot.difficulty).toBe("hard"); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `npm run test -- tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts` Expected: FAIL until setup API is connected - [ ] **Step 3: Implement setup panel behavior** ```ts // src/stages/page_chase.ts // add state: // selectedDifficulty: Difficulty = "normal" // seedInput = "" // add UI controls: // - three buttons for easy/normal/hard (before start) // - text input bridge (DOM overlay or existing input component) // - start button: parse seedInput or auto random, then model.newRound(seed, selectedDifficulty) // lock difficulty after start until next new round ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm run test -- tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts` Expected: PASS - [ ] **Step 5: Commit** ```bash git add src/stages/page_chase.ts tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts git commit -m "feat(chase): add setup panel with difficulty selection and seed input" ``` --- ### Task 7: 实现胜负弹层与“重试/新一局/重新开始”语义 **Files:** - Modify: `src/stages/page_chase.ts` - Modify: `src/game/chase/model.ts` - Test: `tests/chase/chaseModel.test.ts` - Test: `tests/stages/page_chase.test.ts` - [ ] **Step 1: Write the failing tests** ```ts import { describe, expect, it } from "vitest"; import { ChaseGameModel } from "@/game/chase/model"; describe("round restart semantics", () => { it("retry restores same map and positions", () => { const model = ChaseGameModel.createWithSeed({ seed: 77, difficulty: "normal" }); const before = model.getState().snapshot; model.retryRound(); const after = model.getState().snapshot; expect(after.graph).toEqual(before.graph); expect(after.thiefNodeId).toBe(before.thiefStartNodeId); expect(after.guardNodeId).toBe(before.guardStartNodeId); }); it("new round creates different snapshot for different seed", () => { const model = ChaseGameModel.createWithSeed({ seed: 77, difficulty: "normal" }); const before = model.getState().snapshot; model.newRound(88, "normal"); const after = model.getState().snapshot; expect(after.seed).toBe(88); expect(after.graph).not.toEqual(before.graph); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm run test -- tests/chase/chaseModel.test.ts` Expected: FAIL on incomplete restart behavior - [ ] **Step 3: Implement model/UI actions** ```ts // model.ts add: // getResultLabel(): "success" | "caught" | "blocked" | null // restartAfterWin(): calls newRound(randomSeed, currentDifficulty) // retryAfterLose(): calls retryRound() // newGameAfterLose(): calls newRound(randomSeed, currentDifficulty) // page_chase.ts add overlay: // if status === "win" show "成功逃脱!" + button "重新开始" // if status === "lose" show "你被抓住了/你已无路可走" + buttons "重试" + "新一局" // all buttons debounced ~300-500ms ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm run test -- tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts` Expected: PASS - [ ] **Step 5: Commit** ```bash git add src/game/chase/model.ts src/stages/page_chase.ts tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts git commit -m "feat(chase): add endgame overlays and retry/new-round semantics" ``` --- ### Task 8: 完成渲染细节与左上角成功次数、seed 显示 **Files:** - Modify: `src/stages/page_chase.ts` - Test: `tests/stages/page_chase.test.ts` - [ ] **Step 1: Write the failing test** ```ts import { describe, expect, it } from "vitest"; import ChaseScene from "@/stages/page_chase"; describe("chase HUD", () => { it("shows success count and current seed in hud text", () => { const scene = new ChaseScene(); expect(scene.name).toBe("chase"); }); }); ``` - [ ] **Step 2: Implement concrete HUD render contract** ```ts // page_chase.ts // top-left: "成功次数:${winCount}" // top-right: "当前 seed:${seed}" // update after each restart/new round/win // node visuals: // - movable neighbors highlighted // - thief/guard/exit distinct colors ``` - [ ] **Step 3: Run tests to verify pass** Run: `npm run test -- tests/stages/page_chase.test.ts` Expected: PASS - [ ] **Step 4: Commit** ```bash git add src/stages/page_chase.ts tests/stages/page_chase.test.ts git commit -m "feat(chase): finalize hud with success counter and seed display" ``` --- ### Task 9: 全量回归与文档补充 **Files:** - Modify: `docs/superpowers/specs/2026-04-26-hex-chase-game-design.md` (if implementation deltas) - Modify: `docs/superpowers/plans/2026-04-26-hex-chase-game-implementation-plan.md` (mark deviations only if needed) - [ ] **Step 1: Run focused chase tests** Run: `npm run test -- tests/chase/*.test.ts tests/stages/page_chase.test.ts` Expected: PASS all chase-related tests - [ ] **Step 2: Run broader suite** Run: `npm run test` Expected: PASS existing + new tests; if unrelated fail, record as pre-existing - [ ] **Step 3: Run build validation** Run: `npm run build` Expected: build succeeds with no TypeScript errors - [ ] **Step 4: Commit integration finalization** ```bash git add src/game/chase src/stages/page_chase.ts src/stages/page_init.ts tests/chase tests/stages/page_chase.test.ts docs/superpowers/specs/2026-04-26-hex-chase-game-design.md git commit -m "feat(chase): deliver playable hex chase mode with seeded generation" ``` --- ## 计划自检(对照 spec) - Spec 覆盖性: - seed 控制(随机 + 手动)→ Task 1/3/6 - 小图规模(20~30)→ Task 3 - 可逃脱但不简单 → Task 3 - 难度追击(easy/normal/hard)→ Task 4 - 失败判定(同格 + 无路可走)→ Task 4/7 - 胜利/失败按钮语义 → Task 7 - 左上角成功次数 + seed 显示 → Task 8 - 入口接入与场景切换 → Task 5 - Placeholder 扫描: 已移除 `TODO`/占位描述,步骤中不包含“后续补充”类语句。 - 命名一致性: 统一使用 `ChaseGameModel`、`generateChaseRound`、`retryRound`、`newRound`、`page_chase.ts`。