diff --git "a/.github/docs/Salu\346\270\270\346\210\217\344\270\232\345\212\241\350\257\264\346\230\216.md" "b/.github/docs/Salu\346\270\270\346\210\217\344\270\232\345\212\241\350\257\264\346\230\216.md"
index 5dc425f..6081c17 100644
--- "a/.github/docs/Salu\346\270\270\346\210\217\344\270\232\345\212\241\350\257\264\346\230\216.md"
+++ "b/.github/docs/Salu\346\270\270\346\210\217\344\270\232\345\212\241\350\257\264\346\230\216.md"
@@ -2,7 +2,7 @@
本文档以**玩法/业务视角**说明 Salu 的系统、数值与触发时机,用于统一"游戏应该怎么表现/怎么结算"。
-> **设定与剧情**请参阅:`.giithub/docs/Salu游戏设定与剧情v1.0.md`
+> **设定与剧情**请参阅:`.github/docs/Salu游戏设定与剧情v1.0.md`
> **卡牌/敌人/遗物命名映射**已整合到各业务章节(第 4/6/11 章)的表格中。
---
diff --git "a/.github/docs/Salu\346\270\270\346\210\217\350\256\276\345\256\232\344\270\216\345\211\247\346\203\205v1.0.md" "b/.github/docs/Salu\346\270\270\346\210\217\350\256\276\345\256\232\344\270\216\345\211\247\346\203\205v1.0.md"
index 66e10d7..adc459d 100644
--- "a/.github/docs/Salu\346\270\270\346\210\217\350\256\276\345\256\232\344\270\216\345\211\247\346\203\205v1.0.md"
+++ "b/.github/docs/Salu\346\270\270\346\210\217\350\256\276\345\256\232\344\270\216\345\211\247\346\203\205v1.0.md"
@@ -2,7 +2,7 @@
本文档是 **Salu 的"设定/剧情"事实来源**,用于统一叙事方向与世界观风格。
-- **玩法规则/触发/数值**:`.giithub/docs/Salu游戏业务说明(玩法系统与触发规则).md`
+- **玩法规则/触发/数值**:`.github/docs/Salu游戏业务说明(玩法系统与触发规则).md`
- **卡牌/敌人/遗物名称映射**:见业务说明文档各业务章节(第 4/6/11 章)的表格
---
diff --git "a/.github/plans/Apple Vision Pro \345\216\237\347\224\237 3D \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md" "b/.github/plans/Apple Vision Pro \345\216\237\347\224\237 3D \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md"
new file mode 100644
index 0000000..46cba0b
--- /dev/null
+++ "b/.github/plans/Apple Vision Pro \345\216\237\347\224\237 3D \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md"
@@ -0,0 +1,203 @@
+---
+title: Apple Vision Pro 原生 3D 实现(SaluAVP)
+date: 2026-01-29
+updated: 2026-01-29
+architecture: visionOS-only App + Immersive-first (RealityKit)
+target: SaluAVP
+---
+
+# Plan AVP:Apple Vision Pro(visionOS)原生 3D 实现(SaluAVP)
+
+## 0. 概览
+
+### 背景与定位
+
+- `SaluAVP` 是仓库内的 **visionOS-only** 原生 App Target:以 **ImmersiveSpace + RealityKit(原生 3D)** 为主。
+- 2D Window 仅承担“控制面板”:入口 / seed / 存档选择 / 设置 / 历史等,不承载主战斗体验。
+- 逻辑与内容 **仅复用 `GameCore`**(禁止依赖 `GameCLI`),确保跨平台逻辑一致与可测试。
+
+### 目标(Goals)
+
+- 能完成完整一局:入口 → 地图推进 → 房间(战斗/事件/商店/休息)→ Boss → 结算。
+- 同 seed + 同选择路径,核心结果可复现(地图分支/战斗结算/奖励等)。
+- Immersive-first:至少一个可交互 ImmersiveSpace(地图或战斗,优先地图)打通闭环。
+
+### 非目标(Non-goals)
+
+- 首版不追求完整的 VFX/动画系统、写实材质、复杂骨骼动画。
+- 不把“3D 渲染实现”抽到共享层;共享层仅做状态机/桥接(见下文)。
+- 不在 SwiftPM 的 `Sources/` 内引入任何 Apple-only 框架(SwiftUI / SwiftData / RealityKit 等)。
+
+### 核心决策(Decisions)
+
+1. **原生 App 仅保留 `SaluAVP`(visionOS)**
+ - 主玩法与输入模型以沉浸式 3D 为中心;桌面端(macOS)如需体验优先使用 `GameCLI`,避免双 UI 维护成本。
+2. **RealityKit 必选,但只存在于 `SaluNative/`**
+ - `Sources/` 仍然保持纯逻辑、可跨平台构建。
+3. **共享层只共享“状态/桥接”,不共享“渲染实现”**
+ - 共享:路由状态机 / 与 `GameCore` 的桥接 Session / ViewModels。
+ - 不共享:3D 场景构建、实体/材质/动画、沉浸输入手势(全部在 `SaluAVP` 内部)。
+
+### 验收标准(Definition of Done)
+
+- `SaluAVP` 可在 visionOS Simulator 上编译并运行(入口 2D + 至少 1 个可交互 ImmersiveSpace)。
+- 完整一局可通关,并能导出/记录“选择路径”(便于复现实验与回归)。
+- 关键路径无平台专属逻辑泄漏到 `Sources/`(CI/本地跨平台构建不受影响)。
+
+### 验证命令(Build)
+
+```bash
+# visionOS(SaluAVP)
+xcodebuild -project SaluNative/SaluNative.xcodeproj \
+ -scheme SaluAVP \
+ -destination 'platform=visionOS Simulator,name=Apple Vision Pro' \
+ build
+```
+
+```bash
+# 若本次改动涉及 SwiftPM 的 `Sources/**`(例如 GameCore 逻辑),需要补跑:
+swift test
+```
+
+---
+
+## 1. 约束与边界(必须遵守)
+
+- 依赖方向必须保持:
+ - `SaluAVP → GameCore` ✅
+ - `SaluAVP → Shared` ✅(若存在)
+ - `GameCore → (任何 App/UI/RealityKit/SwiftUI/SwiftData)` ❌
+- 可复现性:
+ - `SaluAVP` 只能把“用户选择”作为输入;随机性必须由 `seed` 驱动并由 `GameCore`(或明确注入的 RNG)提供。
+ - UI 层不得用“系统时间/系统随机数”影响战斗与地图结果。
+- 多平台约束:
+ - 不在 SwiftPM `Sources/` 内使用 Apple-only API;AVP 的 UI 代码全部留在 `SaluNative/`。
+ - 若需要“跨 Target 共享”,请放到 `SaluNative/Shared/`,并保持 **不引入 RealityKit**(避免把渲染实现传播到共享层)。
+
+---
+
+## 2. 架构草图(建议,不强制)
+
+### 模块结构(建议)
+
+```
+SaluNative/
+├── SaluNative.xcodeproj
+├── SaluAVP/ # ✅ visionOS-only App(RealityKit 在此处)
+│ ├── SaluAVPApp.swift
+│ ├── ControlPanel/ # 2D 控制面板(入口/seed/设置/历史)
+│ └── Immersive/ # 3D 体验主体(ImmersiveSpace)
+│ ├── ImmersiveRootView.swift
+│ ├── MapScene.swift
+│ └── BattleScene.swift
+└── Shared/ # 可选:跨 Target 共享(禁止 RealityKit)
+ ├── AppRoute.swift
+ ├── GameSession.swift
+ └── ViewModels/
+ ├── RunSession.swift
+ └── BattleSession.swift
+```
+
+### 状态机(建议)
+
+- `AppRoute`(Shared):`controlPanel` ↔ `immersive(map | battle | room)` ↔ `runSummary`
+- `GameSession`(Shared):持有当前 `RunState`(或其等价的 Source of Truth)以及与存档的转换逻辑。
+- `SaluAVP`(Target 内):只关心“如何渲染”和“如何把手势变成选择”,不关心规则细节。
+
+### 数据流(建议)
+
+`Immersive UI(选择/手势)` → `Session(桥接/路由)` → `GameCore(状态推进)` → `Session(同步到可观察状态)` → `UI`
+
+---
+
+## 3. 关键接口速查(以当前 `GameCore` 实现为准)
+
+> 目标:让 AVP 实现方快速定位“应该调用什么”,而不是在计划里复述字段列表。
+
+### 3.1 `RunState`(冒险全局状态)
+
+- 源码:`Sources/GameCore/Run/RunState.swift`
+- 常用入口与流程方法:
+ - `RunState.newRun(seed:)`
+ - `accessibleNodes` / `enterNode(_:)` / `completeCurrentNode()`
+ - `updateFromBattle(playerHP:)` / `restAtNode()`
+
+### 3.2 `RunSnapshot`(存档交换格式)
+
+- 源码:`Sources/GameCore/Run/RunSnapshot.swift`
+- 版本策略:`Sources/GameCore/Run/RunSaveVersion.swift`
+- `GameCore` 只提供“快照模型”;AVP 的持久化落盘策略在 App 层决定(SwiftData / 文件 / iCloud 等)。
+
+---
+
+## 4. 交互与 UX(Immersive-first)
+
+### 交互映射(MVP)
+
+- 地图:指向/点选节点 → 进入节点 → 触发房间 → 完成节点 → 回到地图。
+- 战斗:选卡 →(若需要)选目标 → 出牌 → 结束回合。
+- 强制反馈(所有 MVP 交互都要有):
+ - 可交互/不可交互(灰度/高亮)
+ - 选中态(描边/发光/抬升)
+ - 结果反馈(数值飘字/简易日志/状态图标均可)
+
+### 2D 控制面板(MVP)
+
+- New Run:输入 seed(或随机生成后展示)并开始。
+- Continue:从 App 层持久化恢复(如果 P3 之前未做存档,可先隐藏/置灰)。
+- Immersive 控制:进入/退出 ImmersiveSpace,显示当前 run 的关键摘要(楼层/金币/HP)。
+
+---
+
+## 5. 执行计划(以 AVP 为主)
+
+### P0(必须):工程打通(SaluAVP Skeleton)
+
+- 产出:
+ - `SaluAVP` 目标可编译;能 `import GameCore`。
+ - 2D 控制面板 + 可进入的 ImmersiveSpace(占位场景:1 个地板 + 1 个可点击物体)。
+- DoD:
+ - Simulator 可运行;能在 2D 界面控制 ImmersiveSpace 打开/关闭,不崩溃。
+
+### P1(必须):3D 地图闭环(Map Loop)
+
+- 产出:
+ - ImmersiveSpace 中渲染地图节点(占位几何体即可)。
+ - 节点状态:可达/已完成/当前节点(至少用三态材质/颜色区分)。
+ - 选择节点 → `RunState.enterNode(_:)` → 路由到对应房间(先占位房间)→ `completeCurrentNode()` 返回地图。
+- DoD:
+ - 能从 Act1 开始一路点击推进到 Boss,并触发“run over/won”。
+
+### P2(重要):3D 战斗闭环(Battle Loop)
+
+- 产出:
+ - ImmersiveSpace 中渲染:玩家/敌人/手牌/能量/日志(形式不限,先可读可用)。
+ - Session 桥接 `GameCore` 的战斗推进(建议放在 `SaluNative/Shared/`,以便未来与 macOS 复用)。
+ - 战斗结束后:更新 `RunState`(例如 `updateFromBattle(playerHP:)`)→ 应用奖励/推进地图(奖励可先最小化)。
+- DoD:
+ - 战斗可完整结束(胜/负),并能回到地图继续推进。
+
+### P3(后续):持久化(SwiftData / JSON Blob)
+
+- 原则:继续沿用“索引字段 + JSON Blob”的策略,降低模型演进成本。
+ - 索引字段:`seed`、`updatedAt`、`floor`、`won` 等(便于列表与筛选)。
+ - Blob:`RunSnapshot`(以及若存在的战斗历史快照)。
+- DoD:
+ - 能存/读当前 run;能在控制面板展示“继续游戏”。
+
+### P4(后续):观测性与回归(Determinism + Replay)
+
+- 增加“选择路径记录”(例如节点选择、出牌序列、关键事件),用于:
+ - 同 seed 重放验证(快速定位“逻辑回归 vs UI 差异”)。
+ - 自动化回归(未来可在 CLI 或测试里复用)。
+- DoD:
+ - 把一局 run 的关键选择导出为可读文本/JSON,并能用其重放到同一结果(允许表现差异)。
+
+---
+
+## 6. 风险清单(提前规避)
+
+- **Simulator/真机差异**:输入与性能差距大;MVP 阶段所有交互必须在 Simulator 上可用,真机再做增强。
+- **可复现性被 UI 污染**:UI 层禁止引入“非确定”输入(时间/随机);所有决策必须来自用户选择与 seed。
+- **共享层膨胀**:Shared 只放状态机/桥接/纯 Swift,禁止把 RealityKit/材质/动画放进去。
+- **资产管线不稳定**:先用程序化几何体占位;确认交互与流程稳定后再引入 USDZ/贴图。
diff --git "a/.github/plans/visionOS + macOS GUI \345\216\237\347\224\237\345\256\236\347\216\260\346\226\271\346\241\210\357\274\210SwiftUI\357\274\211.md" "b/.github/plans/visionOS + macOS GUI \345\216\237\347\224\237\345\256\236\347\216\260\346\226\271\346\241\210\357\274\210SwiftUI\357\274\211.md"
deleted file mode 100644
index 7bc468e..0000000
--- "a/.github/plans/visionOS + macOS GUI \345\216\237\347\224\237\345\256\236\347\216\260\346\226\271\346\241\210\357\274\210SwiftUI\357\274\211.md"
+++ /dev/null
@@ -1,495 +0,0 @@
----
-title: Plan A - visionOS + macOS GUI 原生实现(SwiftUI)
-date: 2026-01-13
-updated: 2026-01-14
-architecture: Multiplatform App + 条件编译
----
-
-# Plan A:visionOS + macOS GUI 原生实现方案(SwiftUI)
-
-## 0. 目标与核心决策
-
-### 目标
-
-- 在本仓库新增一套**原生 App**(SwiftUI),同时提供:
- - **macOS App**:桌面窗口体验(键鼠友好)
- - **visionOS App**:窗口式 2D 体验优先,后续可扩展沉浸式(ImmersiveSpace)
-- 逻辑与内容 **100% 复用 `GameCore`**;不破坏现有 CLI(`GameCLI`)的构建与测试。
-- 对外验收:能完成完整一局(主菜单 → 地图推进 → 战斗/事件/商店/休息 → Boss → 结算),并有可复现的 seed 展示与导出。
-
-### 核心决策
-
-1. **CLI 与 Native Apps 视为独立产品线**
- - CLI 继续沿用 JSON 落盘(见 `.giithub/docs/本地存储说明.md`)
- - Native Apps 采用 Apple 原生持久化(首选 SwiftData;必要时混合 `@AppStorage`)
- - 默认不做存档互通(后续可选:通过 `RunSnapshot` 作为“交换格式”实现导入/导出)
-
-2. **Native Apps(macOS/visionOS)采用 Multiplatform App 架构**
- - 单一 App Target 同时支持 macOS 和 visionOS(而非两个独立 Target)
- - 所有代码共享,平台差异用 `#if os(visionOS)` / `#if os(macOS)` 做细节适配
- - 这种架构对 AI 编程更友好(所有代码都是文本文件,无需手动在 Xcode 中勾选 Target Membership)
-
-3. **UI 的“最小可用”策略**
- - 第一阶段先做 **window-based 2D UI**(visionOS 和 macOS 同构)
- - “沉浸式战斗/3D 卡牌”放到后续 P8,避免在核心流程未完成前引入 RealityKit 复杂度
-
-4. **可复现性(Determinism)继续以 `GameCore` 规范为准**
- - UI 层不引入系统随机作为“玩法决策输入”
- - 所有房间内容生成(战斗遭遇/奖励/商店/事件)都基于 seed 派生(参考 `RewardGenerator` / `ShopInventory` / `EventGenerator` 的设计)
-
-### 非目标(v0 不做)
-
-- 不实现 iCloud/CloudKit 同步
-- 不实现多人/联网
-- 不实现跨端共享存档(macOS ↔ visionOS ↔ CLI)——只提供未来扩展点
-- 不做完整美术资源/音效体系(先用 emoji / SF Symbols / 文本)
-
-### 验收标准(每个 P 都要满足)
-
-- SwiftPM 侧:`swift build && swift test` 通过(确保 `GameCLI`/`GameCore` 不受影响)
-- Xcode 侧:
- - `SaluCRH` 能在 macOS 上编译并运行
- - `SaluCRH` 能在 visionOS Simulator 上编译并运行(同一 Target,切换 Destination)
-- 行为侧:同 seed + 同选择路径,战斗/地图/奖励/事件/商店结果可复现(允许 UI 表现差异)
-
-### 测试/验证命令参考
-
-| 组件 | 命令 | 说明 |
-|------|------|------|
-| GameCore | `swift test --filter GameCoreTests` | SwiftPM |
-| GameCLI | `swift test --filter GameCLITests` | SwiftPM |
-| Native(编译 - macOS) | `xcodebuild -project SaluNative/SaluNative.xcodeproj -scheme SaluCRH -destination 'platform=macOS' build` | Xcodebuild |
-| Native(编译 - visionOS) | `xcodebuild -project SaluNative/SaluNative.xcodeproj -scheme SaluCRH -destination 'platform=visionOS Simulator,name=Apple Vision Pro' build` | 同一 Target,切换 destination |
-
----
-
-## 1. 调研范围(本次 plan 实际查过的文件)
-
-> 说明:只列出**本次确实读过/检索过**、且会直接影响 Native App 设计的文件。
-
-- `Package.swift`
-- `README.md`
-- `AGENTS.md`
-- `Sources/GameCore/AGENTS.md`
-- `Sources/GameCLI/AGENTS.md`
-- `Sources/GameCore/Run/RunState.swift`
-- `Sources/GameCore/Run/RunSnapshot.swift`
-- `Sources/GameCore/Run/RunSaveVersion.swift`
-- `Sources/GameCore/Battle/BattleEngine.swift`
-- `Sources/GameCore/Battle/BattleState.swift`
-- `Sources/GameCore/Actions.swift`
-- `Sources/GameCore/History.swift`
-- `Sources/GameCore/Persistence/RunSaveStore.swift`
-- `Sources/GameCore/Persistence/BattleHistoryStore.swift`
-- `Sources/GameCore/Cards/CardRegistry.swift`
-- `Sources/GameCore/Cards/StarterDeck.swift`
-- `Sources/GameCore/Map/RoomType.swift`
-- `Sources/GameCore/Rewards/RewardGenerator.swift`(检索/对齐接口与 seed 派生策略)
-- `Sources/GameCore/Rewards/GoldRewardStrategy.swift`(检索/对齐接口与 seed 派生策略)
-- `Sources/GameCore/Shop/ShopInventory.swift`(检索/对齐接口与 seed 派生策略)
-- `Sources/GameCore/Shop/ShopPricing.swift`(检索/对齐接口)
-- `Sources/GameCore/Events/EventGenerator.swift`(检索/对齐接口与 seed 派生策略)
-- `Sources/GameCore/Events/EventContext.swift`(检索/对齐接口)
-- `Sources/GameCLI/GameCLI.swift`(参考现有“runLoop/battleLoop”流程形态)
-- `Sources/GameCLI/Persistence/SaveService.swift`(参考 `RunSnapshot ↔ RunState` 转换)
-- `Sources/GameCLI/Flow/RoomHandling.swift`
-- `Sources/GameCLI/Flow/RoomHandlerRegistry.swift`
-- `Sources/GameCLI/Rooms/Handlers/BattleRoomHandler.swift`(参考战斗房间:遭遇/奖励/日志/节点完成)
-- `Sources/GameCLI/Rooms/Handlers/RestRoomHandler.swift`(参考休息房间:升级/对话/节点完成)
-- `.giithub/docs/本地存储说明.md`
-- `.giithub/docs/Salu游戏业务说明(玩法系统与触发规则).md`
-
----
-
-## 2. 现状与关键接口(与 UI 直接相关)
-
-### 2.1 `RunState`(冒险全局状态)
-
-已确认(`Sources/GameCore/Run/RunState.swift`):
-
-- 冒险状态里已经包含:
- - 玩家 `Entity`
- - 牌组 `[Card]`
- - 金币 `gold`
- - 遗物 `relicManager`
- - 消耗品 `consumables`(最多 3 槽)
- - 地图 `[MapNode]` + `currentNodeId`
- - `seed/floor/maxFloor/isOver/won`
-- 冒险推进 API:
- - `accessibleNodes`
- - `enterNode(_:)`
- - `completeCurrentNode()`(Boss 完成后自动进入下一幕或结算)
- - `restAtNode()`
- - `apply(_ effect: RunEffect)`(事件/奖励可直接返回一组 `RunEffect` 给 UI 层应用)
-
-### 2.2 `BattleEngine`(战斗引擎)
-
-已确认(`Sources/GameCore/Battle/BattleEngine.swift`):
-
-- 入口:
- - `startBattle()`
- - `handleAction(_:)`(`PlayerAction.playCard(handIndex:targetEnemyIndex:)` / `.endTurn`)
-- 观察点:
- - `state: BattleState`(hand/draw/discard/energy/enemies/isOver/playerWon)
- - `events: [BattleEvent]`(可用于 UI 日志/动效)
-- 目标选择逻辑已内建:
- - 卡牌 `targeting == .singleEnemy` 时会强校验目标合法性,并通过 `.invalidAction` 反馈
-
-### 2.3 存档交换格式:`RunSnapshot`
-
-已确认(`Sources/GameCore/Run/RunSnapshot.swift`):
-
-- `RunSnapshot` 是 `Codable`,字段覆盖 Run 的核心状态:
- - `version/seed/floor/maxFloor/gold/mapNodes/currentNodeId/player/deck/relicIds/consumableIds/isOver/won`
-- 版本策略(`RunSaveVersion`)目前为 **强制等版本**(`version == current` 才允许加载)
-
-### 2.4 战斗历史:`BattleRecord`
-
-已确认(`Sources/GameCore/History.swift`):
-
-- `BattleRecord` 是 `Codable`,含 `Date/UUID`,并支持多敌人记录
-- `BattleRecordBuilder.build(from:seed:)` 可直接从 `BattleEngine` 构建记录
-
----
-
-## 3. 总体架构(Multiplatform App + 条件编译)
-
-> 关键点:
-> 1. **不把 Apple-only 代码放进 SwiftPM 的 `Sources/`**,避免影响 Linux/Windows 构建
-> 2. **使用 Multiplatform App 架构**,单一 Target 同时支持 macOS 和 visionOS
-> 3. **平台差异通过 `#if os()` 条件编译处理**,而非创建多个 Target
-> 4. **对 AI 编程友好**:所有代码都是文本文件,无需手动在 Xcode 中勾选 Target Membership
-
-### 3.1 目录与工程形态
-
-```
-SaluNative/
-├── SaluNative.xcodeproj
-└── SaluCRH/ # Multiplatform App(同时支持 macOS + visionOS)
- ├── SaluCRHApp.swift # @main 入口
- ├── ContentView.swift # 根视图(根据 AppRoute 切换)
- ├── AppRoute.swift # 路由枚举
- ├── GameSession.swift # 流程状态机
- ├── Views/ # 共享 UI
- │ ├── MainMenuView.swift # ✅ P2 已实现
- │ ├── MapView.swift # ✅ P2 已实现
- │ ├── BattleView.swift # P5 待实现
- │ ├── ShopView.swift # P7 待实现
- │ ├── EventView.swift # P6 待实现
- │ └── ...
- ├── ViewModels/ # 视图模型(桥接 GameCore)
- │ └── BattleSession.swift # P5 待实现
- ├── Platform/ # 平台特有代码
- │ └── visionOS/ # visionOS 特有(ImmersiveSpace 等)
- ├── Persistence/ # SwiftData 模型(P3 待实现)
- │ ├── RunSaveEntity.swift
- │ └── BattleRecordEntity.swift
- ├── AGENTS.md # 模块开发规范
- └── Assets.xcassets
-```
-
-`SaluNative.xcodeproj` 通过 “Add Local Package” 引入仓库根目录的 `Package.swift`,从而依赖 `GameCore`。
-
-### 3.2 依赖方向
-
-- `SaluCRH → GameCore` ✅
-- `GameCore → (任何 App/UI/SwiftData)` ❌(保持纯逻辑层约束)
-- `GameCLI ↔ Native Apps` ❌(互不依赖)
-
-### 3.3 平台差异处理
-
-使用条件编译处理平台差异:
-
-```swift
-// 示例:visionOS 特有的 ImmersiveSpace
-#if os(visionOS)
-import RealityKit
-
-struct ImmersiveBattleView: View {
- var body: some View {
- RealityView { ... }
- }
-}
-#endif
-
-// 示例:根据平台调整 UI
-var body: some View {
- #if os(visionOS)
- // visionOS: 更大的点击目标
- CardView().frame(width: 200, height: 300)
- #else
- // macOS: 更紧凑的布局
- CardView().frame(width: 120, height: 180)
- #endif
-}
-```
-
-### 3.4 Xcode 配置要点
-
-在 Target 的 Build Settings 中配置 Supported Platforms:
-- `SUPPORTED_PLATFORMS = macosx xros xrsimulator`
-
-或在 Xcode UI 中:Target → General → Supported Destinations,添加 visionOS。
-
----
-
-## 4. Native 持久化设计(SwiftData 优先)
-
-### 4.1 目标
-
-- 支持:
- - 单一 Run 存档(继续冒险)
- - 战斗历史(可追加、可清空、可统计)
- - 设置项(showLog 等)
-- 支持测试:
- - in-memory SwiftData container(单元测试)
- - 可选:在 debug 下允许指定数据目录(便于复现/隔离)
-
-### 4.2 推荐模型:**“索引字段 + JSON Blob”**(兼顾可查询与易演进)
-
-> 解释:`RunSnapshot`/`BattleRecord` 本身已经是稳定的 `Codable` 结构。把它们作为 blob 存起来,可以极大降低 SwiftData 模型演进成本;同时抽出少量字段用于列表/排序/统计。
-
-- `RunSaveEntity`
- - `id: UUID`
- - `updatedAt: Date`
- - `seed: UInt64`
- - `floor: Int`
- - `isOver: Bool`
- - `won: Bool`
- - `snapshotJSON: Data`(JSONEncoder 编码的 `RunSnapshot`)
-
-- `BattleRecordEntity`
- - `id: UUID`
- - `timestamp: Date`
- - `seed: UInt64`
- - `won: Bool`
- - `turnsPlayed: Int`
- - `playerFinalHP: Int`
- - `totalDamageDealt: Int`
- - `recordJSON: Data`(JSONEncoder 编码的 `BattleRecord`)
-
-### 4.3 `RunSnapshot ↔ RunState` 转换
-
-- 直接参考并“移植” CLI 的转换逻辑(`Sources/GameCLI/Persistence/SaveService.swift`)
-- 注意:
- - 还原时需要校验 `CardRegistry/RelicRegistry/ConsumableRegistry` 里是否存在对应 ID(否则视为损坏存档)
- - 版本策略沿用 `RunSaveVersion.isCompatible`
-
----
-
-## 5. 运行时状态机(Shared)
-
-### 5.1 顶层状态枚举(建议)
-
-```
-AppRoute
-- mainMenu
-- runMap(runState: RunState)
-- roomStart(...)
-- roomBattle(BattleSession)
-- roomElite(BattleSession)
-- roomBoss(BattleSession)
-- rewardCard(offer: CardRewardOffer, goldEarned: Int, context: RewardContext)
-- shop(inventory: ShopInventory, context: ShopContext)
-- event(offer: EventOffer, context: EventContext)
-- rest(...)
-- runResult(won: Bool, snapshot: RunSnapshot?)
-- history
-- statistics
-- settings
-```
-
-### 5.2 BattleSession(桥接 `BattleEngine` 与 SwiftUI)
-
-- `BattleEngine` 本身不是 Observable,需要一层包装:
- - 每次执行 `handleAction` 后,把 `engine.state` 拷贝到 `@Published var state`
- - 把 `engine.events` flush 成 UI log(并在合适时机 `engine.clearEvents()`)
-
-### 5.3 种子派生策略(与 GameCore 对齐)
-
-- **奖励**:用 `RewardContext` + `GoldRewardStrategy` / `RewardGenerator`
-- **商店**:用 `ShopContext` + `ShopInventory.generate`
-- **事件**:用 `EventContext` + `EventGenerator.generate`
-- **战斗遭遇**:
- - 参考 CLI 的做法(`BattleRoomHandler`)+ `ActXEncounterPool`
- - 建议把 `node.id` 加入 seed 派生,以避免同 row 多节点种子碰撞
-
----
-
-## 6. UI 设计(跨 macOS / visionOS 的最小一致体验)
-
-### 6.1 交互约定(统一)
-
-- 任何平台都采用一致的“选择式”交互:
- - 选卡 →(若需要)选目标 → 出牌
- - 按钮:结束回合
- - 战斗结束自动进入奖励/返回地图
-- UI 必须显式展示:
- - seed、floor、当前节点类型
- - 敌人列表的“槽位序号”(与 `targetEnemyIndex` 对齐)
-
-### 6.2 平台差异适配
-
-- **macOS**
- - 鼠标点击为主;可选:键盘数字键快捷出牌/选目标(对齐 CLI 的习惯,方便验收)
- - 可以做更紧凑的信息密度(侧边栏日志/遗物条)
-
-- **visionOS**
- - 点击目标更大、间距更大;避免密集小按钮
- - 初期保持 2D window;后续再引入 ImmersiveSpace
-
----
-
-## 7. 执行计划(按优先级 / 可交付)
-
-### ✅P1(必须):创建 Xcode 工程 + Multiplatform App,能引用 `GameCore`
-
-**目标**
-
-- `SaluCRH` 能在 macOS 和 visionOS 上编译,且能 `import GameCore`
-
-**实现步骤**
-
-- 新建 `SaluNative/SaluNative.xcodeproj`
-- 添加一个 Multiplatform App Target:`SaluCRH`
-- 配置 Supported Platforms 包含 macOS 和 visionOS
-- 通过本地 package 依赖引入 `GameCore`
-- 在入口页验证:
-
-```swift
-import GameCore
-let _ = CardRegistry.require("strike").name
-```
-
-**验证**
-
-- `swift build && swift test`(确保 SwiftPM 侧不受影响)
-- `xcodebuild -project SaluNative/SaluNative.xcodeproj -scheme SaluCRH -destination 'platform=macOS' build`
-- `xcodebuild -project SaluNative/SaluNative.xcodeproj -scheme SaluCRH -destination 'platform=visionOS Simulator,name=Apple Vision Pro' build`
-
-**当前状态**:✅ macOS 已完成,visionOS 待配置 Supported Destinations
-
----
-
-### ✅P2(必须):实现 `GameSession`(最小状态机)+ 主菜单
-
-**目标**
-
-- 主菜单支持:新游戏(输入 seed 可选)、继续(若有存档)、设置、历史/统计入口
-
-**验证**
-
-- macOS 能导航到"新游戏 → 地图页"
-- visionOS(配置 Supported Destinations 后)能导航到"新游戏 → 地图页"
-
----
-
-### P3(必须):SwiftData 落盘(RunSave + BattleHistory)
-
-**目标**
-
-- Run:自动保存、加载、清除
-- BattleHistory:追加、加载、清空、统计(基础字段)
-
-**实现要点**
-
-- `RunSnapshot`/`BattleRecord` 以 JSON blob 存储
-- 转换逻辑移植自 CLI `SaveService`(但不依赖 GameCLI)
-
-**验证**
-
-- 新游戏 → 退出到菜单 → 继续 → 状态恢复一致
-- 打完一场战斗 → 历史里可见记录
-
----
-
-### P4(重要):地图页(RunMap)+ 节点选择 + 房间路由
-
-**目标**
-
-- 地图可视化(至少能看见 `accessibleNodes`)
-- 选择节点后进入对应房间 UI(start/battle/rest/shop/event/elite/boss)
-
-**实现要点**
-
-- `RunState.accessibleNodes` / `enterNode` / `completeCurrentNode`
-
----
-
-### P5(重要):战斗房间(battle/elite/boss)完整跑通
-
-**目标**
-
-- 支持多敌人
-- 出牌、目标选择、结束回合
-- 战斗结束后:
- - 记录 `BattleRecord`
- - 更新 `RunState.updateFromBattle`
- - 发放金币与卡牌奖励(battle/elite)
- - elite/boss 额外:遗物奖励(可跳过)
-
-**实现要点**
-
-- 奖励与金币用 `RewardContext` + `GoldRewardStrategy` / `RewardGenerator`
-- 遭遇生成参考 `ActXEncounterPool`(与 CLI 保持一致或更严格的 seed 派生)
-
----
-
-### P6(重要):事件房间(Event)+ follow-up 支持
-
-**目标**
-
-- 事件展示(名称/描述/选项)
-- 选择选项后应用 `RunEffect`
-- 支持二次选择(例如:`chooseUpgradeableCard`)
-
-**实现要点**
-
-- `EventGenerator.generate(context:)` → `EventOffer`
-- follow-up 用二级 route(例如弹出卡牌选择 sheet)
-
----
-
-### P7(重要):商店房间(Shop)+ 删牌/消耗品槽位
-
-**目标**
-
-- 展示 5 张卡、3 遗物、3 消耗品、删牌服务
-- 购买/金币不足提示
-- 消耗品槽位满时不可购买
-
-**实现要点**
-
-- `ShopInventory.generate(context:)`
-- 交易成功后修改 `RunState`(加卡/加遗物/加消耗品/删牌/扣金币)
-
----
-
-### P8(优化):visionOS 体验增强(不阻塞主流程)
-
-- UI 间距与可点击面积调整
-- 可选:ImmersiveSpace “战斗桌面”原型(只做展示,不影响战斗规则)
-
----
-
-## 8. 风险清单(提前规避)
-
-- SwiftData 对复杂类型(Dictionary 等)支持有限 → 使用 JSON blob + 索引字段方案规避
-- `BattleEngine` 非线程安全(`@unchecked Sendable`) → 强制在 MainActor/单线程使用
-- visionOS 输入“目标选择”易误触 → 采用两步式选择(先选卡再选敌人)并提供取消
-- Xcode 工程引入后影响仓库整洁 → `.gitignore` 必须忽略 `xcuserdata/`、`DerivedData` 等
-
----
-
-## 9. 进度记录
-
-| P | 完成日期 | 验证命令 | 结果 |
-|---|----------|----------|------|
-| P1 | 2026-01-14 | `xcodebuild ... -scheme SaluCRH -destination 'platform=macOS' build` | ✅ macOS 通过 / ⏳ visionOS 待配置 |
-| P2 | 2026-01-14 | `xcodebuild ... -scheme SaluCRH -destination 'platform=macOS' build` | ✅ macOS 通过(主菜单 + 地图页) |
-| P3 | - | - | - |
-| P4 | - | - | - |
-| P5 | - | - | - |
-| P6 | - | - | - |
-| P7 | - | - | - |
-| P8 | - | - | - |
diff --git a/.gitignore b/.gitignore
index 630c57c..09c8fa1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
.DS_Store
.build/
-Packages/
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
diff --git a/AGENTS.md b/AGENTS.md
index d61f9b5..d3d9b33 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,10 +6,12 @@ Salu 是一个跨平台(macOS/Linux/Windows)的回合制卡牌战斗游戏
- `Sources/GameCore/`:纯逻辑层(规则/状态/战斗/卡牌/敌人/地图/存档快照模型)。禁止 I/O、禁止 UI;详细约束见 `Sources/GameCore/AGENTS.md`。
- `Sources/GameCLI/`:CLI/TUI 表现层(终端渲染/输入/房间流程/持久化落盘)。详细约束见 `Sources/GameCLI/AGENTS.md`。
-- `SaluNative/SaluCRH/`:原生 App(Multiplatform SwiftUI + SwiftData,支持 macOS/visionOS)。通过 Xcode 项目管理,依赖 `GameCore`。采用单一 Target + 条件编译 (`#if os()`) 处理平台差异。详见 `SaluNative/SaluCRH/AGENTS.md`。
+- `SaluNative/`:原生 App(Xcode 管理,依赖 `GameCore`,不依赖 `GameCLI`)
+ - `SaluNative/SaluAVP/`:Apple Vision Pro(visionOS)App(原生 3D:ImmersiveSpace + RealityKit,主线)
+ - (可选)`SaluNative/Shared/`:跨 Target 共享的状态机/桥接层(禁止引入 RealityKit)
- `Tests/`:`GameCoreTests`、`GameCLITests`、`GameCLIUITests`。
-- `.giithub/docs/`:设定、剧情与玩法规则说明(写内容/做 UI 时优先对齐这里)。
-- `.giithub/plans/`:技术方案与执行计划。
+- `.github/docs/`:设定、剧情与玩法规则说明(写内容/做 UI 时优先对齐这里)。
+- `.github/plans/`:技术方案与执行计划。
## 构建、测试和开发命令
@@ -26,28 +28,22 @@ swift run # 本地运行(GameCLI)
# - 仅修改文档/CI 配置:通常可跳过构建与测试(但建议至少保证相关命令不明显失效)
```
-### Xcode(原生 App:macOS / visionOS)
+### Xcode(原生 App:visionOS)
```bash
# 或直接双击 SaluNative/SaluNative.xcodeproj
open SaluNative/SaluNative.xcodeproj
-# 命令行编译(macOS)
-xcodebuild -project SaluNative/SaluNative.xcodeproj \
- -scheme SaluCRH \
- -destination 'platform=macOS' \
- build
-
# 命令行编译(visionOS Simulator)
xcodebuild -project SaluNative/SaluNative.xcodeproj \
- -scheme SaluCRH \
+ -scheme SaluAVP \
-destination 'platform=visionOS Simulator,name=Apple Vision Pro' \
build
```
## 本地存储与配置
-- 本地存储默认位置与文件结构见 `.giithub/docs/本地存储说明.md`(run 存档、战斗历史、设置、调试日志)。
+- 本地存储默认位置与文件结构见 `.github/docs/本地存储说明.md`(run 存档、战斗历史、设置、调试日志)。
- 需要隔离数据(测试/调试/复现 bug)时,优先用环境变量覆盖数据目录:`SALU_DATA_DIR=/tmp/salu-test`。
- 复现战斗/地图行为时建议固定随机种子:`swift run GameCLI --seed 1`(也可用 `--seed=1`)。
@@ -71,8 +67,8 @@ swift test --filter GameCLITests
## 开发流程建议
- 修改前先定位模块边界:规则与状态放 `GameCore`,文件读写与终端渲染放 `GameCLI`(避免反向依赖)。
-- 提交前按影响范围验证:修改 `Sources/**/*.swift` 或 `Package.swift` 时至少跑一次 `swift test`;仅改 `SaluNative/` 时至少跑一次 `xcodebuild -project SaluNative/SaluNative.xcodeproj -scheme SaluCRH -destination 'platform=macOS' build`。
-- 文档/剧情/玩法规则的变更优先同步到 `.giithub/docs/`,并在 PR 描述里注明对应章节。
+- 提交前按影响范围验证:修改 `Sources/**/*.swift` 或 `Package.swift` 时至少跑一次 `swift test`;仅改 `SaluNative/` 时至少跑一次 `xcodebuild -project SaluNative/SaluNative.xcodeproj -scheme SaluAVP -destination 'platform=visionOS Simulator,name=Apple Vision Pro' build`。
+- 文档/剧情/玩法规则的变更优先同步到 `.github/docs/`,并在 PR 描述里注明对应章节。
## 提交与 Pull Request 规范
diff --git a/README-en.md b/README-en.md
index ea7ee6b..941c2db 100644
--- a/README-en.md
+++ b/README-en.md
@@ -6,11 +6,10 @@ A cross-platform (macOS/Linux/Windows) turn-based card-battle game inspired by *
### Option 1: Native App (in development 🚧)
-Open `SaluNative/SaluNative.xcodeproj` in Xcode and run `SaluCRH`.
+Open `SaluNative/SaluNative.xcodeproj` in Xcode and run `SaluAVP`.
Supported platforms:
-- **macOS** 14+ (supported ✅)
-- **visionOS** 2+ (in progress ⏳)
+- **visionOS** 2+ (in development 🚧)
> Requirements: Xcode 16+ / macOS 14+
@@ -60,4 +59,4 @@ This project is layered by architecture, and each module follows its own guideli
- `GameCore`: pure logic layer (rules/state/battle/cards/enemies/map/save snapshot models), see [GameCore guidelines](Sources/GameCore/AGENTS.md)
- `GameCLI`: CLI/TUI presentation layer (terminal rendering/input/room flow/persistence), see [GameCLI guidelines](Sources/GameCLI/AGENTS.md)
-- `SaluNative/SaluCRH`: native app (Multiplatform SwiftUI + SwiftData, macOS/visionOS), see [SaluCRH guidelines](SaluNative/SaluCRH/AGENTS.md)
+- `SaluNative/SaluAVP`: native app (visionOS, ImmersiveSpace + RealityKit), see `.github/plans/Plan AVP - Apple Vision Pro 原生 3D 实现(SaluAVP).md`
diff --git a/README.md b/README.md
index 6c5a6bf..6ff7c92 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,10 @@
### 方式一:原生 App(开发中 🚧)
-使用 Xcode 打开 `SaluNative/SaluNative.xcodeproj` 并运行 `SaluCRH`。
+使用 Xcode 打开 `SaluNative/SaluNative.xcodeproj` 并运行 `SaluAVP`。
支持平台:
-- **macOS** 14+(已支持 ✅)
-- **visionOS** 2+(配置中 ⏳)
-
-> 要求:Xcode 16+ / macOS 14+
+- **visionOS** 26.0+(开发中 🚧)
### 方式二:命令行版本(CLI)
@@ -62,4 +59,4 @@ tar -xzf salu-macos.tar.gz
- `GameCore`:纯逻辑层(规则/状态/战斗/卡牌/敌人/地图/存档快照模型),见 [GameCore 开发规范](Sources/GameCore/AGENTS.md)
- `GameCLI`:CLI/TUI 表现层(终端渲染/输入/房间流程/持久化落盘实现),见 [GameCLI 开发规范](Sources/GameCLI/AGENTS.md)
-- `SaluNative/SaluCRH`:原生 App(Multiplatform SwiftUI + SwiftData,支持 macOS/visionOS),见 [SaluCRH 开发规范](SaluNative/SaluCRH/AGENTS.md)
+- `SaluNative/SaluAVP`:原生 App(visionOS,ImmersiveSpace + RealityKit),见 `.github/plans/Plan AVP - Apple Vision Pro 原生 3D 实现(SaluAVP).md`
diff --git a/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json b/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json
new file mode 100644
index 0000000..df6c716
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json
@@ -0,0 +1,10 @@
+{
+ "pathsToIds" : {
+ "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/GridMaterial.usda" : "95052448-183C-40B5-9E52-3717AF9B48FA",
+ "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Immersive.usda" : "65F6F990-A780-4474-B78B-572E0E4E273D",
+ "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Scene.usda" : "0A9B4653-B11E-4D6A-850E-C6FCB621626C",
+ "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/GridMaterial.usda" : "A21C1E11-ABB0-4972-8159-55AD3A9AA5B3",
+ "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Immersive.usda" : "1D572CEB-057A-41C3-B575-04C37501A3C0",
+ "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Scene.usda" : "D66134B1-3681-4A8E-AFE5-29F257229F3B"
+ }
+}
\ No newline at end of file
diff --git a/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json b/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json
new file mode 100644
index 0000000..fcd90e3
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json
@@ -0,0 +1,202 @@
+{
+ "0A9B4653-B11E-4D6A-850E-C6FCB621626C" : {
+ "cameraTransform" : [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0.86602545,
+ -0.49999994,
+ 0,
+ 0,
+ 0.49999994,
+ 0.86602545,
+ 0,
+ -7.719708e-08,
+ 0.36129734,
+ 0.62580043,
+ 1
+ ],
+ "objectMetadataList" : [
+ [
+ "0A9B4653-B11E-4D6A-850E-C6FCB621626C",
+ "Root"
+ ],
+ {
+ "isExpanded" : true,
+ "isLocked" : false
+ }
+ ]
+ },
+ "1D572CEB-057A-41C3-B575-04C37501A3C0" : {
+ "cameraTransform" : [
+ 1,
+ 0,
+ -0,
+ 0,
+ -0,
+ 0.7071069,
+ -0.7071067,
+ 0,
+ 0,
+ 0.7071067,
+ 0.7071069,
+ 0,
+ 0,
+ 2.8834329,
+ -0.1072194,
+ 1
+ ],
+ "objectMetadataList" : [
+ [
+ "1D572CEB-057A-41C3-B575-04C37501A3C0",
+ "Root",
+ "Sphere_Left"
+ ],
+ {
+ "isExpanded" : true,
+ "isLocked" : false
+ },
+ [
+ "1D572CEB-057A-41C3-B575-04C37501A3C0",
+ "Root",
+ "Sphere_Right"
+ ],
+ {
+ "isExpanded" : true,
+ "isLocked" : false
+ },
+ [
+ "1D572CEB-057A-41C3-B575-04C37501A3C0",
+ "Root"
+ ],
+ {
+ "isExpanded" : true,
+ "isLocked" : false
+ }
+ ]
+ },
+ "65F6F990-A780-4474-B78B-572E0E4E273D" : {
+ "cameraTransform" : [
+ 1,
+ 0,
+ -0,
+ 0,
+ -0,
+ 0.86602545,
+ -0.49999988,
+ 0,
+ 0,
+ 0.49999988,
+ 0.86602545,
+ 0,
+ 1.1972517e-08,
+ 2.6179132,
+ 0.43191218,
+ 1
+ ],
+ "objectMetadataList" : [
+ [
+ "65F6F990-A780-4474-B78B-572E0E4E273D",
+ "Root"
+ ],
+ {
+ "isExpanded" : true,
+ "isLocked" : false
+ }
+ ]
+ },
+ "95052448-183C-40B5-9E52-3717AF9B48FA" : {
+ "cameraTransform" : [
+ 0.99999994,
+ 0,
+ -0,
+ 0,
+ -0,
+ 0.8660254,
+ -0.49999994,
+ 0,
+ 0,
+ 0.49999994,
+ 0.8660254,
+ 0,
+ 0,
+ 0.5981957,
+ 1.0361054,
+ 1
+ ],
+ "objectMetadataList" : [
+ [
+ "95052448-183C-40B5-9E52-3717AF9B48FA",
+ "Root"
+ ],
+ {
+ "isExpanded" : true,
+ "isLocked" : false
+ }
+ ]
+ },
+ "A21C1E11-ABB0-4972-8159-55AD3A9AA5B3" : {
+ "cameraTransform" : [
+ 1,
+ 0,
+ -0,
+ 0,
+ -0,
+ 0.8660254,
+ -0.5,
+ 0,
+ 0,
+ 0.5,
+ 0.8660254,
+ 0,
+ 0,
+ 0.23875366,
+ 0.4135335,
+ 1
+ ],
+ "objectMetadataList" : [
+
+ ]
+ },
+ "D66134B1-3681-4A8E-AFE5-29F257229F3B" : {
+ "cameraTransform" : [
+ 1,
+ 0,
+ -0,
+ 0,
+ -0,
+ 0.7071069,
+ -0.7071067,
+ 0,
+ 0,
+ 0.7071067,
+ 0.7071069,
+ 0,
+ 0,
+ 0.53307986,
+ 0.53307986,
+ 1
+ ],
+ "objectMetadataList" : [
+ [
+ "D66134B1-3681-4A8E-AFE5-29F257229F3B",
+ "Root",
+ "Sphere"
+ ],
+ {
+ "isExpanded" : true,
+ "isLocked" : false
+ },
+ [
+ "D66134B1-3681-4A8E-AFE5-29F257229F3B",
+ "Root"
+ ],
+ {
+ "isExpanded" : true,
+ "isLocked" : false
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata b/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata
new file mode 100644
index 0000000..6dea95c
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata
@@ -0,0 +1,17 @@
+{
+ "cameraPresets" : {
+
+ },
+ "secondaryToolbarData" : {
+ "isGridVisible" : true,
+ "sceneReverbPreset" : -1
+ },
+ "unitDefaults" : {
+ "°" : "°",
+ "kg" : "g",
+ "m" : "cm",
+ "m\/s" : "m\/s",
+ "m\/s²" : "m\/s²",
+ "s" : "s"
+ }
+}
\ No newline at end of file
diff --git a/SaluNative/Packages/RealityKitContent/Package.swift b/SaluNative/Packages/RealityKitContent/Package.swift
new file mode 100644
index 0000000..21f87fd
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/Package.swift
@@ -0,0 +1,34 @@
+// swift-tools-version:6.2
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "RealityKitContent",
+ platforms: [
+ .visionOS(.v26),
+ .macOS(.v26),
+ .iOS(.v26),
+ .tvOS(.v26)
+ ],
+ products: [
+ // Products define the executables and libraries a package produces, and make them visible to other packages.
+ .library(
+ name: "RealityKitContent",
+ targets: ["RealityKitContent"]),
+ ],
+ dependencies: [
+ // Dependencies declare other packages that this package depends on.
+ // .package(url: /* package url */, from: "1.0.0"),
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+ // Targets can depend on other targets in this package, and on products in packages this package depends on.
+ .target(
+ name: "RealityKitContent",
+ dependencies: [],
+ swiftSettings: [
+ .enableUpcomingFeature("MemberImportVisibility")
+ ]),
+ ]
+)
\ No newline at end of file
diff --git a/SaluNative/Packages/RealityKitContent/README.md b/SaluNative/Packages/RealityKitContent/README.md
new file mode 100644
index 0000000..486b575
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/README.md
@@ -0,0 +1,3 @@
+# RealityKitContent
+
+A description of this package.
\ No newline at end of file
diff --git a/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Immersive.usda b/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Immersive.usda
new file mode 100644
index 0000000..4b4e5f1
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Immersive.usda
@@ -0,0 +1,54 @@
+#usda 1.0
+(
+ defaultPrim = "Root"
+ metersPerUnit = 1
+ upAxis = "Y"
+)
+
+reorder rootPrims = ["Root", "GridMaterial"]
+
+def Xform "Root"
+{
+ reorder nameChildren = ["Sphere_Left", "Sphere_Right", "GridMaterial"]
+ def Sphere "Sphere_Right" (
+ active = true
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ rel material:binding = (
+ bindMaterialAs = "weakerThanDescendants"
+ )
+ double radius = 0.1
+ quatf xformOp:orient = (1, 0, 0, 0)
+ float3 xformOp:scale = (1, 1, 1)
+ float3 xformOp:translate = (0.5, 1.5, -1.5)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]
+ }
+
+ def Sphere "Sphere_Left" (
+ active = true
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ rel material:binding = (
+ bindMaterialAs = "weakerThanDescendants"
+ )
+ double radius = 0.1
+ quatf xformOp:orient = (1, 0, 0, 0)
+ float3 xformOp:scale = (1, 1, 1)
+ float3 xformOp:translate = (-0.5, 1.5, -1.5)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]
+ }
+
+ def "GridMaterial" (
+ active = true
+ references = @Materials/GridMaterial.usda@
+ )
+ {
+ quatf xformOp:orient = (1, 0, 0, 0)
+ float3 xformOp:scale = (1, 1, 1)
+ float3 xformOp:translate = (0, 0, 0)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]
+ }
+}
+
diff --git a/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda b/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda
new file mode 100644
index 0000000..b7afd02
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda
@@ -0,0 +1,216 @@
+#usda 1.0
+(
+ defaultPrim = "Root"
+ metersPerUnit = 1
+ upAxis = "Y"
+)
+
+def Xform "Root"
+{
+ def Material "GridMaterial"
+ {
+ reorder nameChildren = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "DefaultSurfaceShader", "MaterialXPreviewSurface", "Texcoord", "Add", "Multiply", "Fractional", "LineCounts", "Multiply_1", "Separate2", "Separate2_1", "Ifgreater", "Ifgreater_1", "Max", "Background_Color"]
+ token outputs:mtlx:surface.connect =
+ token outputs:realitykit:vertex
+ token outputs:surface
+ float2 ui:nodegraph:realitykit:subgraphOutputs:pos = (2222, 300.5)
+ float2 ui:nodegraph:realitykit:subgraphOutputs:size = (182, 89)
+ int ui:nodegraph:realitykit:subgraphOutputs:stackingOrder = 749
+
+ def Shader "DefaultSurfaceShader"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (1, 1, 1)
+ float inputs:roughness = 0.75
+ token outputs:surface
+ }
+
+ def Shader "MaterialXPreviewSurface"
+ {
+ uniform token info:id = "ND_UsdPreviewSurface_surfaceshader"
+ float inputs:clearcoat
+ float inputs:clearcoatRoughness
+ color3f inputs:diffuseColor.connect =
+ color3f inputs:emissiveColor
+ float inputs:ior
+ float inputs:metallic = 0.15
+ float3 inputs:normal
+ float inputs:occlusion
+ float inputs:opacity
+ float inputs:opacityThreshold
+ float inputs:roughness = 0.5
+ token outputs:out
+ float2 ui:nodegraph:node:pos = (1967, 300.5)
+ float2 ui:nodegraph:node:size = (208, 297)
+ int ui:nodegraph:node:stackingOrder = 870
+ string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["Advanced"]
+ }
+
+ def Shader "Texcoord"
+ {
+ uniform token info:id = "ND_texcoord_vector2"
+ float2 outputs:out
+ float2 ui:nodegraph:node:pos = (94.14453, 35.29297)
+ float2 ui:nodegraph:node:size = (182, 43)
+ int ui:nodegraph:node:stackingOrder = 1358
+ }
+
+ def Shader "Multiply"
+ {
+ uniform token info:id = "ND_multiply_vector2"
+ float2 inputs:in1.connect =
+ float2 inputs:in2 = (32, 15)
+ float2 inputs:in2.connect =
+ float2 outputs:out
+ float2 ui:nodegraph:node:pos = (275.64453, 47.29297)
+ float2 ui:nodegraph:node:size = (61, 36)
+ int ui:nodegraph:node:stackingOrder = 1348
+ string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["inputs:in2"]
+ }
+
+ def Shader "Fractional"
+ {
+ uniform token info:id = "ND_realitykit_fractional_vector2"
+ float2 inputs:in.connect =
+ float2 outputs:out
+ float2 ui:nodegraph:node:pos = (440.5, 49.5)
+ float2 ui:nodegraph:node:size = (155, 99)
+ int ui:nodegraph:node:stackingOrder = 1345
+ }
+
+ def Shader "BaseColor"
+ {
+ uniform token info:id = "ND_constant_color3"
+ color3f inputs:value = (0.89737034, 0.89737034, 0.89737034) (
+ colorSpace = "Input - Texture - sRGB - sRGB"
+ )
+ color3f inputs:value.connect = None
+ color3f outputs:out
+ float2 ui:nodegraph:node:pos = (1537.5977, 363.07812)
+ float2 ui:nodegraph:node:size = (150, 43)
+ int ui:nodegraph:node:stackingOrder = 1353
+ }
+
+ def Shader "LineColor"
+ {
+ uniform token info:id = "ND_constant_color3"
+ color3f inputs:value = (0.55945957, 0.55945957, 0.55945957) (
+ colorSpace = "Input - Texture - sRGB - sRGB"
+ )
+ color3f inputs:value.connect = None
+ color3f outputs:out
+ float2 ui:nodegraph:node:pos = (1536.9844, 287.86328)
+ float2 ui:nodegraph:node:size = (146, 43)
+ int ui:nodegraph:node:stackingOrder = 1355
+ }
+
+ def Shader "LineWidths"
+ {
+ uniform token info:id = "ND_combine2_vector2"
+ float inputs:in1 = 0.1
+ float inputs:in2 = 0.1
+ float2 outputs:out
+ float2 ui:nodegraph:node:pos = (443.64453, 233.79297)
+ float2 ui:nodegraph:node:size = (151, 43)
+ int ui:nodegraph:node:stackingOrder = 1361
+ }
+
+ def Shader "LineCounts"
+ {
+ uniform token info:id = "ND_combine2_vector2"
+ float inputs:in1 = 24
+ float inputs:in2 = 12
+ float2 outputs:out
+ float2 ui:nodegraph:node:pos = (94.14453, 138.29297)
+ float2 ui:nodegraph:node:size = (153, 43)
+ int ui:nodegraph:node:stackingOrder = 1359
+ }
+
+ def Shader "Remap"
+ {
+ uniform token info:id = "ND_remap_color3"
+ color3f inputs:in.connect =
+ color3f inputs:inhigh.connect = None
+ color3f inputs:inlow.connect = None
+ color3f inputs:outhigh.connect =
+ color3f inputs:outlow.connect =
+ color3f outputs:out
+ float2 ui:nodegraph:node:pos = (1755.5, 300.5)
+ float2 ui:nodegraph:node:size = (95, 171)
+ int ui:nodegraph:node:stackingOrder = 1282
+ string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["inputs:outlow"]
+ }
+
+ def Shader "Separate2"
+ {
+ uniform token info:id = "ND_separate2_vector2"
+ float2 inputs:in.connect =
+ float outputs:outx
+ float outputs:outy
+ float2 ui:nodegraph:node:pos = (1212.6445, 128.91797)
+ float2 ui:nodegraph:node:size = (116, 117)
+ int ui:nodegraph:node:stackingOrder = 1363
+ }
+
+ def Shader "Combine3"
+ {
+ uniform token info:id = "ND_combine3_color3"
+ float inputs:in1.connect =
+ float inputs:in2.connect =
+ float inputs:in3.connect =
+ color3f outputs:out
+ float2 ui:nodegraph:node:pos = (1578.1445, 128.91797)
+ float2 ui:nodegraph:node:size = (146, 54)
+ int ui:nodegraph:node:stackingOrder = 1348
+ }
+
+ def Shader "Range"
+ {
+ uniform token info:id = "ND_range_vector2"
+ bool inputs:doclamp = 1
+ float2 inputs:gamma = (2, 2)
+ float2 inputs:in.connect =
+ float2 inputs:inhigh.connect =
+ float2 inputs:inlow = (0.02, 0.02)
+ float2 inputs:outhigh
+ float2 inputs:outlow
+ float2 outputs:out
+ float2 ui:nodegraph:node:pos = (990.64453, 128.91797)
+ float2 ui:nodegraph:node:size = (98, 207)
+ int ui:nodegraph:node:stackingOrder = 1364
+ }
+
+ def Shader "Subtract"
+ {
+ uniform token info:id = "ND_subtract_vector2"
+ float2 inputs:in1.connect =
+ float2 inputs:in2.connect =
+ float2 outputs:out
+ float2 ui:nodegraph:node:pos = (612.64453, 87.04297)
+ float2 ui:nodegraph:node:size = (63, 36)
+ int ui:nodegraph:node:stackingOrder = 1348
+ }
+
+ def Shader "Absval"
+ {
+ uniform token info:id = "ND_absval_vector2"
+ float2 inputs:in.connect =
+ float2 outputs:out
+ float2 ui:nodegraph:node:pos = (765.64453, 87.04297)
+ float2 ui:nodegraph:node:size = (123, 43)
+ int ui:nodegraph:node:stackingOrder = 1348
+ }
+
+ def Shader "Min"
+ {
+ uniform token info:id = "ND_min_float"
+ float inputs:in1.connect =
+ float inputs:in2.connect =
+ float outputs:out
+ float2 ui:nodegraph:node:pos = (1388.1445, 128.91797)
+ float2 ui:nodegraph:node:size = (114, 36)
+ int ui:nodegraph:node:stackingOrder = 1363
+ }
+ }
+}
+
diff --git a/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda b/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda
new file mode 100644
index 0000000..ae63e29
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda
@@ -0,0 +1,62 @@
+#usda 1.0
+(
+ defaultPrim = "Root"
+ metersPerUnit = 1
+ upAxis = "Y"
+)
+
+reorder rootPrims = ["Root", "GridMaterial"]
+
+def Xform "Root"
+{
+ reorder nameChildren = ["GridMaterial", "Sphere"]
+ rel material:binding = None (
+ bindMaterialAs = "weakerThanDescendants"
+ )
+
+ def Sphere "Sphere" (
+ active = true
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ reorder nameChildren = ["Collider", "InputTarget", "GridMaterial"]
+ rel material:binding = (
+ bindMaterialAs = "weakerThanDescendants"
+ )
+ double radius = 0.1
+ quatf xformOp:orient = (1, 0, 0, 0)
+ float3 xformOp:scale = (1, 1, 1)
+ float3 xformOp:translate = (0, 0, 0)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]
+
+ def RealityKitComponent "Collider"
+ {
+ uint group = 1
+ uniform token info:id = "RealityKit.Collider"
+ uint mask = 4294967295
+ token type = "Default"
+
+ def RealityKitStruct "Shape"
+ {
+ float3 extent = (0.2, 0.2, 0.2)
+ float radius = 0.1
+ token shapeType = "Sphere"
+ }
+ }
+
+ def RealityKitComponent "InputTarget"
+ {
+ uniform token info:id = "RealityKit.InputTarget"
+ }
+ }
+
+ def "GridMaterial" (
+ active = true
+ references = @Materials/GridMaterial.usda@
+ )
+ {
+ float3 xformOp:scale = (1, 1, 1)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]
+ }
+}
+
diff --git a/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift b/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift
new file mode 100644
index 0000000..5caba4e
--- /dev/null
+++ b/SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift
@@ -0,0 +1,4 @@
+import Foundation
+
+/// Bundle for the RealityKitContent project
+public let realityKitContentBundle = Bundle.module
diff --git a/SaluNative/SaluAVP/AppModel.swift b/SaluNative/SaluAVP/AppModel.swift
new file mode 100644
index 0000000..dc3b0f7
--- /dev/null
+++ b/SaluNative/SaluAVP/AppModel.swift
@@ -0,0 +1,21 @@
+//
+// AppModel.swift
+// SaluAVP
+//
+// Created by chii_magnus on 2026/1/29.
+//
+
+import SwiftUI
+
+/// Maintains app-wide state
+@MainActor
+@Observable
+class AppModel {
+ let immersiveSpaceID = "ImmersiveSpace"
+ enum ImmersiveSpaceState {
+ case closed
+ case inTransition
+ case open
+ }
+ var immersiveSpaceState = ImmersiveSpaceState.closed
+}
diff --git a/SaluNative/SaluCRH/Assets.xcassets/AccentColor.colorset/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AccentColor.colorset/Contents.json
similarity index 100%
rename from SaluNative/SaluCRH/Assets.xcassets/AccentColor.colorset/Contents.json
rename to SaluNative/SaluAVP/Assets.xcassets/AccentColor.colorset/Contents.json
diff --git a/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000..04056a5
--- /dev/null
+++ b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "vision",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SaluNative/SaluCRH/Assets.xcassets/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json
similarity index 100%
rename from SaluNative/SaluCRH/Assets.xcassets/Contents.json
rename to SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json
diff --git a/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Contents.json
new file mode 100644
index 0000000..950af4d
--- /dev/null
+++ b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Contents.json
@@ -0,0 +1,17 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "layers" : [
+ {
+ "filename" : "Front.solidimagestacklayer"
+ },
+ {
+ "filename" : "Middle.solidimagestacklayer"
+ },
+ {
+ "filename" : "Back.solidimagestacklayer"
+ }
+ ]
+}
diff --git a/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000..04056a5
--- /dev/null
+++ b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "vision",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000..04056a5
--- /dev/null
+++ b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "vision",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SaluNative/SaluAVP/Assets.xcassets/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/SaluNative/SaluAVP/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/SaluNative/SaluAVP/ContentView.swift b/SaluNative/SaluAVP/ContentView.swift
new file mode 100644
index 0000000..50de846
--- /dev/null
+++ b/SaluNative/SaluAVP/ContentView.swift
@@ -0,0 +1,53 @@
+//
+// ContentView.swift
+// SaluAVP
+//
+// Created by chii_magnus on 2026/1/29.
+//
+
+import SwiftUI
+import RealityKit
+import RealityKitContent
+
+struct ContentView: View {
+
+ @State private var enlarge = false
+
+ var body: some View {
+ RealityView { content in
+ // Add the initial RealityKit content
+ if let scene = try? await Entity(named: "Scene", in: realityKitContentBundle) {
+ content.add(scene)
+ }
+ } update: { content in
+ // Update the RealityKit content when SwiftUI state changes
+ if let scene = content.entities.first {
+ let uniformScale: Float = enlarge ? 1.4 : 1.0
+ scene.transform.scale = [uniformScale, uniformScale, uniformScale]
+ }
+ }
+ .gesture(TapGesture().targetedToAnyEntity().onEnded { _ in
+ enlarge.toggle()
+ })
+ .toolbar {
+ ToolbarItemGroup(placement: .bottomOrnament) {
+ VStack (spacing: 12) {
+ Button {
+ enlarge.toggle()
+ } label: {
+ Text(enlarge ? "Reduce RealityView Content" : "Enlarge RealityView Content")
+ }
+ .animation(.none, value: 0)
+ .fontWeight(.semibold)
+
+ ToggleImmersiveSpaceButton()
+ }
+ }
+ }
+ }
+}
+
+#Preview(windowStyle: .volumetric) {
+ ContentView()
+ .environment(AppModel())
+}
diff --git a/SaluNative/SaluAVP/ImmersiveView.swift b/SaluNative/SaluAVP/ImmersiveView.swift
new file mode 100644
index 0000000..c18098a
--- /dev/null
+++ b/SaluNative/SaluAVP/ImmersiveView.swift
@@ -0,0 +1,30 @@
+//
+// ImmersiveView.swift
+// SaluAVP
+//
+// Created by chii_magnus on 2026/1/29.
+//
+
+import SwiftUI
+import RealityKit
+import RealityKitContent
+
+struct ImmersiveView: View {
+
+ var body: some View {
+ RealityView { content in
+ // Add the initial RealityKit content
+ if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
+ content.add(immersiveContentEntity)
+
+ // Put skybox here. See example in World project available at
+ // https://developer.apple.com/
+ }
+ }
+ }
+}
+
+#Preview(immersionStyle: .mixed) {
+ ImmersiveView()
+ .environment(AppModel())
+}
diff --git a/SaluNative/SaluAVP/Info.plist b/SaluNative/SaluAVP/Info.plist
new file mode 100644
index 0000000..852f456
--- /dev/null
+++ b/SaluNative/SaluAVP/Info.plist
@@ -0,0 +1,23 @@
+
+
+
+
+ UIApplicationSceneManifest
+
+ UIApplicationPreferredDefaultSceneSessionRole
+ UIWindowSceneSessionRoleVolumetricApplication
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UISceneSessionRoleImmersiveSpaceApplication
+
+
+ UISceneInitialImmersionStyle
+ UIImmersionStyleMixed
+
+
+
+
+
+
diff --git a/SaluNative/SaluAVP/SaluAVPApp.swift b/SaluNative/SaluAVP/SaluAVPApp.swift
new file mode 100644
index 0000000..8a62d9c
--- /dev/null
+++ b/SaluNative/SaluAVP/SaluAVPApp.swift
@@ -0,0 +1,34 @@
+//
+// SaluAVPApp.swift
+// SaluAVP
+//
+// Created by chii_magnus on 2026/1/29.
+//
+
+import SwiftUI
+
+@main
+struct SaluAVPApp: App {
+
+ @State private var appModel = AppModel()
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .environment(appModel)
+ }
+ .windowStyle(.volumetric)
+
+ ImmersiveSpace(id: appModel.immersiveSpaceID) {
+ ImmersiveView()
+ .environment(appModel)
+ .onAppear {
+ appModel.immersiveSpaceState = .open
+ }
+ .onDisappear {
+ appModel.immersiveSpaceState = .closed
+ }
+ }
+ .immersionStyle(selection: .constant(.mixed), in: .mixed)
+ }
+}
diff --git a/SaluNative/SaluAVP/ToggleImmersiveSpaceButton.swift b/SaluNative/SaluAVP/ToggleImmersiveSpaceButton.swift
new file mode 100644
index 0000000..67b1b3d
--- /dev/null
+++ b/SaluNative/SaluAVP/ToggleImmersiveSpaceButton.swift
@@ -0,0 +1,58 @@
+//
+// ToggleImmersiveSpaceButton.swift
+// SaluAVP
+//
+// Created by chii_magnus on 2026/1/29.
+//
+
+import SwiftUI
+
+struct ToggleImmersiveSpaceButton: View {
+
+ @Environment(AppModel.self) private var appModel
+
+ @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
+ @Environment(\.openImmersiveSpace) private var openImmersiveSpace
+
+ var body: some View {
+ Button {
+ Task { @MainActor in
+ switch appModel.immersiveSpaceState {
+ case .open:
+ appModel.immersiveSpaceState = .inTransition
+ await dismissImmersiveSpace()
+ // Don't set immersiveSpaceState to .closed because there
+ // are multiple paths to ImmersiveView.onDisappear().
+ // Only set .closed in ImmersiveView.onDisappear().
+
+ case .closed:
+ appModel.immersiveSpaceState = .inTransition
+ switch await openImmersiveSpace(id: appModel.immersiveSpaceID) {
+ case .opened:
+ // Don't set immersiveSpaceState to .open because there
+ // may be multiple paths to ImmersiveView.onAppear().
+ // Only set .open in ImmersiveView.onAppear().
+ break
+
+ case .userCancelled, .error:
+ // On error, we need to mark the immersive space
+ // as closed because it failed to open.
+ fallthrough
+ @unknown default:
+ // On unknown response, assume space did not open.
+ appModel.immersiveSpaceState = .closed
+ }
+
+ case .inTransition:
+ // This case should not ever happen because button is disabled for this case.
+ break
+ }
+ }
+ } label: {
+ Text(appModel.immersiveSpaceState == .open ? "Hide Immersive Space" : "Show Immersive Space")
+ }
+ .disabled(appModel.immersiveSpaceState == .inTransition)
+ .animation(.none, value: 0)
+ .fontWeight(.semibold)
+ }
+}
diff --git a/SaluNative/SaluCRH/AGENTS.md b/SaluNative/SaluCRH/AGENTS.md
deleted file mode 100644
index 8656b01..0000000
--- a/SaluNative/SaluCRH/AGENTS.md
+++ /dev/null
@@ -1,245 +0,0 @@
-# SaluCRH 模块开发规范
-
-SaluCRH 是 Salu 的原生 App 前端,采用 Multiplatform SwiftUI 架构,同时支持 macOS 和 visionOS。
-
-## 模块职责
-
-- **UI 层**:SwiftUI 视图、动画、用户交互
-- **状态管理**:GameSession 状态机、AppRoute 路由
-- **持久化**:SwiftData 存档、战斗历史
-- **平台适配**:通过 `#if os()` 处理 macOS/visionOS 差异
-
-## 依赖关系
-
-```
-SaluCRH → GameCore ✅
-GameCore → SaluCRH ❌(禁止反向依赖)
-GameCLI ↔ SaluCRH ❌(互不依赖)
-```
-
-**核心原则**:所有游戏逻辑来自 `GameCore`,本模块只负责 UI 展示和用户交互。
-
-## 平台支持
-
-- **macOS** 14.0+(当前支持 ✅)
-- **visionOS** 2.0+(配置中)
-
-使用条件编译处理平台差异:
-
-```swift
-#if os(visionOS)
-import RealityKit
-// visionOS 特有代码(如 ImmersiveSpace)
-#elseif os(macOS)
-// macOS 特有代码
-#endif
-```
-
-## 目录结构
-
-```
-SaluCRH/
-├── SaluCRHApp.swift # @main 入口
-├── ContentView.swift # 根视图(根据 AppRoute 切换)
-├── GameSession.swift # 流程状态机
-├── AppRoute.swift # 路由枚举
-├── Views/ # SwiftUI 视图
-│ ├── MainMenuView.swift
-│ ├── MapView.swift
-│ ├── BattleView.swift
-│ ├── ShopView.swift
-│ ├── EventView.swift
-│ └── ...
-├── ViewModels/ # 视图模型(桥接 GameCore)
-│ └── BattleSession.swift # 包装 BattleEngine
-├── Persistence/ # SwiftData 模型
-│ ├── RunSaveEntity.swift
-│ └── BattleRecordEntity.swift
-├── Platform/ # 平台特有代码
-│ └── visionOS/
-│ └── ImmersiveView.swift
-└── Assets.xcassets
-```
-
-## 核心类型
-
-### AppRoute(路由枚举)
-
-```swift
-enum AppRoute {
- case mainMenu
- case runMap(runState: RunState)
- case battle(BattleSession)
- case shop(ShopInventory)
- case event(EventOffer)
- case rest
- case result(won: Bool)
- case history
- case settings
-}
-```
-
-### GameSession(状态机)
-
-```swift
-@Observable
-class GameSession {
- var route: AppRoute = .mainMenu
- var runState: RunState?
-
- func startNewGame(seed: UInt64?) { ... }
- func continueGame() { ... }
- func enterNode(_ node: MapNode) { ... }
-}
-```
-
-### BattleSession(战斗桥接)
-
-包装 `BattleEngine`,将其转换为 SwiftUI 可观察状态:
-
-```swift
-@Observable
-class BattleSession {
- private let engine: BattleEngine
- var state: BattleState { engine.state }
- var events: [BattleEvent] { engine.events }
-
- func playCard(handIndex: Int, targetIndex: Int?) { ... }
- func endTurn() { ... }
-}
-```
-
-## GameCore 集成
-
-### 导入与使用
-
-```swift
-import GameCore
-
-// 访问卡牌注册表
-let strike = CardRegistry.require("strike")
-
-// 创建冒险状态
-let runState = RunState(seed: 12345)
-
-// 创建战斗引擎
-let engine = BattleEngine(...)
-engine.startBattle()
-```
-
-### 关键 GameCore 类型
-
-| 类型 | 用途 |
-|------|------|
-| `RunState` | 冒险全局状态 |
-| `BattleEngine` | 战斗引擎 |
-| `BattleState` | 战斗快照 |
-| `MapNode` | 地图节点 |
-| `Card` | 卡牌实例 |
-| `Entity` | 玩家/敌人实体 |
-| `RunSnapshot` | 存档格式 |
-| `BattleRecord` | 战斗记录 |
-
-### 种子与可复现性
-
-所有随机内容必须通过 `GameCore` 的 seed 派生机制生成,UI 层不引入额外随机源:
-
-```swift
-// ✅ 正确:使用 GameCore 的生成器
-let rewards = RewardGenerator.generate(context: rewardContext)
-let shop = ShopInventory.generate(context: shopContext)
-let event = EventGenerator.generate(context: eventContext)
-
-// ❌ 错误:UI 层使用系统随机
-let randomCard = deck.randomElement() // 禁止
-```
-
-## SwiftData 持久化
-
-### 模型设计
-
-采用 "索引字段 + JSON Blob" 策略:
-
-```swift
-@Model
-final class RunSaveEntity {
- var id: UUID
- var updatedAt: Date
- var seed: UInt64
- var floor: Int
- var isOver: Bool
- var won: Bool
- var snapshotJSON: Data // JSONEncoder(RunSnapshot)
-}
-```
-
-### 转换逻辑
-
-参考 CLI 的 `SaveService`,实现 `RunSnapshot ↔ RunState` 转换。
-
-## UI 规范
-
-### 交互约定
-
-- 选卡 →(若需要)选目标 → 出牌
-- 战斗结束自动进入奖励/返回地图
-- 必须显示:seed、floor、当前节点类型
-
-### 平台差异
-
-```swift
-var body: some View {
- #if os(visionOS)
- // visionOS: 更大的点击目标
- CardView()
- .frame(width: 200, height: 300)
- #else
- // macOS: 更紧凑的布局
- CardView()
- .frame(width: 120, height: 180)
- #endif
-}
-```
-
-## 构建与验证
-
-验证原则:只改 `SaluNative/`(SwiftUI/SwiftData/UI)时,至少跑一次 `xcodebuild ... build`;无需强制跑 `swift test`(除非同时改了 `Sources/**/*.swift` 或 `Package.swift`)。
-
-```bash
-# macOS
-xcodebuild -project SaluNative/SaluNative.xcodeproj \
- -scheme SaluCRH \
- -destination 'platform=macOS' \
- build
-
-# visionOS (配置 Supported Destinations 后)
-xcodebuild -project SaluNative/SaluNative.xcodeproj \
- -scheme SaluCRH \
- -destination 'platform=visionOS Simulator,name=Apple Vision Pro' \
- build
-```
-
-## 构建失败排查指南
-
-**重要**:编译错误通常是因为 GameCore API 使用不正确。遇到构建失败时:
-
-1. **查看 GameCore 源码**:错误提示的类型(如 `RunState`、`Entity`、`MapNode` 等)都定义在 `Sources/GameCore/` 中
-2. **常见 API 差异**:
- - `RunState.newRun(seed:)` - 创建新冒险(不是直接 `init(seed:)`)
- - `Entity.currentHP` / `Entity.maxHP` - 注意大小写
- - `MapNode.roomType` - 不是 `type`
- - `RelicManager.all` - 获取所有遗物 ID(不是 `relics`)
- - `ConsumableID` 是 ID 类型,通过 `ConsumableRegistry.require()` 获取定义
-3. **技术文档**:完整的 GameCore API 参考见 [GameCore 规范](../../Sources/GameCore/AGENTS.md)
-## 禁止事项
-
-- ❌ 在 View 中直接操作 `BattleEngine`(必须通过 `BattleSession`)
-- ❌ 引入 UI 层随机源(所有随机通过 GameCore seed 派生)
-- ❌ 依赖 `GameCLI`(两者互不依赖)
-- ❌ 修改 `GameCore` 来适配 UI(GameCore 保持纯逻辑)
-- ❌ 使用单例 ViewModel(使用依赖注入)
-- ❌ 猜测 API - 遇到不确定的类型/属性,先查看 GameCore 源码
-
-## 参考文档
-- [总体方案](<../../.giithub/plans/visionOS + macOS GUI 原生实现方案(SwiftUI).md>)
-- [GameCore 规范](../../Sources/GameCore/AGENTS.md)
diff --git a/SaluNative/SaluCRH/AppRoute.swift b/SaluNative/SaluCRH/AppRoute.swift
deleted file mode 100644
index f6f64a9..0000000
--- a/SaluNative/SaluCRH/AppRoute.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-import Foundation
-import GameCore
-
-/// 应用路由枚举 - 表示当前应用所处的页面/状态
-enum AppRoute: Equatable {
- /// 主菜单
- case mainMenu
-
- /// 地图界面(冒险中)
- case runMap
-
- /// 战斗界面(普通/精英/Boss)
- case battle
-
- /// 商店界面
- case shop
-
- /// 事件界面
- case event
-
- /// 休息点界面
- case rest
-
- /// 卡牌奖励选择界面
- case cardReward
-
- /// 遗物奖励选择界面
- case relicReward
-
- /// 冒险结果界面(胜利/失败)
- case runResult(won: Bool)
-
- /// 历史记录界面
- case history
-
- /// 统计界面
- case statistics
-
- /// 设置界面
- case settings
-}
diff --git a/SaluNative/SaluCRH/Assets.xcassets/AppIcon.appiconset/Contents.json b/SaluNative/SaluCRH/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 3f00db4..0000000
--- a/SaluNative/SaluCRH/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,58 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "16x16"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "16x16"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "32x32"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "32x32"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "128x128"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "128x128"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "256x256"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "256x256"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "512x512"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "512x512"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/SaluNative/SaluCRH/ContentView.swift b/SaluNative/SaluCRH/ContentView.swift
deleted file mode 100644
index dc095af..0000000
--- a/SaluNative/SaluCRH/ContentView.swift
+++ /dev/null
@@ -1,170 +0,0 @@
-import SwiftUI
-import GameCore
-
-/// 根视图 - 根据 AppRoute 切换不同界面
-struct ContentView: View {
- @State private var session = GameSession()
-
- var body: some View {
- Group {
- switch session.route {
- case .mainMenu:
- MainMenuView()
-
- case .runMap:
- MapView()
-
- case .battle:
- // TODO: P5 - 战斗界面
- PlaceholderView(title: "战斗", icon: "bolt.fill")
-
- case .shop:
- // TODO: P7 - 商店界面
- PlaceholderView(title: "商店", icon: "cart.fill")
-
- case .event:
- // TODO: P6 - 事件界面
- PlaceholderView(title: "事件", icon: "questionmark.circle.fill")
-
- case .rest:
- // TODO: P4 - 休息点界面
- PlaceholderView(title: "休息点", icon: "bed.double.fill")
-
- case .cardReward:
- // TODO: P5 - 卡牌奖励界面
- PlaceholderView(title: "选择卡牌", icon: "rectangle.stack.fill")
-
- case .relicReward:
- // TODO: P5 - 遗物奖励界面
- PlaceholderView(title: "选择遗物", icon: "sparkles")
-
- case .runResult(let won):
- RunResultView(won: won)
-
- case .history:
- // TODO: P3 - 历史记录界面
- PlaceholderView(title: "战斗历史", icon: "clock.fill")
-
- case .statistics:
- // TODO: P3 - 统计界面
- PlaceholderView(title: "统计数据", icon: "chart.bar.fill")
-
- case .settings:
- SettingsView()
- }
- }
- .environment(session)
- }
-}
-
-// MARK: - 占位视图
-
-/// 占位视图 - 用于尚未实现的界面
-struct PlaceholderView: View {
- let title: String
- let icon: String
-
- @Environment(GameSession.self) private var session
-
- var body: some View {
- VStack(spacing: 24) {
- Image(systemName: icon)
- .font(.system(size: 64))
- .foregroundStyle(.secondary)
-
- Text(title)
- .font(.largeTitle)
- .fontWeight(.bold)
-
- Text("功能开发中...")
- .foregroundStyle(.secondary)
-
- Button("返回地图") {
- session.route = .runMap
- }
- .buttonStyle(.bordered)
- }
- .padding()
- }
-}
-
-// MARK: - 冒险结果视图
-
-/// 冒险结果视图
-struct RunResultView: View {
- let won: Bool
-
- @Environment(GameSession.self) private var session
-
- var body: some View {
- VStack(spacing: 32) {
- // 图标
- Image(systemName: won ? "trophy.fill" : "xmark.circle.fill")
- .font(.system(size: 80))
- .foregroundStyle(won ? .yellow : .red)
-
- // 标题
- Text(won ? "胜利!" : "失败")
- .font(.system(size: 48, weight: .bold))
-
- // 描述
- if won {
- Text("恭喜你完成了冒险!")
- .font(.title2)
- .foregroundStyle(.secondary)
- } else {
- Text("你的冒险到此结束...")
- .font(.title2)
- .foregroundStyle(.secondary)
- }
-
- // 冒险信息
- if let run = session.runState {
- VStack(alignment: .leading, spacing: 8) {
- Text("种子: \(run.seed)")
- Text("层数: \(run.floor)/\(run.maxFloor)")
- Text("金币: \(run.gold)")
- }
- .font(.body.monospaced())
- .padding()
- .background(.secondary.opacity(0.1))
- .cornerRadius(8)
- }
-
- // 返回主菜单按钮
- Button("返回主菜单") {
- session.abandonRun()
- }
- .buttonStyle(.borderedProminent)
- }
- .padding(48)
- }
-}
-
-// MARK: - 设置视图
-
-/// 设置视图
-struct SettingsView: View {
- @Environment(GameSession.self) private var session
-
- var body: some View {
- VStack(spacing: 24) {
- Text("设置")
- .font(.largeTitle)
- .fontWeight(.bold)
-
- Text("设置功能开发中...")
- .foregroundStyle(.secondary)
-
- Button("返回") {
- session.navigateToMainMenu()
- }
- .buttonStyle(.bordered)
- }
- .padding()
- }
-}
-
-#Preview {
- ContentView()
-}
diff --git a/SaluNative/SaluCRH/GameSession.swift b/SaluNative/SaluCRH/GameSession.swift
deleted file mode 100644
index 82c065c..0000000
--- a/SaluNative/SaluCRH/GameSession.swift
+++ /dev/null
@@ -1,111 +0,0 @@
-import Foundation
-import GameCore
-import Observation
-
-/// 游戏会话 - 管理应用状态机和冒险状态
-@Observable
-final class GameSession {
-
- // MARK: - 状态
-
- /// 当前路由
- var route: AppRoute = .mainMenu
-
- /// 当前冒险状态(nil 表示没有进行中的冒险)
- var runState: RunState?
-
- /// 是否有存档可以继续
- var hasSavedRun: Bool {
- // TODO: P3 - 从 SwiftData 检查是否有存档
- false
- }
-
- // MARK: - 主菜单操作
-
- /// 开始新游戏
- /// - Parameter seed: 可选的随机种子,nil 表示使用随机种子
- func startNewGame(seed: UInt64? = nil) {
- let actualSeed = seed ?? UInt64.random(in: 0...UInt64.max)
- runState = RunState.newRun(seed: actualSeed)
- route = .runMap
- }
-
- /// 继续游戏(从存档加载)
- func continueGame() {
- // TODO: P3 - 从 SwiftData 加载存档
- // 暂时不实现,等 P3 再做
- }
-
- /// 放弃当前冒险
- func abandonRun() {
- runState = nil
- route = .mainMenu
- // TODO: P3 - 清除存档
- }
-
- // MARK: - 地图导航
-
- /// 进入指定节点
- func enterNode(_ node: MapNode) {
- guard runState != nil else { return }
- _ = runState?.enterNode(node.id)
-
- // 根据节点类型切换到对应界面
- switch node.roomType {
- case .start:
- // 起始节点:显示章节开场文本后自动完成
- completeCurrentNode()
-
- case .battle:
- route = .battle
-
- case .elite:
- route = .battle
-
- case .boss:
- route = .battle
-
- case .rest:
- route = .rest
-
- case .shop:
- route = .shop
-
- case .event:
- route = .event
- }
- }
-
- /// 完成当前节点,返回地图
- func completeCurrentNode() {
- guard runState != nil else { return }
- runState?.completeCurrentNode()
-
- // 检查冒险是否结束
- if let run = runState, run.isOver {
- route = .runResult(won: run.won)
- } else {
- route = .runMap
- }
-
- // TODO: P3 - 自动保存存档
- }
-
- // MARK: - 设置/历史等辅助页面
-
- func navigateToSettings() {
- route = .settings
- }
-
- func navigateToHistory() {
- route = .history
- }
-
- func navigateToStatistics() {
- route = .statistics
- }
-
- func navigateToMainMenu() {
- route = .mainMenu
- }
-}
diff --git a/SaluNative/SaluCRH/SaluCRHApp.swift b/SaluNative/SaluCRH/SaluCRHApp.swift
deleted file mode 100644
index abefc7c..0000000
--- a/SaluNative/SaluCRH/SaluCRHApp.swift
+++ /dev/null
@@ -1,10 +0,0 @@
-import SwiftUI
-
-@main
-struct SaluCRHApp: App {
- var body: some Scene {
- WindowGroup {
- ContentView()
- }
- }
-}
diff --git a/SaluNative/SaluCRH/Views/MainMenuView.swift b/SaluNative/SaluCRH/Views/MainMenuView.swift
deleted file mode 100644
index 002183d..0000000
--- a/SaluNative/SaluCRH/Views/MainMenuView.swift
+++ /dev/null
@@ -1,153 +0,0 @@
-import SwiftUI
-
-/// 主菜单视图
-struct MainMenuView: View {
- @Environment(GameSession.self) private var session
-
- /// 是否显示种子输入弹窗
- @State private var showSeedInput = false
-
- /// 输入的种子字符串
- @State private var seedInput = ""
-
- var body: some View {
- VStack(spacing: 32) {
- // 标题
- titleSection
-
- // 菜单按钮
- menuButtons
-
- // 底部信息
- footerInfo
- }
- .padding(48)
- .frame(minWidth: 400, minHeight: 500)
- .alert("输入种子", isPresented: $showSeedInput) {
- TextField("留空使用随机种子", text: $seedInput)
- Button("开始") {
- startWithSeed()
- }
- Button("取消", role: .cancel) {
- seedInput = ""
- }
- } message: {
- Text("输入数字种子以复现特定冒险")
- }
- }
-
- // MARK: - 子视图
-
- private var titleSection: some View {
- VStack(spacing: 16) {
- Image(systemName: "flame.fill")
- .font(.system(size: 80))
- .foregroundStyle(.orange)
-
- Text("Salu")
- .font(.system(size: 48, weight: .bold))
-
- Text("the Fire")
- .font(.title2)
- .foregroundStyle(.secondary)
- }
- }
-
- private var menuButtons: some View {
- VStack(spacing: 16) {
- // 新游戏按钮
- Button {
- session.startNewGame()
- } label: {
- menuButtonLabel("新游戏", icon: "play.fill")
- }
- .buttonStyle(.borderedProminent)
-
- // 新游戏(指定种子)
- Button {
- showSeedInput = true
- } label: {
- menuButtonLabel("新游戏(指定种子)", icon: "number")
- }
- .buttonStyle(.bordered)
-
- // 继续游戏
- Button {
- session.continueGame()
- } label: {
- menuButtonLabel("继续游戏", icon: "arrow.right.circle.fill")
- }
- .buttonStyle(.bordered)
- .disabled(!session.hasSavedRun)
-
- Divider()
- .padding(.vertical, 8)
-
- // 历史记录
- Button {
- session.navigateToHistory()
- } label: {
- menuButtonLabel("战斗历史", icon: "clock.fill")
- }
- .buttonStyle(.bordered)
-
- // 统计
- Button {
- session.navigateToStatistics()
- } label: {
- menuButtonLabel("统计数据", icon: "chart.bar.fill")
- }
- .buttonStyle(.bordered)
-
- // 设置
- Button {
- session.navigateToSettings()
- } label: {
- menuButtonLabel("设置", icon: "gear")
- }
- .buttonStyle(.bordered)
- }
- }
-
- private var footerInfo: some View {
- VStack(spacing: 4) {
- Text("版本 0.1.0")
- .font(.caption)
- .foregroundStyle(.secondary)
-
- #if DEBUG
- Text("DEBUG 模式")
- .font(.caption2)
- .foregroundStyle(.orange)
- #endif
- }
- }
-
- // MARK: - 辅助方法
-
- private func menuButtonLabel(_ title: String, icon: String) -> some View {
- HStack {
- Image(systemName: icon)
- .frame(width: 24)
- Text(title)
- }
- .frame(width: 200)
- }
-
- private func startWithSeed() {
- if seedInput.isEmpty {
- session.startNewGame()
- } else if let seed = UInt64(seedInput) {
- session.startNewGame(seed: seed)
- } else {
- // 无效输入,使用随机种子
- session.startNewGame()
- }
- seedInput = ""
- }
-}
-
-#Preview {
- MainMenuView()
- .environment(GameSession())
-}
diff --git a/SaluNative/SaluCRH/Views/MapView.swift b/SaluNative/SaluCRH/Views/MapView.swift
deleted file mode 100644
index f539ccc..0000000
--- a/SaluNative/SaluCRH/Views/MapView.swift
+++ /dev/null
@@ -1,249 +0,0 @@
-import SwiftUI
-import GameCore
-
-/// 地图视图 - 显示冒险地图和玩家状态
-struct MapView: View {
- @Environment(GameSession.self) private var session
-
- private var language: GameLanguage { .zhHans }
-
- var body: some View {
- if let run = session.runState {
- HStack(spacing: 0) {
- // 左侧:玩家状态栏
- playerStatusBar(run: run)
-
- Divider()
-
- // 中间:地图
- mapContent(run: run)
-
- Divider()
-
- // 右侧:资源信息
- resourceBar(run: run)
- }
- } else {
- // 没有进行中的冒险
- VStack {
- Text("没有进行中的冒险")
- Button("返回主菜单") {
- session.navigateToMainMenu()
- }
- .buttonStyle(.bordered)
- }
- }
- }
-
- // MARK: - 玩家状态栏
-
- private func playerStatusBar(run: RunState) -> some View {
- VStack(alignment: .leading, spacing: 16) {
- Text("玩家")
- .font(.headline)
-
- // HP
- HStack {
- Image(systemName: "heart.fill")
- .foregroundStyle(.red)
- Text("\(run.player.currentHP)/\(run.player.maxHP)")
- }
-
- // 金币
- HStack {
- Image(systemName: "dollarsign.circle.fill")
- .foregroundStyle(.yellow)
- Text("\(run.gold)")
- }
-
- Divider()
-
- // 遗物
- Text("遗物")
- .font(.headline)
-
- if run.relicManager.all.isEmpty {
- Text("无")
- .foregroundStyle(.secondary)
- } else {
- ForEach(run.relicManager.all, id: \.rawValue) { relicId in
- let def = RelicRegistry.require(relicId)
- Text(def.name.resolved(for: language))
- .font(.caption)
- }
- }
-
- Spacer()
-
- // 放弃冒险按钮
- Button("放弃冒险", role: .destructive) {
- session.abandonRun()
- }
- .buttonStyle(.bordered)
- }
- .padding()
- .frame(width: 180)
- }
-
- // MARK: - 地图内容
-
- private func mapContent(run: RunState) -> some View {
- VStack(spacing: 16) {
- // 标题
- HStack {
- Text("第 \(run.floor) 章")
- .font(.title)
- .fontWeight(.bold)
-
- Spacer()
-
- Text("种子: \(run.seed)")
- .font(.caption.monospaced())
- .foregroundStyle(.secondary)
- }
- .padding(.horizontal)
-
- Divider()
-
- // 地图节点
- ScrollView {
- mapNodes(run: run)
- }
- }
- .frame(minWidth: 400)
- }
-
- private func mapNodes(run: RunState) -> some View {
- VStack(spacing: 8) {
- // 按层级分组显示节点
- let nodesByRow = Dictionary(grouping: run.map) { $0.row }
- let maxRow = nodesByRow.keys.max() ?? 0
-
- ForEach((0...maxRow).reversed(), id: \.self) { row in
- if let nodesInRow = nodesByRow[row] {
- HStack(spacing: 16) {
- ForEach(nodesInRow, id: \.id) { node in
- nodeButton(node: node, run: run)
- }
- }
- .padding(.vertical, 4)
- }
- }
- }
- .padding()
- }
-
- private func nodeButton(node: MapNode, run: RunState) -> some View {
- let isAccessible = run.accessibleNodes.contains { $0.id == node.id }
- let isCurrent = run.currentNodeId == node.id
- let isCompleted = node.isCompleted
-
- return Button {
- if isAccessible {
- session.enterNode(node)
- }
- } label: {
- VStack(spacing: 4) {
- // 节点图标
- Image(systemName: nodeIcon(for: node.roomType))
- .font(.title2)
-
- // 节点类型名称
- Text(nodeTypeName(for: node.roomType))
- .font(.caption)
- }
- .frame(width: 60, height: 60)
- .background(nodeBackground(isAccessible: isAccessible, isCurrent: isCurrent, isCompleted: isCompleted))
- .cornerRadius(8)
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(isCurrent ? Color.blue : Color.clear, lineWidth: 3)
- )
- }
- .buttonStyle(.plain)
- .disabled(!isAccessible)
- .opacity(isCompleted ? 0.5 : 1.0)
- }
-
- // MARK: - 资源栏
-
- private func resourceBar(run: RunState) -> some View {
- VStack(alignment: .leading, spacing: 16) {
- Text("牌组")
- .font(.headline)
-
- Text("\(run.deck.count) 张")
- .font(.body)
-
- Divider()
-
- Text("消耗品")
- .font(.headline)
-
- let consumableCards = run.deck.filter { card in
- guard let def = CardRegistry.get(card.cardId) else { return false }
- return def.type == .consumable
- }
-
- if consumableCards.isEmpty {
- Text("无")
- .foregroundStyle(.secondary)
- } else {
- ForEach(consumableCards, id: \.id) { card in
- let def = CardRegistry.require(card.cardId)
- Text(def.name.resolved(for: language))
- .font(.caption)
- }
- }
-
- Spacer()
- }
- .padding()
- .frame(width: 150)
- }
-
- // MARK: - 辅助方法
-
- private func nodeIcon(for type: RoomType) -> String {
- switch type {
- case .start: return "flag.fill"
- case .battle: return "person.fill"
- case .elite: return "person.2.fill"
- case .boss: return "crown.fill"
- case .rest: return "bed.double.fill"
- case .shop: return "cart.fill"
- case .event: return "questionmark.circle.fill"
- }
- }
-
- private func nodeTypeName(for type: RoomType) -> String {
- switch type {
- case .start: return "起点"
- case .battle: return "战斗"
- case .elite: return "精英"
- case .boss: return "Boss"
- case .rest: return "休息"
- case .shop: return "商店"
- case .event: return "事件"
- }
- }
-
- private func nodeBackground(isAccessible: Bool, isCurrent: Bool, isCompleted: Bool) -> Color {
- if isCompleted {
- return .gray.opacity(0.3)
- } else if isCurrent {
- return .blue.opacity(0.3)
- } else if isAccessible {
- return .green.opacity(0.3)
- } else {
- return .secondary.opacity(0.1)
- }
- }
-}
-
-#Preview {
- let session = GameSession()
- session.startNewGame(seed: 12345)
- return MapView()
- .environment(session)
-}
diff --git a/SaluNative/SaluNative.xcodeproj/project.pbxproj b/SaluNative/SaluNative.xcodeproj/project.pbxproj
index 501c3c1..c63a6cf 100644
--- a/SaluNative/SaluNative.xcodeproj/project.pbxproj
+++ b/SaluNative/SaluNative.xcodeproj/project.pbxproj
@@ -7,37 +7,60 @@
objects = {
/* Begin PBXBuildFile section */
- D8A515242F16C8AA005EBB40 /* GameCore in Frameworks */ = {isa = PBXBuildFile; productRef = D8A515232F16C8AA005EBB40 /* GameCore */; };
+ D84785632F2AE7AF00B5018E /* RealityKitContent in Frameworks */ = {isa = PBXBuildFile; productRef = D84785622F2AE7AF00B5018E /* RealityKitContent */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
- D8A515172F16C856005EBB40 /* SaluCRH.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SaluCRH.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ D847855E2F2AE7AF00B5018E /* SaluAVP.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SaluAVP.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ D84785612F2AE7AF00B5018E /* RealityKitContent */ = {isa = PBXFileReference; lastKnownFileType = folder; path = RealityKitContent; sourceTree = ""; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ D847856F2F2AE7B000B5018E /* Exceptions for "SaluAVP" folder in "SaluAVP" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = D847855D2F2AE7AF00B5018E /* SaluAVP */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
/* Begin PBXFileSystemSynchronizedRootGroup section */
- D8A515182F16C856005EBB40 /* SaluCRH */ = {
+ D847855F2F2AE7AF00B5018E /* SaluAVP */ = {
isa = PBXFileSystemSynchronizedRootGroup;
- path = SaluCRH;
+ exceptions = (
+ D847856F2F2AE7B000B5018E /* Exceptions for "SaluAVP" folder in "SaluAVP" target */,
+ );
+ path = SaluAVP;
sourceTree = "";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
- D8A515142F16C856005EBB40 /* Frameworks */ = {
+ D847855B2F2AE7AF00B5018E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- D8A515242F16C8AA005EBB40 /* GameCore in Frameworks */,
+ D84785632F2AE7AF00B5018E /* RealityKitContent in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ D84785602F2AE7AF00B5018E /* Packages */ = {
+ isa = PBXGroup;
+ children = (
+ D84785612F2AE7AF00B5018E /* RealityKitContent */,
+ );
+ path = Packages;
+ sourceTree = "";
+ };
D8A514962F16BA92005EBB40 = {
isa = PBXGroup;
children = (
- D8A515182F16C856005EBB40 /* SaluCRH */,
+ D847855F2F2AE7AF00B5018E /* SaluAVP */,
+ D84785602F2AE7AF00B5018E /* Packages */,
D8A514A02F16BA92005EBB40 /* Products */,
);
sourceTree = "";
@@ -45,7 +68,7 @@
D8A514A02F16BA92005EBB40 /* Products */ = {
isa = PBXGroup;
children = (
- D8A515172F16C856005EBB40 /* SaluCRH.app */,
+ D847855E2F2AE7AF00B5018E /* SaluAVP.app */,
);
name = Products;
sourceTree = "";
@@ -53,27 +76,27 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- D8A515162F16C856005EBB40 /* SaluCRH */ = {
+ D847855D2F2AE7AF00B5018E /* SaluAVP */ = {
isa = PBXNativeTarget;
- buildConfigurationList = D8A5151F2F16C857005EBB40 /* Build configuration list for PBXNativeTarget "SaluCRH" */;
+ buildConfigurationList = D84785702F2AE7B000B5018E /* Build configuration list for PBXNativeTarget "SaluAVP" */;
buildPhases = (
- D8A515132F16C856005EBB40 /* Sources */,
- D8A515142F16C856005EBB40 /* Frameworks */,
- D8A515152F16C856005EBB40 /* Resources */,
+ D847855A2F2AE7AF00B5018E /* Sources */,
+ D847855B2F2AE7AF00B5018E /* Frameworks */,
+ D847855C2F2AE7AF00B5018E /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
- D8A515182F16C856005EBB40 /* SaluCRH */,
+ D847855F2F2AE7AF00B5018E /* SaluAVP */,
);
- name = SaluCRH;
+ name = SaluAVP;
packageProductDependencies = (
- D8A515232F16C8AA005EBB40 /* GameCore */,
+ D84785622F2AE7AF00B5018E /* RealityKitContent */,
);
- productName = SaluCRH;
- productReference = D8A515172F16C856005EBB40 /* SaluCRH.app */;
+ productName = SaluAVP;
+ productReference = D847855E2F2AE7AF00B5018E /* SaluAVP.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -86,7 +109,7 @@
LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 2600;
TargetAttributes = {
- D8A515162F16C856005EBB40 = {
+ D847855D2F2AE7AF00B5018E = {
CreatedOnToolsVersion = 26.0;
};
};
@@ -108,13 +131,13 @@
projectDirPath = "";
projectRoot = "";
targets = (
- D8A515162F16C856005EBB40 /* SaluCRH */,
+ D847855D2F2AE7AF00B5018E /* SaluAVP */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
- D8A515152F16C856005EBB40 /* Resources */ = {
+ D847855C2F2AE7AF00B5018E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -124,7 +147,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
- D8A515132F16C856005EBB40 /* Sources */ = {
+ D847855A2F2AE7AF00B5018E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -134,6 +157,69 @@
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
+ D847856D2F2AE7B000B5018E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = RDQHYSDFFG;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "$(TARGET_NAME)/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.chiimagnus.SaluAVP;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = xros;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SUPPORTED_PLATFORMS = "xros xrsimulator";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 7;
+ XROS_DEPLOYMENT_TARGET = 26.0;
+ };
+ name = Debug;
+ };
+ D847856E2F2AE7B000B5018E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = RDQHYSDFFG;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "$(TARGET_NAME)/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.chiimagnus.SaluAVP;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = xros;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SUPPORTED_PLATFORMS = "xros xrsimulator";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 7;
+ VALIDATE_PRODUCT = YES;
+ XROS_DEPLOYMENT_TARGET = 26.0;
+ };
+ name = Release;
+ };
D8A514AA2F16BA93005EBB40 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -254,101 +340,23 @@
};
name = Release;
};
- D8A515202F16C857005EBB40 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_STYLE = Automatic;
- COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = RDQHYSDFFG;
- ENABLE_APP_SANDBOX = YES;
- ENABLE_HARDENED_RUNTIME = YES;
- ENABLE_PREVIEWS = YES;
- ENABLE_USER_SELECTED_FILES = readonly;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_KEY_CFBundleDisplayName = "Salu the Fire";
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.card-games";
- INFOPLIST_KEY_NSHumanReadableCopyright = "";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/../Frameworks",
- );
- MACOSX_DEPLOYMENT_TARGET = 14.6;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.chiimagnus.SaluCRH;
- PRODUCT_NAME = "$(TARGET_NAME)";
- REGISTER_APP_GROUPS = YES;
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
- SUPPORTED_PLATFORMS = "macosx xros xrsimulator";
- SUPPORTS_MACCATALYST = NO;
- SWIFT_APPROACHABLE_CONCURRENCY = YES;
- SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = 7;
- XROS_DEPLOYMENT_TARGET = 26.0;
- };
- name = Debug;
- };
- D8A515212F16C857005EBB40 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_STYLE = Automatic;
- COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = RDQHYSDFFG;
- ENABLE_APP_SANDBOX = YES;
- ENABLE_HARDENED_RUNTIME = YES;
- ENABLE_PREVIEWS = YES;
- ENABLE_USER_SELECTED_FILES = readonly;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_KEY_CFBundleDisplayName = "Salu the Fire";
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.card-games";
- INFOPLIST_KEY_NSHumanReadableCopyright = "";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/../Frameworks",
- );
- MACOSX_DEPLOYMENT_TARGET = 14.6;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.chiimagnus.SaluCRH;
- PRODUCT_NAME = "$(TARGET_NAME)";
- REGISTER_APP_GROUPS = YES;
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
- SUPPORTED_PLATFORMS = "macosx xros xrsimulator";
- SUPPORTS_MACCATALYST = NO;
- SWIFT_APPROACHABLE_CONCURRENCY = YES;
- SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = 7;
- XROS_DEPLOYMENT_TARGET = 26.0;
- };
- name = Release;
- };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
- D8A5149A2F16BA92005EBB40 /* Build configuration list for PBXProject "SaluNative" */ = {
+ D84785702F2AE7B000B5018E /* Build configuration list for PBXNativeTarget "SaluAVP" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- D8A514AA2F16BA93005EBB40 /* Debug */,
- D8A514AB2F16BA93005EBB40 /* Release */,
+ D847856D2F2AE7B000B5018E /* Debug */,
+ D847856E2F2AE7B000B5018E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- D8A5151F2F16C857005EBB40 /* Build configuration list for PBXNativeTarget "SaluCRH" */ = {
+ D8A5149A2F16BA92005EBB40 /* Build configuration list for PBXProject "SaluNative" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- D8A515202F16C857005EBB40 /* Debug */,
- D8A515212F16C857005EBB40 /* Release */,
+ D8A514AA2F16BA93005EBB40 /* Debug */,
+ D8A514AB2F16BA93005EBB40 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
@@ -363,10 +371,9 @@
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- D8A515232F16C8AA005EBB40 /* GameCore */ = {
+ D84785622F2AE7AF00B5018E /* RealityKitContent */ = {
isa = XCSwiftPackageProductDependency;
- package = D8A5150B2F16C179005EBB40 /* XCLocalSwiftPackageReference "../../salu" */;
- productName = GameCore;
+ productName = RealityKitContent;
};
/* End XCSwiftPackageProductDependency section */
};
diff --git a/Sources/GameCLI/AGENTS.md b/Sources/GameCLI/AGENTS.md
index a1c4ae1..fdb03a5 100644
--- a/Sources/GameCLI/AGENTS.md
+++ b/Sources/GameCLI/AGENTS.md
@@ -1,6 +1,6 @@
# GameCLI 模块开发规范
-> 设定/剧情/玩法规则文档优先对齐 `.giithub/docs/`。
+> 设定/剧情/玩法规则文档优先对齐 `.github/docs/`。
## 模块定位
diff --git a/Sources/GameCore/AGENTS.md b/Sources/GameCore/AGENTS.md
index aa5cefc..10759fb 100644
--- a/Sources/GameCore/AGENTS.md
+++ b/Sources/GameCore/AGENTS.md
@@ -1,6 +1,6 @@
# GameCore 模块开发规范
-> 设定/剧情/玩法规则文档优先对齐 `.giithub/docs/`。
+> 设定/剧情/玩法规则文档优先对齐 `.github/docs/`。
## 模块定位
diff --git a/Sources/GameCore/Run/ChapterText.swift b/Sources/GameCore/Run/ChapterText.swift
index c637465..a13b473 100644
--- a/Sources/GameCore/Run/ChapterText.swift
+++ b/Sources/GameCore/Run/ChapterText.swift
@@ -2,7 +2,7 @@
//
// v1.0 叙事:章节收束文本和结局文本
// 每章 Boss 战胜利后显示对应的章节文本
-// 对照 .giithub/docs/Salu游戏设定与剧情v1.0.md
+// 对照 .github/docs/Salu游戏设定与剧情v1.0.md
/// 章节文本:定义各章节的收束文本和最终结局文本
public enum ChapterText {