From 8248e31eaf4e40ceff9bf810f4a4e7290fe4956c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:56:56 +0800 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0AVP=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E5=BA=94=E7=94=A8=E7=BB=93=E6=9E=84=E5=92=8C=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=EF=BC=8C=E6=94=AF=E6=8C=81=E6=B2=89=E6=B5=B8=E5=BC=8F?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SaluNative/SaluAVP/AppModel.swift | 21 +++ .../AccentColor.colorset/Contents.json | 11 ++ .../Content.imageset/Contents.json | 12 ++ .../Back.solidimagestacklayer/Contents.json | 6 + .../AppIcon.solidimagestack/Contents.json | 17 ++ .../Content.imageset/Contents.json | 12 ++ .../Front.solidimagestacklayer/Contents.json | 6 + .../Content.imageset/Contents.json | 12 ++ .../Middle.solidimagestacklayer/Contents.json | 6 + .../SaluAVP/Assets.xcassets/Contents.json | 6 + SaluNative/SaluAVP/ContentView.swift | 53 ++++++ SaluNative/SaluAVP/ImmersiveView.swift | 30 ++++ SaluNative/SaluAVP/Info.plist | 23 +++ SaluNative/SaluAVP/SaluAVPApp.swift | 34 ++++ .../SaluAVP/ToggleImmersiveSpaceButton.swift | 58 +++++++ .../SaluNative.xcodeproj/project.pbxproj | 157 ++++++++++++++++++ 16 files changed, 464 insertions(+) create mode 100644 SaluNative/SaluAVP/AppModel.swift create mode 100644 SaluNative/SaluAVP/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json create mode 100644 SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json create mode 100644 SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Contents.json create mode 100644 SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json create mode 100644 SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json create mode 100644 SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json create mode 100644 SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json create mode 100644 SaluNative/SaluAVP/Assets.xcassets/Contents.json create mode 100644 SaluNative/SaluAVP/ContentView.swift create mode 100644 SaluNative/SaluAVP/ImmersiveView.swift create mode 100644 SaluNative/SaluAVP/Info.plist create mode 100644 SaluNative/SaluAVP/SaluAVPApp.swift create mode 100644 SaluNative/SaluAVP/ToggleImmersiveSpaceButton.swift 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/SaluAVP/Assets.xcassets/AccentColor.colorset/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/SaluNative/SaluAVP/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SaluNative/SaluAVP/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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/SaluNative.xcodeproj/project.pbxproj b/SaluNative/SaluNative.xcodeproj/project.pbxproj index 501c3c1..fe806f1 100644 --- a/SaluNative/SaluNative.xcodeproj/project.pbxproj +++ b/SaluNative/SaluNative.xcodeproj/project.pbxproj @@ -7,14 +7,35 @@ objects = { /* Begin PBXBuildFile section */ + D84785632F2AE7AF00B5018E /* RealityKitContent in Frameworks */ = {isa = PBXBuildFile; productRef = D84785622F2AE7AF00B5018E /* RealityKitContent */; }; D8A515242F16C8AA005EBB40 /* GameCore in Frameworks */ = {isa = PBXBuildFile; productRef = D8A515232F16C8AA005EBB40 /* GameCore */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 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 = ""; }; D8A515172F16C856005EBB40 /* SaluCRH.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SaluCRH.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* 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 */ + D847855F2F2AE7AF00B5018E /* SaluAVP */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D847856F2F2AE7B000B5018E /* Exceptions for "SaluAVP" folder in "SaluAVP" target */, + ); + path = SaluAVP; + sourceTree = ""; + }; D8A515182F16C856005EBB40 /* SaluCRH */ = { isa = PBXFileSystemSynchronizedRootGroup; path = SaluCRH; @@ -23,6 +44,14 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + D847855B2F2AE7AF00B5018E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D84785632F2AE7AF00B5018E /* RealityKitContent in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D8A515142F16C856005EBB40 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -34,10 +63,20 @@ /* 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 = ""; @@ -46,6 +85,7 @@ isa = PBXGroup; children = ( D8A515172F16C856005EBB40 /* SaluCRH.app */, + D847855E2F2AE7AF00B5018E /* SaluAVP.app */, ); name = Products; sourceTree = ""; @@ -53,6 +93,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + D847855D2F2AE7AF00B5018E /* SaluAVP */ = { + isa = PBXNativeTarget; + buildConfigurationList = D84785702F2AE7B000B5018E /* Build configuration list for PBXNativeTarget "SaluAVP" */; + buildPhases = ( + D847855A2F2AE7AF00B5018E /* Sources */, + D847855B2F2AE7AF00B5018E /* Frameworks */, + D847855C2F2AE7AF00B5018E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + D847855F2F2AE7AF00B5018E /* SaluAVP */, + ); + name = SaluAVP; + packageProductDependencies = ( + D84785622F2AE7AF00B5018E /* RealityKitContent */, + ); + productName = SaluAVP; + productReference = D847855E2F2AE7AF00B5018E /* SaluAVP.app */; + productType = "com.apple.product-type.application"; + }; D8A515162F16C856005EBB40 /* SaluCRH */ = { isa = PBXNativeTarget; buildConfigurationList = D8A5151F2F16C857005EBB40 /* Build configuration list for PBXNativeTarget "SaluCRH" */; @@ -86,6 +149,9 @@ LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 2600; TargetAttributes = { + D847855D2F2AE7AF00B5018E = { + CreatedOnToolsVersion = 26.0; + }; D8A515162F16C856005EBB40 = { CreatedOnToolsVersion = 26.0; }; @@ -109,11 +175,19 @@ projectRoot = ""; targets = ( D8A515162F16C856005EBB40 /* SaluCRH */, + D847855D2F2AE7AF00B5018E /* SaluAVP */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + D847855C2F2AE7AF00B5018E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D8A515152F16C856005EBB40 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -124,6 +198,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + D847855A2F2AE7AF00B5018E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D8A515132F16C856005EBB40 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -134,6 +215,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 = { @@ -335,6 +479,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + D84785702F2AE7B000B5018E /* Build configuration list for PBXNativeTarget "SaluAVP" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D847856D2F2AE7B000B5018E /* Debug */, + D847856E2F2AE7B000B5018E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D8A5149A2F16BA92005EBB40 /* Build configuration list for PBXProject "SaluNative" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -363,6 +516,10 @@ /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + D84785622F2AE7AF00B5018E /* RealityKitContent */ = { + isa = XCSwiftPackageProductDependency; + productName = RealityKitContent; + }; D8A515232F16C8AA005EBB40 /* GameCore */ = { isa = XCSwiftPackageProductDependency; package = D8A5150B2F16C179005EBB40 /* XCLocalSwiftPackageReference "../../salu" */; From 177656ee89ed0c86c9dce1f28625b006e997cb28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:07:01 +0800 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=9A=84=E5=B9=B3=E5=8F=B0=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E8=AE=BE=E5=A4=87=E7=B3=BB?= =?UTF-8?q?=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SaluNative/SaluNative.xcodeproj/project.pbxproj | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/SaluNative/SaluNative.xcodeproj/project.pbxproj b/SaluNative/SaluNative.xcodeproj/project.pbxproj index fe806f1..1f6fa6c 100644 --- a/SaluNative/SaluNative.xcodeproj/project.pbxproj +++ b/SaluNative/SaluNative.xcodeproj/project.pbxproj @@ -425,14 +425,13 @@ PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = macosx; 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; @@ -464,14 +463,13 @@ PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = macosx; 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; From d07a6c4582376c275c94349334354b2ea1557694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:12:13 +0800 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0SaluAVP?= =?UTF-8?q?=E5=92=8CSaluCRH=E7=9A=84=E5=AE=9E=E7=8E=B0=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=8C=E6=8B=86=E5=88=86=E5=8E=9F=E6=9C=89?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...216\260\357\274\210SaluAVP\357\274\211.md" | 133 +++++ ...216\260\357\274\210SaluCRH\357\274\211.md" | 35 ++ ...241\210\357\274\210SwiftUI\357\274\211.md" | 495 ------------------ 3 files changed, 168 insertions(+), 495 deletions(-) create mode 100644 ".github/plans/Plan AVP - Apple Vision Pro \345\216\237\347\224\237 3D \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md" create mode 100644 ".github/plans/Plan macOS - GUI \345\216\237\347\224\237\345\256\236\347\216\260\357\274\210SaluCRH\357\274\211.md" delete mode 100644 ".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" diff --git "a/.github/plans/Plan AVP - 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/Plan AVP - 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..136aed3 --- /dev/null +++ "b/.github/plans/Plan AVP - 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,133 @@ +--- +title: Plan AVP - 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. 目标与核心决策 + +### 目标 + +- 在本仓库提供一个 **visionOS-only** 的原生 App:`SaluAVP` +- **体验形态**:以 **ImmersiveSpace + RealityKit(原生 3D)** 为主;2D Window 仅作为入口/设置/历史等“控制面板” +- 逻辑与内容 **100% 复用 `GameCore`**,不引入对 `GameCLI` 的依赖 +- 对外验收:能完成完整一局(入口 → 地图推进 → 战斗/事件/商店/休息 → Boss → 结算),并保持 seed 可复现 + +### 核心决策 + +1. **拆分产品线:SaluCRH(macOS)与 SaluAVP(visionOS)** + - AVP 的主玩法呈现与输入模型(沉浸式 3D)与 macOS 窗口化长期分叉,拆 Target 能减少 `#if os()` 污染与耦合 + +2. **RealityKit 必选,但只存在于 `SaluNative/`** + - 禁止把任何 Apple-only 代码(SwiftUI/SwiftData/RealityKit)放入 SwiftPM 的 `Sources/`,避免影响 Linux/Windows 构建 + +3. **共享层只共享“状态/桥接”,不共享“渲染实现”** + - 共享:`GameSession` / 路由状态机 / 与 `GameCore` 的桥接 ViewModels(例如 `BattleSession`) + - 不共享:3D 场景构建、实体/材质/动画、沉浸输入手势(全部放在 `SaluAVP` 内部) + +### 验收标准 + +- Xcode:`SaluAVP` 能在 visionOS Simulator 上编译并运行 +- 行为:同 seed + 同选择路径,战斗/地图/奖励/事件/商店结果可复现(允许 3D 表现差异) +- 3D:核心流程至少有一个可交互 ImmersiveSpace(地图/战斗二选一先打通) + +### 验证命令参考 + +```bash +# visionOS(SaluAVP) +xcodebuild -project SaluNative/SaluNative.xcodeproj \ + -scheme SaluAVP \ + -destination 'platform=visionOS Simulator,name=Apple Vision Pro' \ + build +``` + +--- + +## 1. 关键接口(与 AVP 直接相关) + +> 注意:以下接口以 `Sources/GameCore/` 为准,UI 不猜测 API。 + +### 1.1 `RunState`(冒险全局状态) + +- 冒险状态包含:玩家 `Entity`、牌组 `[Card]`、金币 `gold`、遗物 `relicManager`、地图 `[MapNode]` + `currentNodeId`、`seed/floor/maxFloor/isOver/won` +- “消耗性”现在是 **Consumable Cards**(卡牌类型 `.consumable`),作为卡牌实例存在于 `deck` 中,并受槽位上限约束(见 `RunState.maxConsumableCardSlots`) + +### 1.2 `RunSnapshot`(存档交换格式) + +- `RunSnapshot` 是 `Codable`,覆盖 Run 的核心状态: + - `version/seed/floor/maxFloor/gold/mapNodes/currentNodeId/player/deck/relicIds/isOver/won` +- 版本策略由 `RunSaveVersion` 管理 + +--- + +## 2. 工程与目录建议(visionOS-only) + +``` +SaluNative/ +├── SaluNative.xcodeproj +├── SaluAVP/ # ✅ visionOS-only App +│ ├── SaluAVPApp.swift +│ ├── ContentView.swift # 2D 控制面板(入口/设置/历史) +│ ├── Immersive/ # 3D 体验主体(ImmersiveSpace) +│ │ ├── ImmersiveRootView.swift +│ │ ├── MapScene.swift +│ │ └── BattleScene.swift +│ └── Assets.xcassets +└── Shared/ # 可选:跨 Target 共享(不含 RealityKit) + ├── AppRoute.swift + ├── GameSession.swift + └── ViewModels/ + └── BattleSession.swift +``` + +依赖方向: + +- `SaluAVP → GameCore` ✅ +- `SaluAVP → Shared` ✅(若存在) +- `GameCore → (任何 App/UI/RealityKit)` ❌ + +--- + +## 3. 交互与 UX(Immersive-first) + +- 核心策略:把“选择式交互”映射为 3D 的稳定输入模型 + - 选卡 →(若需要)选目标 → 出牌 → 结束回合 +- 必须有清晰反馈: + - 可交互/不可交互(灰度/高亮) + - 选中态(描边/发光/抬升) + - 操作结果(伤害/格挡/状态变化的可视化或日志) +- 2D 控制面板负责: + - 新游戏(可输入 seed) + - 打开/关闭 ImmersiveSpace + - 设置/历史(后续) + +--- + +## 4. 执行计划(以 AVP 为主) + +### P0(必须):工程打通(SaluAVP) + +- `SaluAVP` 目标可编译;能 `import GameCore` +- 最小页面:2D 控制面板 + 一个可进入的 ImmersiveSpace(占位场景) + +### P1(必须):3D 地图闭环 + +- ImmersiveSpace 中渲染地图节点(占位几何体即可) +- 节点状态:可达/已完成/当前节点 +- 选择节点 → 调用 `RunState.enterNode` → 路由到对应房间(先用占位房间也可)→ `completeCurrentNode` 返回地图 + +### P2(重要):3D 战斗闭环 + +- ImmersiveSpace 中渲染:玩家/敌人/手牌/能量/日志(形式不限) +- 用 `BattleSession` 桥接 `BattleEngine`,实现出牌与结束回合 +- 战斗结束后应用奖励/推进地图(可先最小化奖励) + +### P3(后续):SwiftData 存档与历史 + +- 继续沿用“索引字段 + JSON Blob”的 SwiftData 策略 +- `RunSnapshot`/`BattleRecord` blob 存储,避免模型演进成本 + diff --git "a/.github/plans/Plan macOS - GUI \345\216\237\347\224\237\345\256\236\347\216\260\357\274\210SaluCRH\357\274\211.md" "b/.github/plans/Plan macOS - GUI \345\216\237\347\224\237\345\256\236\347\216\260\357\274\210SaluCRH\357\274\211.md" new file mode 100644 index 0000000..08e06a7 --- /dev/null +++ "b/.github/plans/Plan macOS - GUI \345\216\237\347\224\237\345\256\236\347\216\260\357\274\210SaluCRH\357\274\211.md" @@ -0,0 +1,35 @@ +--- +title: Plan macOS - GUI 原生实现(SaluCRH) +date: 2026-01-29 +updated: 2026-01-29 +architecture: macOS App (SwiftUI) +target: SaluCRH +status: optional +--- + +# Plan macOS:GUI 原生实现(SaluCRH) + +## 0. 定位 + +macOS 版本当前为 **可选/不阻塞**:后续开发资源将优先投向 `SaluAVP`(Apple Vision Pro 原生 3D)。 + +如果需要保留 macOS 版本,建议目标仅为: + +- 提供一个窗口化 2D GUI(SwiftUI),用于快速调试/验收基础流程 +- 逻辑与内容 **100% 复用 `GameCore`**,不依赖 `GameCLI` + +## 1. 验证命令参考 + +```bash +xcodebuild -project SaluNative/SaluNative.xcodeproj \ + -scheme SaluCRH \ + -destination 'platform=macOS' \ + build +``` + +## 2. 建议范围(最小化维护成本) + +- 只维护主菜单/地图/战斗的最小闭环(2D) +- 不追求与 AVP 的 3D 表现一致,只保证核心规则一致(由 `GameCore` 保证) +- 尽量避免引入额外的 macOS-only 复杂特性(快捷键体系、复杂窗口管理等) + 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 | - | - | - | From 05da26936d70ee3726ee188822315859f287c125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:18:01 +0800 Subject: [PATCH 04/13] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=20App=20=E7=BB=93=E6=9E=84=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=B9=20Apple=20Vision=20Pro=20=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d61f9b5..140fc6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,10 @@ 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/SaluCRH/`:macOS App(2D SwiftUI,可选/不阻塞) + - `SaluNative/SaluAVP/`:Apple Vision Pro(visionOS)App(原生 3D:ImmersiveSpace + RealityKit,主线) + - (可选)`SaluNative/Shared/`:跨 Target 共享的状态机/桥接层(禁止引入 RealityKit) - `Tests/`:`GameCoreTests`、`GameCLITests`、`GameCLIUITests`。 - `.giithub/docs/`:设定、剧情与玩法规则说明(写内容/做 UI 时优先对齐这里)。 - `.giithub/plans/`:技术方案与执行计划。 @@ -40,7 +43,7 @@ xcodebuild -project SaluNative/SaluNative.xcodeproj \ # 命令行编译(visionOS Simulator) xcodebuild -project SaluNative/SaluNative.xcodeproj \ - -scheme SaluCRH \ + -scheme SaluAVP \ -destination 'platform=visionOS Simulator,name=Apple Vision Pro' \ build ``` From 37f28babf5d7e4304d525c9ccecad7359027ff9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:18:09 +0800 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20RealityKitCo?= =?UTF-8?q?ntent=20=E5=8C=85=E5=8F=8A=E7=9B=B8=E5=85=B3=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - .../ProjectData/main.json | 10 + .../WorkspaceData/SceneMetadataList.json | 202 ++++++++++++++++ .../WorkspaceData/Settings.rcprojectdata | 17 ++ .../Packages/RealityKitContent/Package.swift | 34 +++ .../Packages/RealityKitContent/README.md | 3 + .../RealityKitContent.rkassets/Immersive.usda | 54 +++++ .../Materials/GridMaterial.usda | 216 ++++++++++++++++++ .../RealityKitContent.rkassets/Scene.usda | 62 +++++ .../RealityKitContent/RealityKitContent.swift | 4 + 10 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json create mode 100644 SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json create mode 100644 SaluNative/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata create mode 100644 SaluNative/Packages/RealityKitContent/Package.swift create mode 100644 SaluNative/Packages/RealityKitContent/README.md create mode 100644 SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Immersive.usda create mode 100644 SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda create mode 100644 SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda create mode 100644 SaluNative/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift 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/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 From 2775b24ddbc538d3375b862077a6fb7dd2f4ed7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:21:31 +0800 Subject: [PATCH 06/13] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E9=93=BE=E6=8E=A5=E4=B8=AD=E7=9A=84=E6=8B=BC=E5=86=99?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...17\344\270\232\345\212\241\350\257\264\346\230\216.md" | 2 +- ...45\256\232\344\270\216\345\211\247\346\203\205v1.0.md" | 2 +- AGENTS.md | 8 ++++---- Sources/GameCLI/AGENTS.md | 2 +- Sources/GameCore/AGENTS.md | 2 +- Sources/GameCore/Run/ChapterText.swift | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) 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/AGENTS.md b/AGENTS.md index 140fc6a..f5ae6c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ Salu 是一个跨平台(macOS/Linux/Windows)的回合制卡牌战斗游戏 - `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/`:技术方案与执行计划。 ## 构建、测试和开发命令 @@ -50,7 +50,7 @@ xcodebuild -project SaluNative/SaluNative.xcodeproj \ ## 本地存储与配置 -- 本地存储默认位置与文件结构见 `.giithub/docs/本地存储说明.md`(run 存档、战斗历史、设置、调试日志)。 +- 本地存储默认位置与文件结构见 `.github/docs/本地存储说明.md`(run 存档、战斗历史、设置、调试日志)。 - 需要隔离数据(测试/调试/复现 bug)时,优先用环境变量覆盖数据目录:`SALU_DATA_DIR=/tmp/salu-test`。 - 复现战斗/地图行为时建议固定随机种子:`swift run GameCLI --seed 1`(也可用 `--seed=1`)。 @@ -75,7 +75,7 @@ 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 描述里注明对应章节。 +- 文档/剧情/玩法规则的变更优先同步到 `.github/docs/`,并在 PR 描述里注明对应章节。 ## 提交与 Pull Request 规范 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 { From 91667e13c80db0b2566f684c9ffd9aba1def7780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:21:37 +0800 Subject: [PATCH 07/13] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=20SaluCRH=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=BC=80=E5=8F=91=E8=A7=84=E8=8C=83=EF=BC=8C?= =?UTF-8?q?=E6=98=8E=E7=A1=AE=E5=B9=B3=E5=8F=B0=E6=94=AF=E6=8C=81=E5=92=8C?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SaluNative/SaluCRH/AGENTS.md | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/SaluNative/SaluCRH/AGENTS.md b/SaluNative/SaluCRH/AGENTS.md index 8656b01..21d5043 100644 --- a/SaluNative/SaluCRH/AGENTS.md +++ b/SaluNative/SaluCRH/AGENTS.md @@ -1,13 +1,13 @@ # SaluCRH 模块开发规范 -SaluCRH 是 Salu 的原生 App 前端,采用 Multiplatform SwiftUI 架构,同时支持 macOS 和 visionOS。 +SaluCRH 是 Salu 的 **macOS** 原生 App 前端(2D SwiftUI)。Apple Vision Pro(visionOS)的原生 3D 实现由 `SaluAVP` Target 承担,本模块不包含 RealityKit/ImmersiveSpace 主流程。 ## 模块职责 - **UI 层**:SwiftUI 视图、动画、用户交互 - **状态管理**:GameSession 状态机、AppRoute 路由 - **持久化**:SwiftData 存档、战斗历史 -- **平台适配**:通过 `#if os()` 处理 macOS/visionOS 差异 +- **macOS 适配**:窗口布局、键鼠交互、快捷键(可选) ## 依赖关系 @@ -22,18 +22,8 @@ GameCLI ↔ SaluCRH ❌(互不依赖) ## 平台支持 - **macOS** 14.0+(当前支持 ✅) -- **visionOS** 2.0+(配置中) -使用条件编译处理平台差异: - -```swift -#if os(visionOS) -import RealityKit -// visionOS 特有代码(如 ImmersiveSpace) -#elseif os(macOS) -// macOS 特有代码 -#endif -``` +> visionOS:请在 `SaluNative/SaluAVP/`(`SaluAVP` scheme)实现原生 3D 体验,不要在本模块引入 RealityKit。 ## 目录结构 @@ -55,9 +45,6 @@ SaluCRH/ ├── Persistence/ # SwiftData 模型 │ ├── RunSaveEntity.swift │ └── BattleRecordEntity.swift -├── Platform/ # 平台特有代码 -│ └── visionOS/ -│ └── ImmersiveView.swift └── Assets.xcassets ``` @@ -120,7 +107,7 @@ import GameCore let strike = CardRegistry.require("strike") // 创建冒险状态 -let runState = RunState(seed: 12345) +let runState = RunState.newRun(seed: 12345) // 创建战斗引擎 let engine = BattleEngine(...) @@ -189,15 +176,9 @@ final class RunSaveEntity { ```swift var body: some View { - #if os(visionOS) - // visionOS: 更大的点击目标 - CardView() - .frame(width: 200, height: 300) - #else // macOS: 更紧凑的布局 CardView() .frame(width: 120, height: 180) - #endif } ``` @@ -211,12 +192,6 @@ 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 ``` ## 构建失败排查指南 @@ -229,7 +204,7 @@ xcodebuild -project SaluNative/SaluNative.xcodeproj \ - `Entity.currentHP` / `Entity.maxHP` - 注意大小写 - `MapNode.roomType` - 不是 `type` - `RelicManager.all` - 获取所有遗物 ID(不是 `relics`) - - `ConsumableID` 是 ID 类型,通过 `ConsumableRegistry.require()` 获取定义 + - “消耗品”已是 **Consumable Cards**:通过 `CardRegistry` 判断 `CardDefinition.type == .consumable`,并作为实例存在于 `RunState.deck` 3. **技术文档**:完整的 GameCore API 参考见 [GameCore 规范](../../Sources/GameCore/AGENTS.md) ## 禁止事项 @@ -241,5 +216,4 @@ xcodebuild -project SaluNative/SaluNative.xcodeproj \ - ❌ 猜测 API - 遇到不确定的类型/属性,先查看 GameCore 源码 ## 参考文档 -- [总体方案](<../../.giithub/plans/visionOS + macOS GUI 原生实现方案(SwiftUI).md>) - [GameCore 规范](../../Sources/GameCore/AGENTS.md) From 927ce5e80d9c1390b2b026e369df99183d82150f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:23:48 +0800 Subject: [PATCH 08/13] =?UTF-8?q?docs:=20=E5=88=A0=E9=99=A4=20macOS=20GUI?= =?UTF-8?q?=20=E5=8E=9F=E7=94=9F=E5=AE=9E=E7=8E=B0=EF=BC=88SaluCRH?= =?UTF-8?q?=EF=BC=89=E8=AE=A1=E5=88=92=E6=96=87=E6=A1=A3=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=96=87=E6=A1=A3=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...216\260\357\274\210SaluCRH\357\274\211.md" | 35 ---- SaluNative/SaluCRH/AGENTS.md | 174 +++--------------- 2 files changed, 28 insertions(+), 181 deletions(-) delete mode 100644 ".github/plans/Plan macOS - GUI \345\216\237\347\224\237\345\256\236\347\216\260\357\274\210SaluCRH\357\274\211.md" diff --git "a/.github/plans/Plan macOS - GUI \345\216\237\347\224\237\345\256\236\347\216\260\357\274\210SaluCRH\357\274\211.md" "b/.github/plans/Plan macOS - GUI \345\216\237\347\224\237\345\256\236\347\216\260\357\274\210SaluCRH\357\274\211.md" deleted file mode 100644 index 08e06a7..0000000 --- "a/.github/plans/Plan macOS - GUI \345\216\237\347\224\237\345\256\236\347\216\260\357\274\210SaluCRH\357\274\211.md" +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Plan macOS - GUI 原生实现(SaluCRH) -date: 2026-01-29 -updated: 2026-01-29 -architecture: macOS App (SwiftUI) -target: SaluCRH -status: optional ---- - -# Plan macOS:GUI 原生实现(SaluCRH) - -## 0. 定位 - -macOS 版本当前为 **可选/不阻塞**:后续开发资源将优先投向 `SaluAVP`(Apple Vision Pro 原生 3D)。 - -如果需要保留 macOS 版本,建议目标仅为: - -- 提供一个窗口化 2D GUI(SwiftUI),用于快速调试/验收基础流程 -- 逻辑与内容 **100% 复用 `GameCore`**,不依赖 `GameCLI` - -## 1. 验证命令参考 - -```bash -xcodebuild -project SaluNative/SaluNative.xcodeproj \ - -scheme SaluCRH \ - -destination 'platform=macOS' \ - build -``` - -## 2. 建议范围(最小化维护成本) - -- 只维护主菜单/地图/战斗的最小闭环(2D) -- 不追求与 AVP 的 3D 表现一致,只保证核心规则一致(由 `GameCore` 保证) -- 尽量避免引入额外的 macOS-only 复杂特性(快捷键体系、复杂窗口管理等) - diff --git a/SaluNative/SaluCRH/AGENTS.md b/SaluNative/SaluCRH/AGENTS.md index 21d5043..731344e 100644 --- a/SaluNative/SaluCRH/AGENTS.md +++ b/SaluNative/SaluCRH/AGENTS.md @@ -48,152 +48,6 @@ SaluCRH/ └── 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.newRun(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 { - // macOS: 更紧凑的布局 - CardView() - .frame(width: 120, height: 180) -} -``` - -## 构建与验证 - -验证原则:只改 `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 -``` - ## 构建失败排查指南 **重要**:编译错误通常是因为 GameCore API 使用不正确。遇到构建失败时: @@ -217,3 +71,31 @@ xcodebuild -project SaluNative/SaluNative.xcodeproj \ ## 参考文档 - [GameCore 规范](../../Sources/GameCore/AGENTS.md) + + +# Plan macOS:GUI 原生实现(SaluCRH) + +## 0. 定位 + +macOS 版本当前为 **可选/不阻塞**:后续开发资源将优先投向 `SaluAVP`(Apple Vision Pro 原生 3D)。 + +如果需要保留 macOS 版本,建议目标仅为: + +- 提供一个窗口化 2D GUI(SwiftUI),用于快速调试/验收基础流程 +- 逻辑与内容 **100% 复用 `GameCore`**,不依赖 `GameCLI` + +## 1. 验证命令参考 + +```bash +xcodebuild -project SaluNative/SaluNative.xcodeproj \ + -scheme SaluCRH \ + -destination 'platform=macOS' \ + build +``` + +## 2. 建议范围(最小化维护成本) + +- 只维护主菜单/地图/战斗的最小闭环(2D) +- 不追求与 AVP 的 3D 表现一致,只保证核心规则一致(由 `GameCore` 保证) +- 尽量避免引入额外的 macOS-only 复杂特性(快捷键体系、复杂窗口管理等) + From 1da5cbc3a481584574191b3e9c44d788529a2d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:32:26 +0800 Subject: [PATCH 09/13] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20Apple=20Visi?= =?UTF-8?q?on=20Pro=20=E5=8E=9F=E7=94=9F=203D=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E6=96=87=E6=A1=A3=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E4=B8=8E=E6=A0=B8=E5=BF=83=E5=86=B3=E7=AD=96?= =?UTF-8?q?=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...216\260\357\274\210SaluAVP\357\274\211.md" | 206 ++++++++++++------ 1 file changed, 138 insertions(+), 68 deletions(-) diff --git "a/.github/plans/Plan AVP - 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/Plan AVP - Apple Vision Pro \345\216\237\347\224\237 3D \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md" index 136aed3..7c900ff 100644 --- "a/.github/plans/Plan AVP - 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/Plan AVP - Apple Vision Pro \345\216\237\347\224\237 3D \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md" @@ -8,34 +8,43 @@ target: SaluAVP # Plan AVP:Apple Vision Pro(visionOS)原生 3D 实现(SaluAVP) -## 0. 目标与核心决策 +## 0. 概览 -### 目标 +### 背景与定位 -- 在本仓库提供一个 **visionOS-only** 的原生 App:`SaluAVP` -- **体验形态**:以 **ImmersiveSpace + RealityKit(原生 3D)** 为主;2D Window 仅作为入口/设置/历史等“控制面板” -- 逻辑与内容 **100% 复用 `GameCore`**,不引入对 `GameCLI` 的依赖 -- 对外验收:能完成完整一局(入口 → 地图推进 → 战斗/事件/商店/休息 → Boss → 结算),并保持 seed 可复现 +- `SaluAVP` 是仓库内的 **visionOS-only** 原生 App Target:以 **ImmersiveSpace + RealityKit(原生 3D)** 为主。 +- 2D Window 仅承担“控制面板”:入口 / seed / 存档选择 / 设置 / 历史等,不承载主战斗体验。 +- 逻辑与内容 **仅复用 `GameCore`**(禁止依赖 `GameCLI`),确保跨平台逻辑一致与可测试。 -### 核心决策 +### 目标(Goals) -1. **拆分产品线:SaluCRH(macOS)与 SaluAVP(visionOS)** - - AVP 的主玩法呈现与输入模型(沉浸式 3D)与 macOS 窗口化长期分叉,拆 Target 能减少 `#if os()` 污染与耦合 +- 能完成完整一局:入口 → 地图推进 → 房间(战斗/事件/商店/休息)→ Boss → 结算。 +- 同 seed + 同选择路径,核心结果可复现(地图分支/战斗结算/奖励等)。 +- Immersive-first:至少一个可交互 ImmersiveSpace(地图或战斗,优先地图)打通闭环。 -2. **RealityKit 必选,但只存在于 `SaluNative/`** - - 禁止把任何 Apple-only 代码(SwiftUI/SwiftData/RealityKit)放入 SwiftPM 的 `Sources/`,避免影响 Linux/Windows 构建 +### 非目标(Non-goals) + +- 首版不追求完整的 VFX/动画系统、写实材质、复杂骨骼动画。 +- 不把“3D 渲染实现”抽到共享层;共享层仅做状态机/桥接(见下文)。 +- 不在 SwiftPM 的 `Sources/` 内引入任何 Apple-only 框架(SwiftUI / SwiftData / RealityKit 等)。 + +### 核心决策(Decisions) +1. **拆分产品线:SaluCRH(macOS)与 SaluAVP(visionOS)** + - 输入模型与呈现形态长期分叉,拆 Target 避免 `#if os()` 泛滥与耦合。 +2. **RealityKit 必选,但只存在于 `SaluNative/`** + - `Sources/` 仍然保持纯逻辑、可跨平台构建。 3. **共享层只共享“状态/桥接”,不共享“渲染实现”** - - 共享:`GameSession` / 路由状态机 / 与 `GameCore` 的桥接 ViewModels(例如 `BattleSession`) - - 不共享:3D 场景构建、实体/材质/动画、沉浸输入手势(全部放在 `SaluAVP` 内部) + - 共享:路由状态机 / 与 `GameCore` 的桥接 Session / ViewModels。 + - 不共享:3D 场景构建、实体/材质/动画、沉浸输入手势(全部在 `SaluAVP` 内部)。 -### 验收标准 +### 验收标准(Definition of Done) -- Xcode:`SaluAVP` 能在 visionOS Simulator 上编译并运行 -- 行为:同 seed + 同选择路径,战斗/地图/奖励/事件/商店结果可复现(允许 3D 表现差异) -- 3D:核心流程至少有一个可交互 ImmersiveSpace(地图/战斗二选一先打通) +- `SaluAVP` 可在 visionOS Simulator 上编译并运行(入口 2D + 至少 1 个可交互 ImmersiveSpace)。 +- 完整一局可通关,并能导出/记录“选择路径”(便于复现实验与回归)。 +- 关键路径无平台专属逻辑泄漏到 `Sources/`(CI/本地跨平台构建不受影响)。 -### 验证命令参考 +### 验证命令(Build) ```bash # visionOS(SaluAVP) @@ -45,89 +54,150 @@ xcodebuild -project SaluNative/SaluNative.xcodeproj \ build ``` ---- - -## 1. 关键接口(与 AVP 直接相关) - -> 注意:以下接口以 `Sources/GameCore/` 为准,UI 不猜测 API。 - -### 1.1 `RunState`(冒险全局状态) +```bash +# 若本次改动涉及 SwiftPM 的 `Sources/**`(例如 GameCore 逻辑),需要补跑: +swift test +``` -- 冒险状态包含:玩家 `Entity`、牌组 `[Card]`、金币 `gold`、遗物 `relicManager`、地图 `[MapNode]` + `currentNodeId`、`seed/floor/maxFloor/isOver/won` -- “消耗性”现在是 **Consumable Cards**(卡牌类型 `.consumable`),作为卡牌实例存在于 `deck` 中,并受槽位上限约束(见 `RunState.maxConsumableCardSlots`) +--- -### 1.2 `RunSnapshot`(存档交换格式) +## 1. 约束与边界(必须遵守) -- `RunSnapshot` 是 `Codable`,覆盖 Run 的核心状态: - - `version/seed/floor/maxFloor/gold/mapNodes/currentNodeId/player/deck/relicIds/isOver/won` -- 版本策略由 `RunSaveVersion` 管理 +- 依赖方向必须保持: + - `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**(使其可被 macOS target 复用)。 --- -## 2. 工程与目录建议(visionOS-only) +## 2. 架构草图(建议,不强制) + +### 模块结构(建议) ``` SaluNative/ ├── SaluNative.xcodeproj -├── SaluAVP/ # ✅ visionOS-only App +├── SaluAVP/ # ✅ visionOS-only App(RealityKit 在此处) │ ├── SaluAVPApp.swift -│ ├── ContentView.swift # 2D 控制面板(入口/设置/历史) -│ ├── Immersive/ # 3D 体验主体(ImmersiveSpace) -│ │ ├── ImmersiveRootView.swift -│ │ ├── MapScene.swift -│ │ └── BattleScene.swift -│ └── Assets.xcassets -└── Shared/ # 可选:跨 Target 共享(不含 RealityKit) +│ ├── 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 内):只关心“如何渲染”和“如何把手势变成选择”,不关心规则细节。 + +### 数据流(建议) -- `SaluAVP → GameCore` ✅ -- `SaluAVP → Shared` ✅(若存在) -- `GameCore → (任何 App/UI/RealityKit)` ❌ +`Immersive UI(选择/手势)` → `Session(桥接/路由)` → `GameCore(状态推进)` → `Session(同步到可观察状态)` → `UI` --- -## 3. 交互与 UX(Immersive-first) +## 3. 关键接口速查(以当前 `GameCore` 实现为准) -- 核心策略:把“选择式交互”映射为 3D 的稳定输入模型 - - 选卡 →(若需要)选目标 → 出牌 → 结束回合 -- 必须有清晰反馈: +> 目标:让 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 控制面板负责: - - 新游戏(可输入 seed) - - 打开/关闭 ImmersiveSpace - - 设置/历史(后续) + - 结果反馈(数值飘字/简易日志/状态图标均可) + +### 2D 控制面板(MVP) + +- New Run:输入 seed(或随机生成后展示)并开始。 +- Continue:从 App 层持久化恢复(如果 P3 之前未做存档,可先隐藏/置灰)。 +- Immersive 控制:进入/退出 ImmersiveSpace,显示当前 run 的关键摘要(楼层/金币/HP)。 --- -## 4. 执行计划(以 AVP 为主) +## 5. 执行计划(以 AVP 为主) + +### P0(必须):工程打通(SaluAVP Skeleton) -### P0(必须):工程打通(SaluAVP) +- 产出: + - `SaluAVP` 目标可编译;能 `import GameCore`。 + - 2D 控制面板 + 可进入的 ImmersiveSpace(占位场景:1 个地板 + 1 个可点击物体)。 +- DoD: + - Simulator 可运行;能在 2D 界面控制 ImmersiveSpace 打开/关闭,不崩溃。 -- `SaluAVP` 目标可编译;能 `import GameCore` -- 最小页面:2D 控制面板 + 一个可进入的 ImmersiveSpace(占位场景) +### P1(必须):3D 地图闭环(Map Loop) -### P1(必须):3D 地图闭环 +- 产出: + - ImmersiveSpace 中渲染地图节点(占位几何体即可)。 + - 节点状态:可达/已完成/当前节点(至少用三态材质/颜色区分)。 + - 选择节点 → `RunState.enterNode(_:)` → 路由到对应房间(先占位房间)→ `completeCurrentNode()` 返回地图。 +- DoD: + - 能从 Act1 开始一路点击推进到 Boss,并触发“run over/won”。 -- ImmersiveSpace 中渲染地图节点(占位几何体即可) -- 节点状态:可达/已完成/当前节点 -- 选择节点 → 调用 `RunState.enterNode` → 路由到对应房间(先用占位房间也可)→ `completeCurrentNode` 返回地图 +### P2(重要):3D 战斗闭环(Battle Loop) -### P2(重要):3D 战斗闭环 +- 产出: + - ImmersiveSpace 中渲染:玩家/敌人/手牌/能量/日志(形式不限,先可读可用)。 + - Session 桥接 `GameCore` 的战斗推进(建议放在 `SaluNative/Shared/`,以便未来与 macOS 复用)。 + - 战斗结束后:更新 `RunState`(例如 `updateFromBattle(playerHP:)`)→ 应用奖励/推进地图(奖励可先最小化)。 +- DoD: + - 战斗可完整结束(胜/负),并能回到地图继续推进。 -- ImmersiveSpace 中渲染:玩家/敌人/手牌/能量/日志(形式不限) -- 用 `BattleSession` 桥接 `BattleEngine`,实现出牌与结束回合 -- 战斗结束后应用奖励/推进地图(可先最小化奖励) +### P3(后续):持久化(SwiftData / JSON Blob) -### P3(后续):SwiftData 存档与历史 +- 原则:继续沿用“索引字段 + JSON Blob”的策略,降低模型演进成本。 + - 索引字段:`seed`、`updatedAt`、`floor`、`won` 等(便于列表与筛选)。 + - Blob:`RunSnapshot`(以及若存在的战斗历史快照)。 +- DoD: + - 能存/读当前 run;能在控制面板展示“继续游戏”。 + +### P4(后续):观测性与回归(Determinism + Replay) + +- 增加“选择路径记录”(例如节点选择、出牌序列、关键事件),用于: + - 同 seed 重放验证(快速定位“逻辑回归 vs UI 差异”)。 + - 自动化回归(未来可在 CLI 或测试里复用)。 +- DoD: + - 把一局 run 的关键选择导出为可读文本/JSON,并能用其重放到同一结果(允许表现差异)。 + +--- -- 继续沿用“索引字段 + JSON Blob”的 SwiftData 策略 -- `RunSnapshot`/`BattleRecord` blob 存储,避免模型演进成本 +## 6. 风险清单(提前规避) +- **Simulator/真机差异**:输入与性能差距大;MVP 阶段所有交互必须在 Simulator 上可用,真机再做增强。 +- **可复现性被 UI 污染**:UI 层禁止引入“非确定”输入(时间/随机);所有决策必须来自用户选择与 seed。 +- **共享层膨胀**:Shared 只放状态机/桥接/纯 Swift,禁止把 RealityKit/材质/动画放进去。 +- **资产管线不稳定**:先用程序化几何体占位;确认交互与流程稳定后再引入 USDZ/贴图。 From 2e574241524495cfcf0341d30cf0e8ec3d07861e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:34:53 +0800 Subject: [PATCH 10/13] - --- ... \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename ".github/plans/Plan AVP - Apple Vision Pro \345\216\237\347\224\237 3D \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md" => ".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" (99%) diff --git "a/.github/plans/Plan AVP - 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" similarity index 99% rename from ".github/plans/Plan AVP - Apple Vision Pro \345\216\237\347\224\237 3D \345\256\236\347\216\260\357\274\210SaluAVP\357\274\211.md" rename to ".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" index 7c900ff..d35d070 100644 --- "a/.github/plans/Plan AVP - 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" @@ -1,5 +1,5 @@ --- -title: Plan AVP - Apple Vision Pro 原生 3D 实现(SaluAVP) +title: Apple Vision Pro 原生 3D 实现(SaluAVP) date: 2026-01-29 updated: 2026-01-29 architecture: visionOS-only App + Immersive-first (RealityKit) From 8ade5e5d01c9b677aef100f1bdf37963627245a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:30:29 +0800 Subject: [PATCH 11/13] =?UTF-8?q?refactor:=20=E5=88=A0=E9=99=A4=20SaluCRH?= =?UTF-8?q?=20=E6=A8=A1=E5=9D=97=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E9=9B=86=E4=B8=AD=E5=BC=80=E5=8F=91=20Apple?= =?UTF-8?q?=20Vision=20Pro=20=E5=8E=9F=E7=94=9F=203D=20=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SaluNative/SaluCRH/AGENTS.md | 101 ------- SaluNative/SaluCRH/AppRoute.swift | 41 --- .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 58 ---- .../SaluCRH/Assets.xcassets/Contents.json | 6 - SaluNative/SaluCRH/ContentView.swift | 170 ------------ SaluNative/SaluCRH/GameSession.swift | 111 -------- SaluNative/SaluCRH/SaluCRHApp.swift | 10 - SaluNative/SaluCRH/Views/MainMenuView.swift | 153 ----------- SaluNative/SaluCRH/Views/MapView.swift | 249 ------------------ .../SaluNative.xcodeproj/project.pbxproj | 148 ----------- 11 files changed, 1058 deletions(-) delete mode 100644 SaluNative/SaluCRH/AGENTS.md delete mode 100644 SaluNative/SaluCRH/AppRoute.swift delete mode 100644 SaluNative/SaluCRH/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 SaluNative/SaluCRH/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 SaluNative/SaluCRH/Assets.xcassets/Contents.json delete mode 100644 SaluNative/SaluCRH/ContentView.swift delete mode 100644 SaluNative/SaluCRH/GameSession.swift delete mode 100644 SaluNative/SaluCRH/SaluCRHApp.swift delete mode 100644 SaluNative/SaluCRH/Views/MainMenuView.swift delete mode 100644 SaluNative/SaluCRH/Views/MapView.swift diff --git a/SaluNative/SaluCRH/AGENTS.md b/SaluNative/SaluCRH/AGENTS.md deleted file mode 100644 index 731344e..0000000 --- a/SaluNative/SaluCRH/AGENTS.md +++ /dev/null @@ -1,101 +0,0 @@ -# SaluCRH 模块开发规范 - -SaluCRH 是 Salu 的 **macOS** 原生 App 前端(2D SwiftUI)。Apple Vision Pro(visionOS)的原生 3D 实现由 `SaluAVP` Target 承担,本模块不包含 RealityKit/ImmersiveSpace 主流程。 - -## 模块职责 - -- **UI 层**:SwiftUI 视图、动画、用户交互 -- **状态管理**:GameSession 状态机、AppRoute 路由 -- **持久化**:SwiftData 存档、战斗历史 -- **macOS 适配**:窗口布局、键鼠交互、快捷键(可选) - -## 依赖关系 - -``` -SaluCRH → GameCore ✅ -GameCore → SaluCRH ❌(禁止反向依赖) -GameCLI ↔ SaluCRH ❌(互不依赖) -``` - -**核心原则**:所有游戏逻辑来自 `GameCore`,本模块只负责 UI 展示和用户交互。 - -## 平台支持 - -- **macOS** 14.0+(当前支持 ✅) - -> visionOS:请在 `SaluNative/SaluAVP/`(`SaluAVP` scheme)实现原生 3D 体验,不要在本模块引入 RealityKit。 - -## 目录结构 - -``` -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 -└── Assets.xcassets -``` - -## 构建失败排查指南 - -**重要**:编译错误通常是因为 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`) - - “消耗品”已是 **Consumable Cards**:通过 `CardRegistry` 判断 `CardDefinition.type == .consumable`,并作为实例存在于 `RunState.deck` -3. **技术文档**:完整的 GameCore API 参考见 [GameCore 规范](../../Sources/GameCore/AGENTS.md) -## 禁止事项 - -- ❌ 在 View 中直接操作 `BattleEngine`(必须通过 `BattleSession`) -- ❌ 引入 UI 层随机源(所有随机通过 GameCore seed 派生) -- ❌ 依赖 `GameCLI`(两者互不依赖) -- ❌ 修改 `GameCore` 来适配 UI(GameCore 保持纯逻辑) -- ❌ 使用单例 ViewModel(使用依赖注入) -- ❌ 猜测 API - 遇到不确定的类型/属性,先查看 GameCore 源码 - -## 参考文档 -- [GameCore 规范](../../Sources/GameCore/AGENTS.md) - - -# Plan macOS:GUI 原生实现(SaluCRH) - -## 0. 定位 - -macOS 版本当前为 **可选/不阻塞**:后续开发资源将优先投向 `SaluAVP`(Apple Vision Pro 原生 3D)。 - -如果需要保留 macOS 版本,建议目标仅为: - -- 提供一个窗口化 2D GUI(SwiftUI),用于快速调试/验收基础流程 -- 逻辑与内容 **100% 复用 `GameCore`**,不依赖 `GameCLI` - -## 1. 验证命令参考 - -```bash -xcodebuild -project SaluNative/SaluNative.xcodeproj \ - -scheme SaluCRH \ - -destination 'platform=macOS' \ - build -``` - -## 2. 建议范围(最小化维护成本) - -- 只维护主菜单/地图/战斗的最小闭环(2D) -- 不追求与 AVP 的 3D 表现一致,只保证核心规则一致(由 `GameCore` 保证) -- 尽量避免引入额外的 macOS-only 复杂特性(快捷键体系、复杂窗口管理等) - 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/AccentColor.colorset/Contents.json b/SaluNative/SaluCRH/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/SaluNative/SaluCRH/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} 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/Assets.xcassets/Contents.json b/SaluNative/SaluCRH/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/SaluNative/SaluCRH/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "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 1f6fa6c..c63a6cf 100644 --- a/SaluNative/SaluNative.xcodeproj/project.pbxproj +++ b/SaluNative/SaluNative.xcodeproj/project.pbxproj @@ -8,13 +8,11 @@ /* Begin PBXBuildFile section */ D84785632F2AE7AF00B5018E /* RealityKitContent in Frameworks */ = {isa = PBXBuildFile; productRef = D84785622F2AE7AF00B5018E /* RealityKitContent */; }; - D8A515242F16C8AA005EBB40 /* GameCore in Frameworks */ = {isa = PBXBuildFile; productRef = D8A515232F16C8AA005EBB40 /* GameCore */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 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 = ""; }; - D8A515172F16C856005EBB40 /* SaluCRH.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SaluCRH.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -36,11 +34,6 @@ path = SaluAVP; sourceTree = ""; }; - D8A515182F16C856005EBB40 /* SaluCRH */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = SaluCRH; - sourceTree = ""; - }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -52,14 +45,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D8A515142F16C856005EBB40 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D8A515242F16C8AA005EBB40 /* GameCore in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -74,7 +59,6 @@ D8A514962F16BA92005EBB40 = { isa = PBXGroup; children = ( - D8A515182F16C856005EBB40 /* SaluCRH */, D847855F2F2AE7AF00B5018E /* SaluAVP */, D84785602F2AE7AF00B5018E /* Packages */, D8A514A02F16BA92005EBB40 /* Products */, @@ -84,7 +68,6 @@ D8A514A02F16BA92005EBB40 /* Products */ = { isa = PBXGroup; children = ( - D8A515172F16C856005EBB40 /* SaluCRH.app */, D847855E2F2AE7AF00B5018E /* SaluAVP.app */, ); name = Products; @@ -116,29 +99,6 @@ productReference = D847855E2F2AE7AF00B5018E /* SaluAVP.app */; productType = "com.apple.product-type.application"; }; - D8A515162F16C856005EBB40 /* SaluCRH */ = { - isa = PBXNativeTarget; - buildConfigurationList = D8A5151F2F16C857005EBB40 /* Build configuration list for PBXNativeTarget "SaluCRH" */; - buildPhases = ( - D8A515132F16C856005EBB40 /* Sources */, - D8A515142F16C856005EBB40 /* Frameworks */, - D8A515152F16C856005EBB40 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - D8A515182F16C856005EBB40 /* SaluCRH */, - ); - name = SaluCRH; - packageProductDependencies = ( - D8A515232F16C8AA005EBB40 /* GameCore */, - ); - productName = SaluCRH; - productReference = D8A515172F16C856005EBB40 /* SaluCRH.app */; - productType = "com.apple.product-type.application"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -152,9 +112,6 @@ D847855D2F2AE7AF00B5018E = { CreatedOnToolsVersion = 26.0; }; - D8A515162F16C856005EBB40 = { - CreatedOnToolsVersion = 26.0; - }; }; }; buildConfigurationList = D8A5149A2F16BA92005EBB40 /* Build configuration list for PBXProject "SaluNative" */; @@ -174,7 +131,6 @@ projectDirPath = ""; projectRoot = ""; targets = ( - D8A515162F16C856005EBB40 /* SaluCRH */, D847855D2F2AE7AF00B5018E /* SaluAVP */, ); }; @@ -188,13 +144,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D8A515152F16C856005EBB40 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -205,13 +154,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D8A515132F16C856005EBB40 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ @@ -398,82 +340,6 @@ }; 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; - 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; - 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; - 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; - XROS_DEPLOYMENT_TARGET = 26.0; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -495,15 +361,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - D8A5151F2F16C857005EBB40 /* Build configuration list for PBXNativeTarget "SaluCRH" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D8A515202F16C857005EBB40 /* Debug */, - D8A515212F16C857005EBB40 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ @@ -518,11 +375,6 @@ isa = XCSwiftPackageProductDependency; productName = RealityKitContent; }; - D8A515232F16C8AA005EBB40 /* GameCore */ = { - isa = XCSwiftPackageProductDependency; - package = D8A5150B2F16C179005EBB40 /* XCLocalSwiftPackageReference "../../salu" */; - productName = GameCore; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = D8A514972F16BA92005EBB40 /* Project object */; From 7ebf33a581e6eddf2c2c3ebabc25d454aa684c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:51:54 +0800 Subject: [PATCH 12/13] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=8C=E5=8F=AA=E5=81=9AAVP=E7=89=88=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=81=9AmacOS=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...236\347\216\260\357\274\210SaluAVP\357\274\211.md" | 6 +++--- AGENTS.md | 11 ++--------- README-en.md | 7 +++---- README.md | 7 +++---- 4 files changed, 11 insertions(+), 20 deletions(-) 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" index d35d070..46cba0b 100644 --- "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" @@ -30,8 +30,8 @@ target: SaluAVP ### 核心决策(Decisions) -1. **拆分产品线:SaluCRH(macOS)与 SaluAVP(visionOS)** - - 输入模型与呈现形态长期分叉,拆 Target 避免 `#if os()` 泛滥与耦合。 +1. **原生 App 仅保留 `SaluAVP`(visionOS)** + - 主玩法与输入模型以沉浸式 3D 为中心;桌面端(macOS)如需体验优先使用 `GameCLI`,避免双 UI 维护成本。 2. **RealityKit 必选,但只存在于 `SaluNative/`** - `Sources/` 仍然保持纯逻辑、可跨平台构建。 3. **共享层只共享“状态/桥接”,不共享“渲染实现”** @@ -72,7 +72,7 @@ swift test - UI 层不得用“系统时间/系统随机数”影响战斗与地图结果。 - 多平台约束: - 不在 SwiftPM `Sources/` 内使用 Apple-only API;AVP 的 UI 代码全部留在 `SaluNative/`。 - - 若需要“跨 Target 共享”,请放到 `SaluNative/Shared/`,并保持 **不引入 RealityKit**(使其可被 macOS target 复用)。 + - 若需要“跨 Target 共享”,请放到 `SaluNative/Shared/`,并保持 **不引入 RealityKit**(避免把渲染实现传播到共享层)。 --- diff --git a/AGENTS.md b/AGENTS.md index f5ae6c9..d3d9b33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,6 @@ Salu 是一个跨平台(macOS/Linux/Windows)的回合制卡牌战斗游戏 - `Sources/GameCore/`:纯逻辑层(规则/状态/战斗/卡牌/敌人/地图/存档快照模型)。禁止 I/O、禁止 UI;详细约束见 `Sources/GameCore/AGENTS.md`。 - `Sources/GameCLI/`:CLI/TUI 表现层(终端渲染/输入/房间流程/持久化落盘)。详细约束见 `Sources/GameCLI/AGENTS.md`。 - `SaluNative/`:原生 App(Xcode 管理,依赖 `GameCore`,不依赖 `GameCLI`) - - `SaluNative/SaluCRH/`:macOS App(2D SwiftUI,可选/不阻塞) - `SaluNative/SaluAVP/`:Apple Vision Pro(visionOS)App(原生 3D:ImmersiveSpace + RealityKit,主线) - (可选)`SaluNative/Shared/`:跨 Target 共享的状态机/桥接层(禁止引入 RealityKit) - `Tests/`:`GameCoreTests`、`GameCLITests`、`GameCLIUITests`。 @@ -29,18 +28,12 @@ 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 SaluAVP \ @@ -74,7 +67,7 @@ swift test --filter GameCLITests ## 开发流程建议 - 修改前先定位模块边界:规则与状态放 `GameCore`,文件读写与终端渲染放 `GameCLI`(避免反向依赖)。 -- 提交前按影响范围验证:修改 `Sources/**/*.swift` 或 `Package.swift` 时至少跑一次 `swift test`;仅改 `SaluNative/` 时至少跑一次 `xcodebuild -project SaluNative/SaluNative.xcodeproj -scheme SaluCRH -destination 'platform=macOS' build`。 +- 提交前按影响范围验证:修改 `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..63d6ab1 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,10 @@ ### 方式一:原生 App(开发中 🚧) -使用 Xcode 打开 `SaluNative/SaluNative.xcodeproj` 并运行 `SaluCRH`。 +使用 Xcode 打开 `SaluNative/SaluNative.xcodeproj` 并运行 `SaluAVP`。 支持平台: -- **macOS** 14+(已支持 ✅) -- **visionOS** 2+(配置中 ⏳) +- **visionOS** 2+(开发中 🚧) > 要求:Xcode 16+ / macOS 14+ @@ -62,4 +61,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` From a238d967f7aa1c71bec0c7c5574f8685500c8378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=92=F0=9D=93=B1=F0=9D=93=B2=F0=9D=93=B2=20?= =?UTF-8?q?=F0=9D=93=9C=F0=9D=93=AA=F0=9D=93=B0=F0=9D=93=B7=F0=9D=93=BE?= =?UTF-8?q?=F0=9D=93=BC?= <151696847+chiimagnus@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:53:29 +0800 Subject: [PATCH 13/13] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20README?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=AD=A3=20visionOS=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E8=A6=81=E6=B1=82=E4=B8=BA=2026.0+?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 63d6ab1..6ff7c92 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,7 @@ 使用 Xcode 打开 `SaluNative/SaluNative.xcodeproj` 并运行 `SaluAVP`。 支持平台: -- **visionOS** 2+(开发中 🚧) - -> 要求:Xcode 16+ / macOS 14+ +- **visionOS** 26.0+(开发中 🚧) ### 方式二:命令行版本(CLI)