diff --git a/Assets/000_assets/material/M_SquareFrame 1.mat b/Assets/000_assets/material/M_SquareFrame 1.mat index 8fabdca0..86dc28ec 100644 --- a/Assets/000_assets/material/M_SquareFrame 1.mat +++ b/Assets/000_assets/material/M_SquareFrame 1.mat @@ -213,7 +213,7 @@ Material: - _Dst: 10 - _DstBlend: 0 - _DstBlendAlpha: 0 - - _EdgeValue: 0.75684065 + - _EdgeValue: 0.5578781 - _EnvironmentReflections: 1 - _FNLfanxiangkaiguan: 0 - _Face: 1 @@ -258,7 +258,7 @@ Material: - _Mask_scale: 1 - _Metallic: 0 - _OcclusionStrength: 1 - - _Opacity: 0.24315935 + - _Opacity: 0.44212192 - _Parallax: 0.005 - _Pass: 0 - _QueueOffset: 0 diff --git a/Assets/AmplifyShaderEditor/Examples/Built-In Samples.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/Built-In Samples.unitypackage.meta new file mode 100644 index 00000000..d97f4fc6 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/Built-In Samples.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e00e6f90ab8233e46a41c5e33917c642 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples 10x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 10x.unitypackage.meta new file mode 100644 index 00000000..d4db3873 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 10x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9da5530d5ebfab24c8ecad68795e720f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples 12x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 12x.unitypackage.meta new file mode 100644 index 00000000..c3c98f27 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 12x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: aa102d640b98b5d4781710a3a3dd6983 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples 14x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 14x.unitypackage.meta new file mode 100644 index 00000000..ba827d29 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 14x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7a0bb33169d95ec499136d59cb25918b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples 15x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 15x.unitypackage.meta new file mode 100644 index 00000000..3c5ca728 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 15x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 641c955d37d2fac4f87e00ac5c9d9bd8 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples 16x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 16x.unitypackage.meta new file mode 100644 index 00000000..249f4ab1 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 16x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2690f45490c175045bbdc63395bf6278 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.0.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.0.unitypackage.meta new file mode 100644 index 00000000..3ee0254d --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.0.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fbd1fd9b3a70fad429d1eaaa5799c2a5 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.1.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.1.unitypackage.meta new file mode 100644 index 00000000..4557a1b4 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.1.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3579d9cf4b75c564faa8fffc58a9f3f6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.2.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.2.unitypackage.meta new file mode 100644 index 00000000..677da0f0 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples 17.2.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0023a0858ba124646a55dfcb7231ed46 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/HDRP Samples.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/HDRP Samples.unitypackage.meta new file mode 100644 index 00000000..872a1e1a --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/HDRP Samples.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d1c0b77896049554fa4b635531caf741 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/Sample Resources.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/Sample Resources.unitypackage.meta new file mode 100644 index 00000000..9d944382 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/Sample Resources.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c0a0a980c9ba86345bc15411db88d34f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples 10x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples 10x.unitypackage.meta new file mode 100644 index 00000000..819d3c7d --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples 10x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2edbf4a9b9544774bbef617e92429664 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples 12x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples 12x.unitypackage.meta new file mode 100644 index 00000000..9c0d665d --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples 12x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 13ab599a7bda4e54fba3e92a13c9580a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples 14x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples 14x.unitypackage.meta new file mode 100644 index 00000000..624d8621 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples 14x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f6f268949ccf3f34fa4d18e92501ed82 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples 15x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples 15x.unitypackage.meta new file mode 100644 index 00000000..7e2a8a41 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples 15x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 69bc3229216b1504ea3e28b5820bbb0d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples 16x.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples 16x.unitypackage.meta new file mode 100644 index 00000000..9d2cb068 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples 16x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4f665a06c5a2aa5499fa1c79ac058999 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples 17.0.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples 17.0.unitypackage.meta new file mode 100644 index 00000000..aa70a0f5 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples 17.0.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8a87ed432fe2d97498c0de5fae312e35 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples 17.1.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples 17.1.unitypackage.meta new file mode 100644 index 00000000..013c35c6 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples 17.1.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7c3bfbbeb9427b94099254e2e2768ad4 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples 17.2.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples 17.2.unitypackage.meta new file mode 100644 index 00000000..42051740 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples 17.2.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c5303861611f41c438a30be552da5de4 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Examples/URP Samples.unitypackage.meta b/Assets/AmplifyShaderEditor/Examples/URP Samples.unitypackage.meta new file mode 100644 index 00000000..da8ce241 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Examples/URP Samples.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a9d68dd8913f05d4d9ce75e7b40c6044 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 10x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 10x.unitypackage.meta new file mode 100644 index 00000000..602bd09a --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 10x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2243c8b4e1ab6914995699133f67ab5a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 12x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 12x.unitypackage.meta new file mode 100644 index 00000000..e51ee6d4 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 12x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9a5e61a8b3421b944863d0946e32da0a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 14x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 14x.unitypackage.meta new file mode 100644 index 00000000..82a2ab0e --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 14x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 89f0b84148d149d4d96b838d7ef60e92 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 15x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 15x.unitypackage.meta new file mode 100644 index 00000000..54836f2b --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 15x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 19939ee2cdb76e0489b1b8cd4bed7f3d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 16x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 16x.unitypackage.meta new file mode 100644 index 00000000..65b98bd3 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 16x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 70777e8ce9f3c8d4a8182ca2f965cdb2 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.0.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.0.unitypackage.meta new file mode 100644 index 00000000..c32e9f8f --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.0.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ba281a1a00c8ac54c914e0763299f637 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.1.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.1.unitypackage.meta new file mode 100644 index 00000000..0a18ad9c --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.1.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e6fc8948257acee42b666d0bfe1d782c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.2.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.2.unitypackage.meta new file mode 100644 index 00000000..314aa79b --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/HDRP 17.2.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4b5cb8698f2d9c14fadf8e2383441d37 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 10x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 10x.unitypackage.meta new file mode 100644 index 00000000..34f23836 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 10x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b460b52e6c1feae45b70b7ddc2c45bd6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 12x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 12x.unitypackage.meta new file mode 100644 index 00000000..04c4f886 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 12x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 57fcea0ed8b5eb347923c4c21fa31b57 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 14x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 14x.unitypackage.meta new file mode 100644 index 00000000..d5cd3087 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 14x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2e9da72e7e3196146bf7d27450013734 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 15x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 15x.unitypackage.meta new file mode 100644 index 00000000..0b6cd6d0 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 15x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0904cdf24ddcd5042b024326476220d5 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 16x.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 16x.unitypackage.meta new file mode 100644 index 00000000..2b33d7ab --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 16x.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 929783250050f8a448821b6ca1f2c578 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.0.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.0.unitypackage.meta new file mode 100644 index 00000000..e0b5ad87 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.0.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fcc4d2eb0af82e546ae75506872cf092 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.1.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.1.unitypackage.meta new file mode 100644 index 00000000..6338d388 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.1.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cd0a0171c5157b748afe763b89f71211 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.2.unitypackage.meta b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.2.unitypackage.meta new file mode 100644 index 00000000..47f972a0 --- /dev/null +++ b/Assets/AmplifyShaderEditor/Plugins/EditorResources/Templates/URP 17.2.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f4990f6ace6142c4bbbf41cdd80b0bd3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Built-in Pipeline.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Built-in Pipeline.unitypackage.meta new file mode 100644 index 00000000..d4c3ffb4 --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Built-in Pipeline.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1782b72cd0e99a54fac09382c482e3db +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 2022.3+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 2022.3+.unitypackage.meta new file mode 100644 index 00000000..378f3f8d --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 2022.3+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 49d5bcbbd4cbd754b98cf3200197b0f1 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.0+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.0+.unitypackage.meta new file mode 100644 index 00000000..2d5423a9 --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.0+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: eefe45a405f061045be947217e30ed10 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.1+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.1+.unitypackage.meta new file mode 100644 index 00000000..ccb4a903 --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.1+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2e995dfe11e22d34d92432383d15c067 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.2+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.2+.unitypackage.meta new file mode 100644 index 00000000..03ead5d2 --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.2+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ec984c51d4ae2504184eeb292734c672 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.3+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.3+.unitypackage.meta new file mode 100644 index 00000000..767ec46b --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/High Definition 6000.3+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e447b3d7d913d694ca35f74e30581840 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 2022.3+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 2022.3+.unitypackage.meta new file mode 100644 index 00000000..3502d9ed --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 2022.3+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6584a66582083a1459dcf5e4e87f6d62 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.0+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.0+.unitypackage.meta new file mode 100644 index 00000000..615e9057 --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.0+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0190b8bde50f12943926613d9a63c89a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.1+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.1+.unitypackage.meta new file mode 100644 index 00000000..9835f913 --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.1+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ae2ce8ad295486349839288636aed1ed +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.2+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.2+.unitypackage.meta new file mode 100644 index 00000000..1002df1c --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.2+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d69745226619e3241a8e04ce86aee6a6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.3+.unitypackage.meta b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.3+.unitypackage.meta new file mode 100644 index 00000000..98204661 --- /dev/null +++ b/Assets/External VFXs/BOXOPHOBIC/Atmospheric Height Fog/Core/Pipelines/Universal 6000.3+.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 37e6a9374416bc946a55779c58d0d984 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/FR2_Cache.asset b/Assets/FR2_Cache.asset index f2b2a29e..7179470a 100644 --- a/Assets/FR2_Cache.asset +++ b/Assets/FR2_Cache.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79e7db0058671de2ea0e17b331d0d8462622995f71cbdabeb7e714b5347ce104 -size 8743187 +oid sha256:b359099b033971eef02329b388e932dcb183b53e1d369d2a274fbdd8ec33eda2 +size 8774819 diff --git a/Assets/Le Tai's Asset/TranslucentImage/URP support.unitypackage.meta b/Assets/Le Tai's Asset/TranslucentImage/URP support.unitypackage.meta new file mode 100644 index 00000000..9e552100 --- /dev/null +++ b/Assets/Le Tai's Asset/TranslucentImage/URP support.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e45363610a59a4543a9793b3bf2be4aa +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Easy Save 3/Editor/ES3Asmdefs.unitypackage.meta b/Assets/Plugins/Easy Save 3/Editor/ES3Asmdefs.unitypackage.meta new file mode 100644 index 00000000..550f254c --- /dev/null +++ b/Assets/Plugins/Easy Save 3/Editor/ES3Asmdefs.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: de41a70ead12af544a4e99a925b269d2 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Sirenix/Demos/Custom Attribute Processors.unitypackage.meta b/Assets/Plugins/Sirenix/Demos/Custom Attribute Processors.unitypackage.meta new file mode 100644 index 00000000..1eadf09a --- /dev/null +++ b/Assets/Plugins/Sirenix/Demos/Custom Attribute Processors.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f597f19f656ba56eae4f6a3a7cc528f4 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Sirenix/Demos/Custom Drawers.unitypackage.meta b/Assets/Plugins/Sirenix/Demos/Custom Drawers.unitypackage.meta new file mode 100644 index 00000000..f073d2ab --- /dev/null +++ b/Assets/Plugins/Sirenix/Demos/Custom Drawers.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 48e08dc33330d11e9d4a1b246c52e4f6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Sirenix/Demos/Editor Windows.unitypackage.meta b/Assets/Plugins/Sirenix/Demos/Editor Windows.unitypackage.meta new file mode 100644 index 00000000..130722af --- /dev/null +++ b/Assets/Plugins/Sirenix/Demos/Editor Windows.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ed09910c0094cb27be8f3ca264680da3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Sirenix/Demos/Sample - RPG Editor.unitypackage.meta b/Assets/Plugins/Sirenix/Demos/Sample - RPG Editor.unitypackage.meta new file mode 100644 index 00000000..f9b8ed82 --- /dev/null +++ b/Assets/Plugins/Sirenix/Demos/Sample - RPG Editor.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cc355dd4cf1e6173beaeb22c2858cbe1 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/GameElements/Track/ParticleTracker.prefab b/Assets/Prefabs/GameElements/Track/ParticleTracker.prefab index 65ddea1d..c6fc6a6b 100644 --- a/Assets/Prefabs/GameElements/Track/ParticleTracker.prefab +++ b/Assets/Prefabs/GameElements/Track/ParticleTracker.prefab @@ -49,7 +49,7 @@ MonoBehaviour: m_EditorClassIdentifier: updateMethod: 2 _spline: {fileID: 0} - _autoUpdate: 1 + _autoUpdate: 0 _rotationModifier: blend: 1 useClippedPercent: 0 @@ -67,19 +67,19 @@ MonoBehaviour: useClippedPercent: 0 keys: [] _clipFromSample: - position: {x: -717.52625, y: 487.53882, z: 0} - up: {x: 0, y: 0, z: -1} - forward: {x: 0.028753094, y: 0.9943591, z: -0.10209477} - color: {r: 1, g: 1, b: 1, a: 1} - size: 1 + position: {x: 0, y: 0, z: 0} + up: {x: 0, y: 0, z: 0} + forward: {x: 0, y: 0, z: 0} + color: {r: 0, g: 0, b: 0, a: 0} + size: 0 percent: 0 _clipToSample: - position: {x: -689.4071, y: 502.93277, z: 4.664875} - up: {x: 0, y: 0, z: -1} - forward: {x: -0.39787814, y: 0.8626521, z: 0.3122889} - color: {r: 1, g: 1, b: 1, a: 1} - size: 1 - percent: 1 + position: {x: 0, y: 0, z: 0} + up: {x: 0, y: 0, z: 0} + forward: {x: 0, y: 0, z: 0} + color: {r: 0, g: 0, b: 0, a: 0} + size: 0 + percent: 0 _loopSamples: 0 _clipFrom: 0 _clipTo: 1 @@ -4919,14 +4919,30 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: serializationData: - SerializedFormat: 0 - SerializedBytes: 29010b00000065006c0065006d0065006e00740047007500690064000000000000000000000000000000000001010d0000007300750062006d006f00640075006c0065004c006900730074002f00000000015e000000530079007300740065006d002e0043006f006c006c0065006300740069006f006e0073002e00470065006e0065007200690063002e004c00690073007400600031005b005b004900630068006e0069002e00520068007900740068006d00470061006d0065002e005300750062006d006f00640075006c00650042006100730065002c00200041007300730065006d0062006c0079002d004300530068006100720070005d005d002c0020006d00730063006f0072006c0069006200000000000600000000000000000705 + SerializedFormat: 2 + SerializedBytes: ReferencedUnityObjects: [] SerializedBytesString: Prefab: {fileID: 0} PrefabModificationsReferencedUnityObjects: [] PrefabModifications: [] - SerializationNodes: [] + SerializationNodes: + - Name: elementGuid + Entry: 2 + Data: 00000000000000000000000000000000 + - Name: submoduleList + Entry: 7 + Data: 0|System.Collections.Generic.List`1[[Ichni.RhythmGame.SubmoduleBase, + Assembly-CSharp]], mscorlib + - Name: + Entry: 12 + Data: 0 + - Name: + Entry: 13 + Data: + - Name: + Entry: 8 + Data: elementName: tags: [] parentElement: {fileID: 0} diff --git a/Assets/Prefabs/Story/TutorialBlockUI.prefab b/Assets/Prefabs/Story/TutorialBlockUI.prefab index 9be2c031..9b0f1c64 100644 --- a/Assets/Prefabs/Story/TutorialBlockUI.prefab +++ b/Assets/Prefabs/Story/TutorialBlockUI.prefab @@ -10,7 +10,6 @@ GameObject: m_Component: - component: {fileID: 4937946904652960982} - component: {fileID: 459525017163665976} - - component: {fileID: 8785710776160535657} m_Layer: 5 m_Name: OutPort m_TagString: Untagged @@ -34,7 +33,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 1, y: 0.5} m_AnchorMax: {x: 1, y: 0.5} - m_AnchoredPosition: {x: -15, y: -5} + m_AnchoredPosition: {x: -30, y: -9.5} m_SizeDelta: {x: 25, y: 25} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &459525017163665976 @@ -45,36 +44,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2549272938280010857} m_CullTransparentMesh: 1 ---- !u!114 &8785710776160535657 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2549272938280010857} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 0, b: 0, a: 1} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 0} - m_Type: 0 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 --- !u!1 &2947357181438598616 GameObject: m_ObjectHideFlags: 0 @@ -205,7 +174,6 @@ GameObject: m_Component: - component: {fileID: 5784757909052703056} - component: {fileID: 3000483006876940928} - - component: {fileID: 3450002000089659239} m_Layer: 5 m_Name: InPort m_TagString: Untagged @@ -229,7 +197,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0.5} m_AnchorMax: {x: 0, y: 0.5} - m_AnchoredPosition: {x: 35, y: -6} + m_AnchoredPosition: {x: 72, y: -9.5} m_SizeDelta: {x: 25, y: 25} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &3000483006876940928 @@ -240,36 +208,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 5694744930588099522} m_CullTransparentMesh: 1 ---- !u!114 &3450002000089659239 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5694744930588099522} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 0, g: 0, b: 1, a: 1} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 0} - m_Type: 0 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 --- !u!1 &7991069327751894070 GameObject: m_ObjectHideFlags: 0 @@ -374,20 +312,23 @@ MonoBehaviour: m_VerticalAlignment: 512 m_textAlignment: 65535 m_characterSpacing: 0 + m_characterHorizontalScale: 1 m_wordSpacing: 0 m_lineSpacing: 0 m_lineSpacingMax: 0 m_paragraphSpacing: 0 m_charWidthMaxAdj: 0 - m_enableWordWrapping: 1 + m_TextWrappingMode: 1 m_wordWrappingRatios: 0.4 m_overflowMode: 0 m_linkedTextComponent: {fileID: 0} parentLinkedComponent: {fileID: 0} m_enableKerning: 1 + m_ActiveFontFeatures: 6e72656b m_enableExtraPadding: 0 checkPaddingRequired: 0 m_isRichText: 1 + m_EmojiFallbackSupport: 1 m_parseCtrlCharacters: 1 m_isOrthographic: 1 m_isCullingEnabled: 0 @@ -415,7 +356,7 @@ GameObject: m_Component: - component: {fileID: 5350638854831129010} - component: {fileID: 6895756204793606274} - - component: {fileID: 3620540326085937893} + - component: {fileID: 1599109356054254412} m_Layer: 5 m_Name: TutorialBlockUI m_TagString: Untagged @@ -441,9 +382,9 @@ RectTransform: - {fileID: 4937946904652960982} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 0, y: -540} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 600, y: 375} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &6895756204793606274 @@ -454,7 +395,7 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 8279096577214194803} m_CullTransparentMesh: 1 ---- !u!114 &3620540326085937893 +--- !u!114 &1599109356054254412 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -463,15 +404,22 @@ MonoBehaviour: m_GameObject: {fileID: 8279096577214194803} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 821e50e1519236a46b03eb1f005acebe, type: 3} + m_Script: {fileID: 11500000, guid: 66b3d929576d6c54ca51069ae0e313e5, type: 3} m_Name: - m_EditorClassIdentifier: - blockName: - blockPosition: {x: 0, y: 0} + m_EditorClassIdentifier: Assembly-CSharp::Ichni.Story.UI.TutorialBlockView + serializationData: + SerializedFormat: 2 + SerializedBytes: + ReferencedUnityObjects: [] + SerializedBytesString: + Prefab: {fileID: 0} + PrefabModificationsReferencedUnityObjects: [] + PrefabModifications: [] + SerializationNodes: [] + blockId: state: 0 blockRect: {fileID: 5350638854831129010} inPort: {fileID: 5784757909052703056} outPort: {fileID: 4937946904652960982} button: {fileID: 3931523710101296998} - tutorialName: tutorialNameText: {fileID: 8391808794720387191} diff --git a/Assets/Resources/Chapter0.asset b/Assets/Resources/Chapter0.asset index 8846b73e..1c974ff0 100644 --- a/Assets/Resources/Chapter0.asset +++ b/Assets/Resources/Chapter0.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2309890c6fdbd15a650823b7026c920dcf3efec3b3588b311adec24b7b35a904 -size 16200 +oid sha256:c7ec334ce8848f2e927287f8186bcbbac7e0dcd9e582f5824bf9fa4503b6a2dd +size 17506 diff --git a/Assets/Resources/TutorialCollection.asset b/Assets/Resources/TutorialCollection.asset index c255ac6b..be733baf 100644 --- a/Assets/Resources/TutorialCollection.asset +++ b/Assets/Resources/TutorialCollection.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86c4261a4c15a369410dbc5fd72a38d0ed5db1a8bac82143122631b305af2c66 -size 3766 +oid sha256:735bc731ac19917b2481c80b09db44a8fa89d2b67faeea7a8e1f1ad0b848abdf +size 3840 diff --git a/Assets/Scenes/MenuScene.unity b/Assets/Scenes/MenuScene.unity index 403cebe9..e18de68e 100644 --- a/Assets/Scenes/MenuScene.unity +++ b/Assets/Scenes/MenuScene.unity @@ -16859,6 +16859,8 @@ MonoBehaviour: charterName: color: {r: 0, g: 0, b: 0, a: 0} isAvailable: 0 + ForceFullScreenJudge: 0 + chartRevision: 1 songLength: 0 bpm: 0 chapterSwitch: @@ -20474,7 +20476,7 @@ MonoBehaviour: backgroundFrames: {fileID: 2007806589} framePositionOffset: -1 songListController: {fileID: 886518565} - playSongUI: {fileID: 0} + playSongUI: {fileID: 2067971169} songInfoUI: {fileID: 1283588608} difficultySelectionContainer: {fileID: 1665548623} selectedSong: @@ -20501,6 +20503,8 @@ MonoBehaviour: charterName: color: {r: 0, g: 0, b: 0, a: 0} isAvailable: 0 + ForceFullScreenJudge: 0 + chartRevision: 1 --- !u!4 &1506872042 Transform: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Game/Animations/Camera/CameraFieldOfView.cs b/Assets/Scripts/Game/Animations/Camera/CameraFieldOfView.cs index 8712ba33..4ba6531d 100644 --- a/Assets/Scripts/Game/Animations/Camera/CameraFieldOfView.cs +++ b/Assets/Scripts/Game/Animations/Camera/CameraFieldOfView.cs @@ -45,7 +45,7 @@ namespace Ichni.RhythmGame { targetGameCamera.perspectiveAngle = fieldOfView.value; targetGameCamera.RefreshFOV(); - Debug.Log(targetGameCamera.cam.fieldOfView + " " + fieldOfView.value); + //Debug.Log(targetGameCamera.cam.fieldOfView + " " + fieldOfView.value); } } diff --git a/Assets/Scripts/Manager/GameManager.cs b/Assets/Scripts/Manager/GameManager.cs index 2ced17f7..9b21f93e 100644 --- a/Assets/Scripts/Manager/GameManager.cs +++ b/Assets/Scripts/Manager/GameManager.cs @@ -66,6 +66,7 @@ namespace Ichni CoreServices.TimeProvider = this; timeDurations = new List(); playingRecorder = new PlayingRecorder(); + playingRecorder.Initialize(); updateScheduler = new ElementUpdateScheduler(); CoreServices.UpdateScheduler = updateScheduler; } @@ -89,17 +90,47 @@ namespace Ichni public partial class GameManager { + /// + /// 将当前演奏结果写入独立的游戏记录存档。 + /// 由 SongPlayer 在实际歌曲结束后、打开 Summary 前调用。 + /// 剧情树、选项和 Yarn 变量仍由 单独管理。 + /// 当前 Difficulty 的 Chart Revision 会一并传入,从而在谱面改版时只清除不可比较的单谱面成绩。 + /// + public bool SaveCurrentPlayRecord() + { + if (GameSaveManager.instance == null || GameSaveManager.instance.SongSaveModule == null) + { + Debug.LogWarning("Cannot save play record because SongSaveModule is not initialized."); + return false; + } + + if (InformationTransistor.instance == null || + InformationTransistor.instance.song == null || + InformationTransistor.instance.difficulty == null) + { + Debug.LogWarning("Cannot save play record because the selected song or difficulty is missing."); + return false; + } + + return GameSaveManager.instance.SongSaveModule.RecordPlayResult( + InformationTransistor.instance.song.songName, + InformationTransistor.instance.difficulty.saveDifficultyId, + InformationTransistor.instance.difficulty.GetChartRevision(), + playingRecorder); + } + public static void RestartGame() { + // 保留 InformationTransistor 中的歌曲和返回目标,使教程与普通歌曲的重开都回到正确页面。 SceneManager.LoadScene("GameScene"); Time.timeScale = 1f; // 确保重启时时间缩放恢复正常 } public static void ReturnToMenu() { - //InformationTransistor.instance.isReturnedFromGame = true; - SceneManager.LoadScene("MenuScene"); Time.timeScale = 1f; // 确保返回时时间缩放恢复正常 + // 返回目标已在进入 GameScene 时写入 InformationTransistor;这里不再写互斥布尔值。 + SceneManager.LoadScene("MenuScene"); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Manager/PlayingRecorder.cs b/Assets/Scripts/Manager/PlayingRecorder.cs index 07a360c5..2b48d0ef 100644 --- a/Assets/Scripts/Manager/PlayingRecorder.cs +++ b/Assets/Scripts/Manager/PlayingRecorder.cs @@ -4,6 +4,11 @@ using UnityEngine; namespace Ichni.RhythmGame { + /// + /// 单次演奏的运行时判定记录。 + /// 本类随 GameScene 重建,只负责本局结算;长期最佳成绩由 SongSaveModule 写入 ES3, + /// 因此不要把此对象直接当作存档模型使用。 + /// public class PlayingRecorder { public int perfectCount; @@ -17,13 +22,28 @@ namespace Ichni.RhythmGame public int currentCombo; public int maxCombo; + /// 本局判定偏差图数据,仅供当前 Summary 使用,不写入存档。 public List resultData = new List(); public bool isFullCombo; public bool isAllPerfect; + /// + /// 在进入一局新演奏时重置全部瞬时状态。 + /// 初始化为 100% / FC / AP 是为了让首个非 Perfect 判定能正确打破对应状态; + /// 无有效判定的空局不会被 SongSaveModule 写入成绩。 + /// public void Initialize() { + if (resultData == null) + { + resultData = new List(); + } + else + { + resultData.Clear(); + } + perfectCount = 0; goodCount = 0; badCount = 0; @@ -105,4 +125,4 @@ namespace Ichni.RhythmGame GameManager.Instance.gameUICanvas.UpdateCombo(currentCombo); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Manager/SongPlayer.cs b/Assets/Scripts/Manager/SongPlayer.cs index d60d9474..cba89600 100644 --- a/Assets/Scripts/Manager/SongPlayer.cs +++ b/Assets/Scripts/Manager/SongPlayer.cs @@ -170,7 +170,14 @@ namespace Ichni if (in_type == AkCallbackType.AK_MusicSyncExit) { + if (isFinished) + { + return; + } + isFinished = true; + // 先持久化再展示结算,确保玩家即使在 Summary 停留期间退出也不会丢失本局成绩。 + GameManager.Instance.SaveCurrentPlayRecord(); GameManager.Instance.summaryPageCanvas.SetUpSummary(); GameManager.Instance.summaryPageCanvas.FadeIn(); } @@ -184,4 +191,4 @@ namespace Ichni return segmentInfo.iCurrentPosition; } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Menu/ChapterSelection/ChapterSelectionUnit.cs b/Assets/Scripts/Menu/ChapterSelection/ChapterSelectionUnit.cs index 38955bdb..1aa6f854 100644 --- a/Assets/Scripts/Menu/ChapterSelection/ChapterSelectionUnit.cs +++ b/Assets/Scripts/Menu/ChapterSelection/ChapterSelectionUnit.cs @@ -21,6 +21,14 @@ namespace Ichni.Menu public Sprite avatar; public Switch chapterSwitch; + /// + /// 章节剧情入口与选曲入口共用的内容解锁规则。 + /// 留空表示章节从新档开始即可访问;需要锁定时请使用 , + /// 并遵守 中的下划线 Key 规则。 + /// + [LabelText("Unlock Requirement")] + public UnlockRequirement unlockRequirement = new UnlockRequirement(); + [Searchable] public List songs = new List(); @@ -54,11 +62,6 @@ namespace Ichni.Menu public partial class ChapterSelectionUnit { - public List GetRelatedSongNamesOfUnlockKey(string key) - { - return (from song in songs where song.storyUnlockKey == key select song.songName).ToList(); - } - public (int, int, int, int, int) GetChapterSaveInfo() { int beatmapCount = 0; @@ -72,9 +75,19 @@ namespace Ichni.Menu if (GameSaveManager.instance.SongSaveModule.songStatusSaves.TryGetValue(song.songName, out var songStatus)) { bool thisSongFinished = false; - foreach (BeatmapSave beatmapSave in songStatus.beatmapSaves) + foreach (DifficultyData difficulty in song.difficultyDataList) { + if (difficulty == null || difficulty.saveDifficultyId < 0) + { + continue; + } + beatmapCount++; + if (!songStatus.TryGetBeatmapSave(difficulty.saveDifficultyId, out BeatmapSave beatmapSave)) + { + continue; + } + if (!beatmapSave.IsEmpty()) { thisSongFinished = true; @@ -131,17 +144,24 @@ namespace Ichni.Menu [FoldoutGroup("$songName")] public List difficultyDataList; + /// + /// 歌曲自身的内容解锁规则。章节是否开放由 单独判断; + /// 真正进入歌曲时两者必须同时满足。 + /// [FoldoutGroup("$songName")] - public string storyUnlockKey; - - [FoldoutGroup("$songName")] - public string paymentUnlockKey; + [LabelText("Unlock Requirement")] + public UnlockRequirement unlockRequirement = new UnlockRequirement(); } [Serializable] public class DifficultyData { - public int difficultyIndex; + /// + /// 歌曲成绩的稳定 ID,不等同于选曲 UI 的难度列表位置。 + /// 发布后不要更改或复用既有 ID;新增难度必须使用从未分配过的非负 ID。 + /// + [FormerlySerializedAs("difficultyIndex")] + public int saveDifficultyId; public string difficultyName; public string displayDifficultyName; public int difficultyValue; @@ -149,14 +169,19 @@ namespace Ichni.Menu public Color color; public bool isAvailable; public bool ForceFullScreenJudge = false; + + [MinValue(1)] + [Tooltip("仅当 Note、Timing 或判定规则变化,导致成绩不可与旧谱面比较时递增;UI、文本、封面和音量调整不递增。")] + public int chartRevision = 1; + public DifficultyData() { } - public DifficultyData(int difficultyIndex, string difficultyName, string displayDifficultyName, int difficultyValue, string charterName, Color color, bool ForceFullScreenJudge = false) + public DifficultyData(int saveDifficultyId, string difficultyName, string displayDifficultyName, int difficultyValue, string charterName, Color color, bool ForceFullScreenJudge = false) { - this.difficultyIndex = difficultyIndex; + this.saveDifficultyId = saveDifficultyId; this.difficultyName = difficultyName; this.displayDifficultyName = displayDifficultyName; this.charterName = charterName; @@ -170,5 +195,13 @@ namespace Ichni.Menu { return string.IsNullOrEmpty(displayDifficultyName) ? difficultyName : displayDifficultyName; } + + /// + /// 返回合法的 Revision。配置侧始终至少使用 1;0 只会被视为未初始化值并被钳制。 + /// + public int GetChartRevision() + { + return Mathf.Max(1, chartRevision); + } } } diff --git a/Assets/Scripts/Menu/InformationTransistor.cs b/Assets/Scripts/Menu/InformationTransistor.cs index cd61b8b2..47b08915 100644 --- a/Assets/Scripts/Menu/InformationTransistor.cs +++ b/Assets/Scripts/Menu/InformationTransistor.cs @@ -7,12 +7,26 @@ using UnityEngine.Serialization; namespace Ichni { + /// + /// GameScene 返回 MenuScene 后应恢复的页面。 + /// 该枚举替代互斥的布尔标记,避免普通歌曲与教程返回状态同时残留。 + /// + public enum MenuReturnDestination + { + None, + SongSelection, + Story + } + public class InformationTransistor : MonoBehaviour { public static InformationTransistor instance; - - public bool isReturnedFromGame; - public bool isReturnedFromTutorial; + + /// + /// 当前游戏运行结束后,MenuScene 应恢复的唯一目标页面。 + /// 运行期间保存在 DontDestroyOnLoad 对象中,不写入玩家存档。 + /// + public MenuReturnDestination menuReturnDestination; public ChapterSelectionUnit chapter; public SongItemData song; @@ -30,8 +44,7 @@ namespace Ichni { instance = this; DontDestroyOnLoad(gameObject); - isReturnedFromGame = false; - isReturnedFromTutorial = false; + menuReturnDestination = MenuReturnDestination.None; } else { @@ -42,11 +55,48 @@ namespace Ichni public void SetInformation(ChapterSelectionUnit chapter, SongItemData song, DifficultyData difficulty) { + // 保留旧入口,供现有选曲流程在播放过场前预先写入歌曲信息。 + // 真正进入 GameScene 时,MenuManager.EnterGame 会补充 SongSelection 返回目标。 + PrepareGameLaunch(chapter, song, difficulty, MenuReturnDestination.None); + } + + /// + /// 统一准备一次 GameScene 运行所需的上下文。 + /// 普通选曲和 TutorialBlock 都必须通过此入口写入,防止章节、歌曲、难度与返回目标来自不同旧运行。 + /// + public bool PrepareGameLaunch(ChapterSelectionUnit chapter, SongItemData song, + DifficultyData difficulty, MenuReturnDestination returnDestination) + { + if (chapter == null || song == null || difficulty == null) + { + Debug.LogWarning("[InformationTransistor] 缺少章节、歌曲或难度,无法准备 GameScene 上下文。"); + return false; + } + this.chapter = chapter; this.song = song; this.difficulty = difficulty; - this.chapterSwitch = chapter.chapterSwitch; - this.songSwitch = song.songSwitch; + chapterSwitch = chapter.chapterSwitch; + songSwitch = song.songSwitch; + menuReturnDestination = returnDestination; + return true; + } + + /// + /// 由现有选曲过场在真正激活 GameScene 前设置返回目标。 + /// + public void SetMenuReturnDestination(MenuReturnDestination returnDestination) + { + menuReturnDestination = returnDestination; + } + + /// + /// MenuScene 已依据返回目标完成恢复后调用。 + /// 只清除一次性路由,不清除歌曲与难度,以免当前 UI 的运行时缓存被意外破坏。 + /// + public void ClearMenuReturnDestination() + { + menuReturnDestination = MenuReturnDestination.None; } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Menu/MenuInformationRecorder.cs b/Assets/Scripts/Menu/MenuInformationRecorder.cs index 0bebdce9..0d95384b 100644 --- a/Assets/Scripts/Menu/MenuInformationRecorder.cs +++ b/Assets/Scripts/Menu/MenuInformationRecorder.cs @@ -24,29 +24,70 @@ namespace Ichni.Menu } } - public SongSelectionRecord GetRecordOfThisChapter() + /// + /// 取得当前 Chapter 的选曲缓存;缓存缺失时创建“第一首有效歌曲 + 难度位置 0”的默认记录。 + /// 此方法只保证缓存对象可用,不保证该歌曲或难度仍能游玩。歌曲失效与难度回退由选曲 UI 在 + /// 每次进入页面时完成,避免把运行期缓存变成持久化存档。 + /// + public bool TryGetRecordOfThisChapter(out SongSelectionRecord record) { + record = null; ChapterSelectionUnit currentChapter = ChapterSelectionManager.instance.currentChapter; - if (songSelectionRecords.TryGetValue(currentChapter, out SongSelectionRecord record)) + if (currentChapter == null || currentChapter.songs == null) { - return record; + return false; } - else + + if (songSelectionRecords.TryGetValue(currentChapter, out SongSelectionRecord cachedRecord) && cachedRecord != null) { - return new SongSelectionRecord(currentChapter.songs[0], currentChapter.songs[0].difficultyDataList[0]); + record = cachedRecord; + return true; } + + SongItemData firstValidSong = currentChapter.songs.Find(song => song != null); + if (firstValidSong == null) + { + return false; + } + + record = new SongSelectionRecord(firstValidSong, 0); + songSelectionRecords[currentChapter] = record; + return true; + } + + /// + /// 将缓存修正为当前 Chapter 中实际存在的歌曲,并保留调用方提供的难度列表偏好位置。 + /// 这里只写内存缓存,玩家重启游戏后不会恢复该选择。 + /// + public void SetRecordForChapter(ChapterSelectionUnit chapter, SongItemData song, int difficultyListIndex) + { + if (chapter == null || song == null) + { + return; + } + + songSelectionRecords[chapter] = new SongSelectionRecord(song, difficultyListIndex); } } public class SongSelectionRecord { public SongItemData song; - public int difficultyIndex; - - public SongSelectionRecord(SongItemData song, DifficultyData difficulty) + public int difficultyListIndex; + + /// + /// 使用难度在歌曲列表中的位置构建缓存。 + /// 这是选曲 UI 的临时偏好,不等同于 DifficultyData.saveDifficultyId 的游戏记录稳定 ID。 + /// + public SongSelectionRecord(SongItemData song, int difficultyListIndex) { this.song = song; - difficultyIndex = song.difficultyDataList.IndexOf(difficulty); + this.difficultyListIndex = difficultyListIndex; + } + + public SongSelectionRecord(SongItemData song, DifficultyData difficulty) + : this(song, song?.difficultyDataList?.IndexOf(difficulty) ?? 0) + { } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Menu/MenuManager.cs b/Assets/Scripts/Menu/MenuManager.cs index ca2a1717..99629175 100644 --- a/Assets/Scripts/Menu/MenuManager.cs +++ b/Assets/Scripts/Menu/MenuManager.cs @@ -3,10 +3,13 @@ using System.Collections; using System.Collections.Generic; using AK.Wwise; using Ichni.Menu; +using Ichni.Menu.UI; +using Ichni.Story; using Ichni.Story.UI; using Ichni.UI; using Sirenix.OdinInspector; using SLSUtilities.WwiseAssistance; +using UniRx; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.Serialization; @@ -21,6 +24,7 @@ namespace Ichni public LoginPage loginPage; public ChapterSelectionUIPage chapterSelectionUIPage; public StoryUIPage storyUIPage; + public SelectionBoxUIPage selectionBoxUIPage; // dialogUIPage 已随旧对话系统移除;阶段 3 将接入新的 VN 对话页面引用 public SongSelectionUIPage songSelectionUIPage; public TransitionUIPage transitionUIPage; @@ -34,6 +38,12 @@ namespace Ichni { AsyncOperation asyncOperation; public bool isEnteringGame; + + /// + /// 当前 MenuScene 是否已经完成 GameScene 预加载且尚未处于切场中。 + /// TutorialFlowController 在写入“已处理”剧情变量前使用此状态进行最后一次启动前检查。 + /// + public bool CanBeginGame => !isEnteringGame && asyncOperation != null; private void Awake() { @@ -44,24 +54,39 @@ namespace Ichni { isEnteringGame = false; - if (InformationTransistor.instance.isReturnedFromGame) + InformationTransistor information = InformationTransistor.instance; + MenuReturnDestination returnDestination = information != null + ? information.menuReturnDestination + : MenuReturnDestination.None; + + if (returnDestination == MenuReturnDestination.SongSelection && + information != null && information.chapter != null && + information.song != null && information.difficulty != null) { startUIPage.mainCanvasGroup.gameObject.SetActive(false); ChapterSelectionManager.instance.currentChapter = - InformationTransistor.instance.chapter; + information.chapter; AudioManager.SetSwitch(ChapterSelectionManager.instance.currentChapter.chapterSwitch); - MenuInformationRecorder.instance.songSelectionRecords.Add( - ChapterSelectionManager.instance.currentChapter, - new SongSelectionRecord(InformationTransistor.instance.song, InformationTransistor.instance.difficulty)); + MenuInformationRecorder.instance.songSelectionRecords[ + ChapterSelectionManager.instance.currentChapter] = + new SongSelectionRecord(information.song, information.difficulty); songSelectionUIPage.FadeIn(); } - else if (InformationTransistor.instance.isReturnedFromTutorial) + else if (returnDestination == MenuReturnDestination.Story && + information != null && information.chapter != null) { startUIPage.mainCanvasGroup.gameObject.SetActive(false); - ChapterSelectionManager.instance.currentChapter = InformationTransistor.instance.chapter; + ChapterSelectionManager.instance.currentChapter = information.chapter; AudioManager.SetSwitch(ChapterSelectionManager.instance.currentChapter.chapterSwitch); + + // MenuScene 会在教程游玩后重新创建,必须显式重建章节故事树, + // 不能依赖旧场景中残留的 Block 实例。 + StoryManager.instance?.OpenChapter(ChapterSelectionManager.instance.currentChapter.chapterIndex); storyUIPage.FadeIn(); } + + // 返回目标是一次性运行态信息。无论是否能恢复页面,都不能让它在下次重进菜单时重复生效。 + information?.ClearMenuReturnDestination(); Application.targetFrameRate = SettingsManager.instance.gameSettings.targetFrame; asyncOperation = SceneManager.LoadSceneAsync("GameScene"); @@ -73,15 +98,59 @@ namespace Ichni { public void EnterGame() { - InformationTransistor.instance.isReturnedFromTutorial = false; - InformationTransistor.instance.isReturnedFromGame = true; + EnterGame(MenuReturnDestination.SongSelection); + } + + /// + /// 激活已预加载的 GameScene,并写入唯一的菜单返回目标。 + /// 普通选曲使用 ,教程使用 + /// 。 + /// + public void EnterGame(MenuReturnDestination returnDestination) + { + InformationTransistor.instance?.SetMenuReturnDestination(returnDestination); EnterGameScene(); } + + /// + /// 为 TutorialBlock 等非选曲入口准备上下文并播放通用转场。 + /// 运行成功后会返回 true;返回 false 时不会修改剧情变量或存档,调用方应保留原状。 + /// + public bool TryBeginGame(ChapterSelectionUnit chapter, SongItemData song, + DifficultyData difficulty, MenuReturnDestination returnDestination, UIPageBase sourcePage) + { + if (!CanBeginGame) + { + Debug.LogWarning("[MenuManager] GameScene 未完成预加载或菜单已在切换,忽略启动请求。"); + return false; + } + + if (InformationTransistor.instance == null || + !InformationTransistor.instance.PrepareGameLaunch(chapter, song, difficulty, returnDestination)) + { + return false; + } + + isEnteringGame = true; + sourcePage?.FadeOut(); + transitionUIPage?.FadeIn(); + AudioManager.Post(AK.EVENTS.ENTERTOGAME); + + Observable.Timer(TimeSpan.FromSeconds(0.6f)).Subscribe(_ => EnterGameScene()); + return true; + } public void EnterGameScene() { - MenuInputManager.instance.gameInput.Menu.Disable(); + if (asyncOperation == null) + { + Debug.LogError("[MenuManager] GameScene 尚未完成预加载,无法进入游戏。"); + isEnteringGame = false; + return; + } + + MenuInputManager.instance?.gameInput?.Menu.Disable(); asyncOperation.allowSceneActivation = true; } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Menu/MessagePage.meta b/Assets/Scripts/Menu/MessagePage.meta new file mode 100644 index 00000000..fa4d8402 --- /dev/null +++ b/Assets/Scripts/Menu/MessagePage.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4029d987b6779e440b58bc5e9b457db1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/NewStorySystem/UI/StoryMessageBoxUIPage.cs b/Assets/Scripts/Menu/MessagePage/MessageUIPage.cs similarity index 97% rename from Assets/Scripts/NewStorySystem/UI/StoryMessageBoxUIPage.cs rename to Assets/Scripts/Menu/MessagePage/MessageUIPage.cs index 61db3d15..970c77db 100644 --- a/Assets/Scripts/NewStorySystem/UI/StoryMessageBoxUIPage.cs +++ b/Assets/Scripts/Menu/MessagePage/MessageUIPage.cs @@ -9,9 +9,9 @@ namespace Ichni.Story.UI /// 用于在剧情系统中管理并顺序展示 MessageBox 的独立 UI 页面。 /// 支持动态实例化给定的 MessageBox Prefab,并在所有消息展示完毕后进行大页面的 FadeOut。 /// - public class StoryMessageBoxUIPage : UIPageBase + public class MessageUIPage : UIPageBase { - public static StoryMessageBoxUIPage instance; + public static MessageUIPage instance; [Header("Prefabs & Containers")] [Tooltip("默认使用的 MessageBox 预制体(请从 Project 中拖拽,而非场景中的实例)")] @@ -97,6 +97,7 @@ namespace Ichni.Story.UI // 唤醒大页面(执行 UIPageBase 的渐入并拦截底层射线) this.FadeIn(0.5f, false, ShowNextInQueue); } + } /// diff --git a/Assets/Scripts/NewStorySystem/UI/StoryMessageBoxUIPage.cs.meta b/Assets/Scripts/Menu/MessagePage/MessageUIPage.cs.meta similarity index 100% rename from Assets/Scripts/NewStorySystem/UI/StoryMessageBoxUIPage.cs.meta rename to Assets/Scripts/Menu/MessagePage/MessageUIPage.cs.meta diff --git a/Assets/Scripts/Menu/Tutorial/TutorialCollection.cs b/Assets/Scripts/Menu/Tutorial/TutorialCollection.cs index b3aed34f..19c486b9 100644 --- a/Assets/Scripts/Menu/Tutorial/TutorialCollection.cs +++ b/Assets/Scripts/Menu/Tutorial/TutorialCollection.cs @@ -9,6 +9,35 @@ namespace Ichni.Menu [CreateAssetMenu(fileName = "TutorialCollection", menuName = "Ichni/TutorialCollection")] public class TutorialCollection : SerializedScriptableObject { + /// + /// 教程 Key 到教程曲目配置的映射。 + /// Key 必须使用小写英文、数字和下划线,例如 chapter0_intro; + /// 不要使用章节展示名、曲名或点号作为 Key。 + /// public Dictionary songs = new Dictionary(); + + /// + /// 按 TutorialBlock 配置的稳定 Key 查找教程曲目。 + /// 教程运行流程只通过此函数读取集合,避免各处直接访问字典而产生空引用或不一致的错误信息。 + /// + public bool TryGetTutorialSong(string tutorialKey, out SongItemData song) + { + song = null; + + if (string.IsNullOrWhiteSpace(tutorialKey)) + { + Debug.LogWarning("[TutorialCollection] Tutorial Key 为空,无法查找教程曲目。"); + return false; + } + + if (songs == null || !songs.TryGetValue(tutorialKey, out song) || song == null) + { + Debug.LogWarning($"[TutorialCollection] 未找到 Tutorial Key '{tutorialKey}' 对应的教程曲目。"); + song = null; + return false; + } + + return true; + } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/NewStorySystem/Data/StoryBlockDefinition.cs b/Assets/Scripts/NewStorySystem/Data/StoryBlockDefinition.cs index db998d7d..581b08a6 100644 --- a/Assets/Scripts/NewStorySystem/Data/StoryBlockDefinition.cs +++ b/Assets/Scripts/NewStorySystem/Data/StoryBlockDefinition.cs @@ -97,6 +97,36 @@ namespace Ichni.Story [LabelText("Tutorial Name")] public string tutorialName; + /// + /// 教程曲目在 中的稳定 Key。 + /// 只用于运行时查找,不使用展示名称或章节名称作为逻辑匹配条件。 + /// 命名统一使用小写英文、数字和下划线,例如 chapter0_intro。 + /// + [FoldoutGroup("$blockId")] + [ShowIf("blockType", StoryBlockType.Tutorial)] + [LabelText("Tutorial Key")] + public string tutorialKey; + + /// + /// 教程被玩家选择“游玩”或“跳过”后写入的全局剧情变量。 + /// 后续 Block 应通过 检查该变量是否等于 1 来解锁。 + /// 命名统一使用小写英文、数字和下划线,例如 + /// story_tutorial_chapter0_intro_resolved。 + /// + [FoldoutGroup("$blockId")] + [ShowIf("blockType", StoryBlockType.Tutorial)] + [LabelText("Progress Variable")] + public string tutorialProgressVariable; + + /// + /// 要启动的教程难度的稳定存档 ID,对应 。 + /// 不使用难度列表下标,避免调整难度排列后教程进入错误谱面。 + /// + [FoldoutGroup("$blockId")] + [ShowIf("blockType", StoryBlockType.Tutorial)] + [LabelText("Tutorial Difficulty Save ID")] + public int tutorialDifficultySaveId = -1; + /// /// 预览 / 调试用的显示标题:按类型取对应字段,缺省时回退到 blockId。 /// diff --git a/Assets/Scripts/NewStorySystem/Data/StoryVariables.cs b/Assets/Scripts/NewStorySystem/Data/StoryVariables.cs index afa5d2d1..5b6a1f46 100644 --- a/Assets/Scripts/NewStorySystem/Data/StoryVariables.cs +++ b/Assets/Scripts/NewStorySystem/Data/StoryVariables.cs @@ -94,6 +94,20 @@ namespace Ichni.Story Variables.stringVariables[key] = value; } + /// + /// 删除指定剧情变量在三种底层字典中的所有残留值。 + /// 仅删除内存值;需要持久化和故事树刷新时,应调用 的统一接口。 + /// + public static void Remove(string key) + { + if (string.IsNullOrWhiteSpace(key)) return; + + StoryVariablesSave vars = Variables; + vars.floatVariables.Remove(key); + vars.stringVariables.Remove(key); + vars.boolVariables.Remove(key); + } + public static void ClearAll() { StoryVariablesSave vars = Variables; diff --git a/Assets/Scripts/NewStorySystem/Dialogue/StoryDialogueController.cs b/Assets/Scripts/NewStorySystem/Dialogue/StoryDialogueController.cs index 7f24f3d1..04ad22e1 100644 --- a/Assets/Scripts/NewStorySystem/Dialogue/StoryDialogueController.cs +++ b/Assets/Scripts/NewStorySystem/Dialogue/StoryDialogueController.cs @@ -157,7 +157,7 @@ namespace Ichni.Story.Dialogue action?.Invoke(); } - StoryMessageBoxUIPage.instance.FadeIn(); + MessageUIPage.instance.FadeIn(); } if (storyUIPage != null) diff --git a/Assets/Scripts/NewStorySystem/Progress.meta b/Assets/Scripts/NewStorySystem/Progress.meta new file mode 100644 index 00000000..cc697f8a --- /dev/null +++ b/Assets/Scripts/NewStorySystem/Progress.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 90d61ae2c68e4a058826e1af1052cd3d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/NewStorySystem/Progress/StoryProgress.cs b/Assets/Scripts/NewStorySystem/Progress/StoryProgress.cs new file mode 100644 index 00000000..5eb2ab19 --- /dev/null +++ b/Assets/Scripts/NewStorySystem/Progress/StoryProgress.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace Ichni.Story +{ + /// + /// 外部玩法系统修改剧情进度的统一入口。 + /// + /// StoryVariables 只负责读写内存中的变量字典,不会自动落盘或刷新故事树。 + /// 本类将“写变量、保存变量、重算 Block 状态”收口,避免教程、小游戏等系统重复实现 + /// 不完整的存档流程。 + /// + public static class StoryProgress + { + /// + /// 写入布尔剧情变量、保存到 StorySaveModule,并立即重新推导当前故事树。 + /// 可供教程以外的玩法系统使用。 + /// + public static bool SetBoolAndRefresh(string variableName, bool value) + { + if (!TrySetBoolAndSave(variableName, value)) + return false; + + StoryManager.instance?.treeController?.RefreshAllStates(); + return true; + } + + /// + /// 处理一次 TutorialBlock 的选择结果。 + /// 无论选择“游玩”还是“跳过”,都会将其进度变量设为 true,并将教程节点标记为完成。 + /// 后续 Block 的解锁应依赖 , + /// 而不是依赖该节点的 completedBlockIds,以便和 Yarn 及其它剧情变量共用同一条件系统。 + /// + public static bool ResolveTutorial(StoryBlockDefinition tutorialBlock) + { + if (tutorialBlock == null || tutorialBlock.blockType != StoryBlockType.Tutorial) + { + Debug.LogWarning("[StoryProgress] 无法处理空对象或非 Tutorial 类型的 Block。"); + return false; + } + + if (string.IsNullOrWhiteSpace(tutorialBlock.blockId) || + string.IsNullOrWhiteSpace(tutorialBlock.tutorialProgressVariable)) + { + Debug.LogWarning($"[StoryProgress] TutorialBlock '{tutorialBlock?.blockId}' 缺少 Block ID 或 Progress Variable,拒绝解锁。"); + return false; + } + + StoryTreeController treeController = StoryManager.instance?.treeController; + if (treeController == null) + { + Debug.LogWarning("[StoryProgress] StoryTreeController 未就绪,拒绝写入教程进度。"); + return false; + } + + if (!TrySetBoolAndSave(tutorialBlock.tutorialProgressVariable, true)) + return false; + + // completedBlockIds 只负责教程节点自身的 Completed 外观与不可重复点击; + // 后续节点应在 StoryData 中以 VariableCondition 读取上面的剧情变量。 + treeController.OnBlockCompleted(tutorialBlock.blockId); + return true; + } + + /// + /// 清除一个章节中所有 TutorialBlock 声明的进度变量。 + /// 目前供 StoryTreeController 的章节进度调试重置使用,确保变量条件与 completedBlockIds 一起回到初始状态。 + /// + public static void ClearTutorialProgressForChapter(StoryData storyData) + { + if (storyData?.blocks == null || + GameSaveManager.instance == null || GameSaveManager.instance.StorySaveModule == null) + { + return; + } + + bool changed = false; + foreach (StoryBlockDefinition block in storyData.blocks) + { + if (block == null || block.blockType != StoryBlockType.Tutorial || + string.IsNullOrWhiteSpace(block.tutorialProgressVariable)) + { + continue; + } + + if (!StoryVariables.HasVariable(block.tutorialProgressVariable)) + continue; + + StoryVariables.Remove(block.tutorialProgressVariable); + changed = true; + } + + if (changed) + GameSaveManager.instance.StorySaveModule.SaveVariables(); + } + + private static bool TrySetBoolAndSave(string variableName, bool value) + { + if (string.IsNullOrWhiteSpace(variableName)) + { + Debug.LogWarning("[StoryProgress] 剧情变量名为空,拒绝写入。"); + return false; + } + + if (GameSaveManager.instance == null || GameSaveManager.instance.StorySaveModule == null) + { + Debug.LogWarning("[StoryProgress] StorySaveModule 未就绪,拒绝写入剧情变量。"); + return false; + } + + StoryVariables.SetBool(variableName, value); + GameSaveManager.instance.StorySaveModule.SaveVariables(); + return true; + } + } +} diff --git a/Assets/Scripts/NewStorySystem/Progress/StoryProgress.cs.meta b/Assets/Scripts/NewStorySystem/Progress/StoryProgress.cs.meta new file mode 100644 index 00000000..3bd9b527 --- /dev/null +++ b/Assets/Scripts/NewStorySystem/Progress/StoryProgress.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c2e5a902d1d14be4a2e1f06c2353d4a1 diff --git a/Assets/Scripts/NewStorySystem/Progress/TutorialFlowController.cs b/Assets/Scripts/NewStorySystem/Progress/TutorialFlowController.cs new file mode 100644 index 00000000..f04d89b7 --- /dev/null +++ b/Assets/Scripts/NewStorySystem/Progress/TutorialFlowController.cs @@ -0,0 +1,139 @@ +using Ichni.Menu; +using Ichni.Menu.UI; +using System.Collections.Generic; +using UnityEngine; + +namespace Ichni.Story +{ + /// + /// TutorialBlock 的菜单层流程协调器。 + /// 负责验证配置、生成通用 SelectionBox 选项、写入剧情进度,并在“游玩”时发起 GameScene 运行。 + /// 不保存“教程成绩”或“教程是否通关”;本阶段规则是玩家确认游玩或跳过即视为已处理。 + /// + public static class TutorialFlowController + { + /// + /// 由 点击时调用。配置无效或 SelectionBox 页面未配置时只报错,不修改剧情进度。 + /// + public static void RequestTutorial(StoryBlockDefinition tutorialBlock) + { + if (!TryResolveTutorialRunData(tutorialBlock, out ChapterSelectionUnit chapter, + out SongItemData song, out DifficultyData difficulty)) + { + return; + } + + MenuManager menuManager = MenuManager.instance; + SelectionBoxUIPage selectionBoxPage = menuManager?.selectionBoxUIPage; + if (selectionBoxPage == null) + { + Debug.LogWarning("[TutorialFlowController] MenuManager 未配置 SelectionBoxUIPage,教程不会被解锁。"); + return; + } + + // TutorialFlowController 只描述“有哪些选择、各自触发什么效果”; + // 具体按钮由通用 SelectionBox 在运行时生成,后续其它系统可复用同一 UI。 + selectionBoxPage.Show( + tutorialBlock.tutorialName, + string.Empty, + new List + { + new("游玩教程", () => PlayTutorial(tutorialBlock, chapter, song, difficulty)), + new("跳过教程", () => SkipTutorial(tutorialBlock)) + }); + } + + private static void PlayTutorial(StoryBlockDefinition tutorialBlock, ChapterSelectionUnit chapter, + SongItemData song, DifficultyData difficulty) + { + MenuManager menuManager = MenuManager.instance; + if (menuManager == null || menuManager.isEnteringGame) + { + Debug.LogWarning("[TutorialFlowController] 菜单正在切换,忽略教程游玩请求。"); + return; + } + + // 先验证能否启动 GameScene,再写入“已处理”进度;避免无效配置导致错误解锁。 + if (!CanBeginTutorialGame(chapter, song, difficulty)) + return; + + if (!StoryProgress.ResolveTutorial(tutorialBlock)) + return; + + // 规则:玩家确认“游玩教程”的瞬间即解锁后续剧情,不等待教程结算。 + menuManager.TryBeginGame(chapter, song, difficulty, MenuReturnDestination.Story, + menuManager.storyUIPage); + } + + private static void SkipTutorial(StoryBlockDefinition tutorialBlock) + { + // 规则:玩家确认“跳过教程”的瞬间即解锁后续剧情,并保留在故事树。 + StoryProgress.ResolveTutorial(tutorialBlock); + } + + private static bool CanBeginTutorialGame(ChapterSelectionUnit chapter, SongItemData song, DifficultyData difficulty) + { + if (MenuManager.instance == null || InformationTransistor.instance == null || + !MenuManager.instance.CanBeginGame) + { + Debug.LogWarning("[TutorialFlowController] MenuManager、InformationTransistor 或 GameScene 预加载未就绪,无法进入教程。"); + return false; + } + + if (chapter == null || song == null || difficulty == null) + { + Debug.LogWarning("[TutorialFlowController] 教程缺少章节、歌曲或难度配置,无法进入教程。"); + return false; + } + + return true; + } + + /// + /// 解析 TutorialBlock 的 Key、教程曲目与稳定难度 ID。 + /// 任何一项缺失都会失败且不写入剧情变量,防止错误配置意外解锁后续内容。 + /// + private static bool TryResolveTutorialRunData(StoryBlockDefinition tutorialBlock, + out ChapterSelectionUnit chapter, out SongItemData song, out DifficultyData difficulty) + { + chapter = ChapterSelectionManager.instance?.currentChapter; + song = null; + difficulty = null; + + if (tutorialBlock == null || tutorialBlock.blockType != StoryBlockType.Tutorial || + string.IsNullOrWhiteSpace(tutorialBlock.tutorialKey) || + string.IsNullOrWhiteSpace(tutorialBlock.tutorialProgressVariable) || + tutorialBlock.tutorialDifficultySaveId < 0) + { + Debug.LogWarning($"[TutorialFlowController] TutorialBlock '{tutorialBlock?.blockId}' 的 Tutorial Key、Progress Variable 或 Difficulty Save ID 未配置完整。"); + return false; + } + + TutorialCollection collection = ChapterSelectionManager.instance?.tutorialCollection; + if (chapter == null || collection == null || !collection.TryGetTutorialSong(tutorialBlock.tutorialKey, out song)) + { + Debug.LogWarning($"[TutorialFlowController] TutorialBlock '{tutorialBlock.blockId}' 无法解析章节或教程曲目。"); + return false; + } + + if (song.difficultyDataList == null) + { + Debug.LogWarning($"[TutorialFlowController] 教程 '{tutorialBlock.tutorialKey}' 没有难度列表。"); + return false; + } + + foreach (DifficultyData candidate in song.difficultyDataList) + { + if (candidate != null && candidate.isAvailable && + candidate.saveDifficultyId == tutorialBlock.tutorialDifficultySaveId) + { + difficulty = candidate; + return true; + } + } + + Debug.LogWarning($"[TutorialFlowController] 教程 '{tutorialBlock.tutorialKey}' 中不存在可用的 Difficulty Save ID '{tutorialBlock.tutorialDifficultySaveId}'。"); + return false; + } + } +} diff --git a/Assets/Scripts/NewStorySystem/Progress/TutorialFlowController.cs.meta b/Assets/Scripts/NewStorySystem/Progress/TutorialFlowController.cs.meta new file mode 100644 index 00000000..d5884843 --- /dev/null +++ b/Assets/Scripts/NewStorySystem/Progress/TutorialFlowController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a44d2b7e3f954b9b86f6beec1a7cb621 diff --git a/Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs b/Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs index 7577e43a..86603741 100644 --- a/Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs +++ b/Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs @@ -15,6 +15,12 @@ namespace Ichni private const string ChapterStoryKey = "ChapterStory"; private const string VariablesKey = "StoryVariables"; + // 每个剧情存档文件都保存自己的 Schema Version;不要把它与歌曲成绩存档的 Version 混用。 + // 当前项目尚未正式发布,剧情存档统一从 v1 开始;非 v1 的测试存档会被直接重置。 + // 正式发布后升级 Schema 时,必须用非破坏性迁移替代当前的重置策略。 + private const string StorySaveSchemaVersionKey = "StorySaveSchemaVersion"; + private const int CurrentStorySaveSchemaVersion = 1; + // 已加载章节的内存缓存(chapterIndex -> 章节存档) private readonly Dictionary _chapterSaves = new Dictionary(); @@ -28,31 +34,67 @@ namespace Ichni private static string VariablesPath => Application.persistentDataPath + "/StorySaves/StoryVariables.json"; + /// + /// 补齐章节存档的可空集合,并以文件名对应的 chapterIndex 作为唯一归属。 + /// 当前用于保证新建 v1 存档的运行时集合非空;未来迁移逻辑应单独实现,不应依赖此方法隐式转换。 + /// + private static ChapterStorySave NormalizeChapterSave(ChapterStorySave save, string chapterIndex) + { + save ??= new ChapterStorySave(); + save.chapterIndex = chapterIndex; + save.completedBlockIds ??= new List(); + save.selectedChoices ??= new Dictionary(); + return save; + } + + /// + /// 补齐全局剧情变量字典,保证新建 v1 存档可被 Yarn 变量访问。 + /// + private static StoryVariablesSave NormalizeVariables(StoryVariablesSave storyVariables) + { + storyVariables ??= new StoryVariablesSave(); + storyVariables.floatVariables ??= new Dictionary(); + storyVariables.stringVariables ??= new Dictionary(); + storyVariables.boolVariables ??= new Dictionary(); + return storyVariables; + } + // ── 故事树 + 选项(按章节)──────────────────────────────────────────────── /// - /// 判断某章节是否尚无存档(首次进入)。 + /// 判断某章节是否尚无可用的 v1 存档(首次进入或旧测试存档待重置)。 /// - public bool IsNewChapter(string chapterIndex) => - !ES3.FileExists(GetChapterSavePath(chapterIndex)); + public bool IsNewChapter(string chapterIndex) + { + string path = GetChapterSavePath(chapterIndex); + return !ES3.FileExists(path) || !ES3.KeyExists(ChapterStoryKey, path) || + !ES3.KeyExists(StorySaveSchemaVersionKey, path) || + ES3.Load(StorySaveSchemaVersionKey, path) != CurrentStorySaveSchemaVersion; + } /// - /// 加载指定章节的存档容器;无存档时返回一个新的空容器。加载结果会写入内存缓存。 + /// 加载指定章节的 v1 存档容器;无存档或非 v1 的预发布测试存档会直接重置为空容器。 + /// 加载结果会写入内存缓存。 /// public ChapterStorySave LoadChapter(string chapterIndex) { string path = GetChapterSavePath(chapterIndex); + bool hasExistingFile = ES3.FileExists(path); + int loadedSchemaVersion = hasExistingFile && ES3.KeyExists(StorySaveSchemaVersionKey, path) + ? ES3.Load(StorySaveSchemaVersionKey, path) + : 0; - ChapterStorySave save; - if (ES3.FileExists(path) && ES3.KeyExists(ChapterStoryKey, path)) + if (hasExistingFile && loadedSchemaVersion != CurrentStorySaveSchemaVersion) { - save = ES3.Load(ChapterStoryKey, path); - } - else - { - save = new ChapterStorySave { chapterIndex = chapterIndex }; + Debug.LogWarning($"Resetting pre-release chapter story save '{chapterIndex}' from schema v{loadedSchemaVersion} to v{CurrentStorySaveSchemaVersion}."); + ES3.DeleteFile(path); + hasExistingFile = false; } + ChapterStorySave save = hasExistingFile && ES3.KeyExists(ChapterStoryKey, path) + ? ES3.Load(ChapterStoryKey, path) + : new ChapterStorySave(); + save = NormalizeChapterSave(save, chapterIndex); _chapterSaves[chapterIndex] = save; return save; } @@ -62,8 +104,17 @@ namespace Ichni /// public void SaveChapter(ChapterStorySave save) { + if (save == null || string.IsNullOrEmpty(save.chapterIndex)) + { + Debug.LogWarning("Cannot save a chapter story record without a chapterIndex."); + return; + } + + save = NormalizeChapterSave(save, save.chapterIndex); _chapterSaves[save.chapterIndex] = save; - ES3.Save(ChapterStoryKey, save, GetChapterSavePath(save.chapterIndex)); + string path = GetChapterSavePath(save.chapterIndex); + ES3.Save(ChapterStoryKey, save, path); + ES3.Save(StorySaveSchemaVersionKey, CurrentStorySaveSchemaVersion, path); } /// @@ -101,14 +152,22 @@ namespace Ichni /// public void LoadVariables() { - if (ES3.FileExists(VariablesPath) && ES3.KeyExists(VariablesKey, VariablesPath)) + bool hasExistingFile = ES3.FileExists(VariablesPath); + int loadedSchemaVersion = hasExistingFile && ES3.KeyExists(StorySaveSchemaVersionKey, VariablesPath) + ? ES3.Load(StorySaveSchemaVersionKey, VariablesPath) + : 0; + + if (hasExistingFile && loadedSchemaVersion != CurrentStorySaveSchemaVersion) { - variables = ES3.Load(VariablesKey, VariablesPath); - } - else - { - variables = new StoryVariablesSave(); + Debug.LogWarning($"Resetting pre-release global story variables from schema v{loadedSchemaVersion} to v{CurrentStorySaveSchemaVersion}."); + ES3.DeleteFile(VariablesPath); + hasExistingFile = false; } + + variables = hasExistingFile && ES3.KeyExists(VariablesKey, VariablesPath) + ? ES3.Load(VariablesKey, VariablesPath) + : new StoryVariablesSave(); + variables = NormalizeVariables(variables); } /// @@ -116,7 +175,9 @@ namespace Ichni /// public void SaveVariables() { + variables = NormalizeVariables(variables); ES3.Save(VariablesKey, variables, VariablesPath); + ES3.Save(StorySaveSchemaVersionKey, CurrentStorySaveSchemaVersion, VariablesPath); } // ── 清档 ──────────────────────────────────────────────────────────────── diff --git a/Assets/Scripts/NewStorySystem/Tree/StoryManager.cs b/Assets/Scripts/NewStorySystem/Tree/StoryManager.cs index b86b56e7..707d2c1f 100644 --- a/Assets/Scripts/NewStorySystem/Tree/StoryManager.cs +++ b/Assets/Scripts/NewStorySystem/Tree/StoryManager.cs @@ -44,13 +44,13 @@ namespace Ichni.Story } /// - /// 清除全部剧情存档(所有章节故事树 + 全局变量 + 剧情解锁 key)。 + /// 清除全部剧情存档(所有章节故事树 + 全局变量 + 由剧情授予的内容解锁 Key)。 /// [Button] public void ClearAllStorySave() { GameSaveManager.instance.StorySaveModule.ClearAllStoryline(); - GameSaveManager.instance.SongSaveModule.ClearStoryKeys(); + GameSaveManager.instance.UnlockSaveModule.ClearAllKeys(); } } } diff --git a/Assets/Scripts/NewStorySystem/Tree/StoryTreeController.cs b/Assets/Scripts/NewStorySystem/Tree/StoryTreeController.cs index 70433f29..39040d5f 100644 --- a/Assets/Scripts/NewStorySystem/Tree/StoryTreeController.cs +++ b/Assets/Scripts/NewStorySystem/Tree/StoryTreeController.cs @@ -216,7 +216,12 @@ namespace Ichni.Story public void OnBlockCompleted(string blockId) { if (!_completed.Add(blockId)) - return; // 已完成,无需处理 + { + // 剧情变量可能在 block 已完成后被外部系统修正;即使完成集合没有变化, + // 也要重新推导依赖变量的后续 Block 状态。 + RefreshAllStates(); + return; + } RefreshAllStates(); SaveChapter(); @@ -382,6 +387,9 @@ namespace Ichni.Story return; } + // TutorialBlock 的后续解锁由全局剧情变量驱动;只清空 completedBlockIds + // 会造成“节点未完成但后续仍解锁”的错误测试结果,因此同时清除本章节声明的教程变量。 + StoryProgress.ClearTutorialProgressForChapter(_storyData); _completed.Clear(); SaveChapter(); BuildChapter(_chapterIndex); diff --git a/Assets/Scripts/NewStorySystem/Tree/TutorialBlockView.cs b/Assets/Scripts/NewStorySystem/Tree/TutorialBlockView.cs index fab91b09..72b3aa7b 100644 --- a/Assets/Scripts/NewStorySystem/Tree/TutorialBlockView.cs +++ b/Assets/Scripts/NewStorySystem/Tree/TutorialBlockView.cs @@ -4,7 +4,8 @@ using UnityEngine; namespace Ichni.Story.UI { /// - /// 教程块视图(占位)。点击后将进入该章节的教程游玩,后续阶段接入。 + /// 教程块视图。点击后由 显示确认窗口, + /// 玩家选择游玩或跳过时都会写入教程剧情变量并刷新故事树。 /// public class TutorialBlockView : StoryBlockView { @@ -21,8 +22,19 @@ namespace Ichni.Story.UI protected override void OnClicked() { - // 占位:后续接入教程进入流程 - Debug.Log($"[TutorialBlockView] 点击教程块 '{blockId}' (tutorial='{definition.tutorialName}')。占位实现,教程进入将在后续阶段接入。"); + TutorialFlowController.RequestTutorial(definition); + } + + /// + /// 已处理的教程不再重复弹出“游玩 / 跳过”确认窗口。 + /// 教程重玩将来应由独立的教程入口提供,不复用剧情进度节点。 + /// + public override void ApplyState(StoryBlockState newState) + { + base.ApplyState(newState); + + if (button != null && newState == StoryBlockState.Completed) + button.interactable = false; } } } diff --git a/Assets/Scripts/NewStorySystem/YarnFunctions/StoryTreeCommands.cs b/Assets/Scripts/NewStorySystem/YarnFunctions/StoryTreeCommands.cs index 83541132..3980d39e 100644 --- a/Assets/Scripts/NewStorySystem/YarnFunctions/StoryTreeCommands.cs +++ b/Assets/Scripts/NewStorySystem/YarnFunctions/StoryTreeCommands.cs @@ -14,39 +14,29 @@ namespace Ichni.Story.YarnFunctions // ── Yarn Commands ──────────────────────────────────────────────────────── /// - /// 解锁一首新歌曲,并在当前这段对话结束(对话 UI 淡出)后,弹出 MessageBox 提示。 - /// 格式: <<unlock_song songUnlockKey>> + /// 兼容既有 Yarn 脚本:授予歌曲相关的解锁 Key,并在当前对话结束后弹出提示。 + /// 格式: <<unlock_song unlock_key>> + /// Key 必须使用小写字母、数字和下划线,例如 story_ch0_prologue_completed + /// 新内容若不需要歌曲提示,优先使用 。 /// - /// 要解锁的歌曲密钥/ID + /// 要授予的稳定解锁 Key,而不是歌曲显示名称。 [YarnCommand("unlock_song")] public static void UnlockSong(string songUnlockKey) { - if (GameSaveManager.instance == null || GameSaveManager.instance.SongSaveModule == null) + if (!TryGrantUnlockKey(songUnlockKey, out bool wasNewlyGranted) || !wasNewlyGranted) { - Debug.LogError("[StoryTreeCommands] 无法解锁歌曲:GameSaveManager 或其 SongSaveModule 未初始化!"); return; } - // 1. 检查是否已经解锁 - if (GameSaveManager.instance.SongSaveModule.CheckStoryKey(songUnlockKey)) - { - Debug.Log($"[StoryTreeCommands] 歌曲 {songUnlockKey} 已处于解锁状态。"); - return; - } - - // 2. 写入解锁状态并保存存档 - GameSaveManager.instance.SongSaveModule.storyUnlockKeys.Add(songUnlockKey); - GameSaveManager.instance.SongSaveModule.SaveStoryUnlockKeys(); - Debug.Log($"[StoryTreeCommands] 已成功写入并保存歌曲解锁密钥:{songUnlockKey}"); - - // 3. 将 UI 弹窗提示加入到对话结束队列中 + // 将歌曲提示加入对话结束队列。弹窗本地化将由后续 Localization 阶段替换, + // 这里不参与内容是否解锁的授权判断。 if (StoryDialogueController.instance != null) { StoryDialogueController.instance.dialogueEndActions.Add(() => { - if (StoryMessageBoxUIPage.instance != null) + if (MessageUIPage.instance != null) { - StoryMessageBoxUIPage.instance.ShowUnlockMessage(songUnlockKey); + MessageUIPage.instance.ShowUnlockMessage(songUnlockKey); } else { @@ -60,6 +50,18 @@ namespace Ichni.Story.YarnFunctions } } + /// + /// 通用内容解锁命令,可用于章节、歌曲、教程及未来内容。 + /// 格式: <<grant_unlock unlock_key>>。 + /// 本命令只授予并保存 Key,不绑定任何 UI 提示;需要提示时应由 Yarn 额外调用 show_message。 + /// + /// 符合 的稳定解锁 Key。 + [YarnCommand("grant_unlock")] + public static void GrantUnlock(string unlockKey) + { + TryGrantUnlockKey(unlockKey, out _); + } + /// /// 在对话结束后,弹出一个自定义 MessageBox 提示。 /// 会尝试使用 Unity Localization 进行翻译,如果找不到对应条目则回退显示原文本(Key)。 @@ -76,10 +78,39 @@ namespace Ichni.Story.YarnFunctions Debug.Log($"[StoryTreeCommands] 准备在对话结束后显示自定义弹窗:标题='{translatedTitle}',内容='{translatedContent}'"); StoryDialogueController.instance.dialogueEndActions.Add(() => { - StoryMessageBoxUIPage.instance.ShowCustomMessage(translatedTitle, translatedContent); + MessageUIPage.instance.ShowCustomMessage(translatedTitle, translatedContent); }); } + /// + /// 所有 Yarn 解锁命令共用的授予入口。它负责模块存在性检查、重复授予的幂等处理及立即 ES3 保存。 + /// + /// 模块已正常处理该 Key 时返回 true;Key 非法或模块尚未初始化时返回 false。 + private static bool TryGrantUnlockKey(string unlockKey, out bool wasNewlyGranted) + { + wasNewlyGranted = false; + UnlockSaveModule unlockSaveModule = GameSaveManager.instance?.UnlockSaveModule; + if (unlockSaveModule == null) + { + Debug.LogError("[StoryTreeCommands] 无法授予解锁 Key:UnlockSaveModule 尚未初始化!"); + return false; + } + + if (unlockSaveModule.HasKey(unlockKey)) + { + Debug.Log($"[StoryTreeCommands] 解锁 Key 已存在:{unlockKey}"); + return true; + } + + wasNewlyGranted = unlockSaveModule.GrantKey(unlockKey); + if (wasNewlyGranted) + { + Debug.Log($"[StoryTreeCommands] 已成功写入并保存解锁 Key:{unlockKey}"); + } + + return wasNewlyGranted; + } + // ── 内部辅助 ───────────────────────────────────────────────────────────── private static string GetTranslatedText(string tableName, string key) diff --git a/Assets/Scripts/Saving/BeatmapSave.cs b/Assets/Scripts/Saving/BeatmapSave.cs index bc25ed9f..361238b6 100644 --- a/Assets/Scripts/Saving/BeatmapSave.cs +++ b/Assets/Scripts/Saving/BeatmapSave.cs @@ -4,12 +4,27 @@ using UnityEngine; namespace Ichni.RhythmGame { + /// + /// 单个歌曲难度的最佳成绩。 + /// 这里只保存当前 Chart Revision 下可比较的最佳记录,不保存逐局历史、判定时间线或 Replay。 + /// public class BeatmapSave { + /// 历史最高 Accuracy,固定使用 0 到 1;显示时由 UI 乘以 100。 public float accuracy; + + /// 当前 Revision 下是否曾达成 Full Combo。 public bool isFullCombo; + + /// 当前 Revision 下是否曾达成 All Perfect。 public bool isAllPerfect; + /// 成绩对应的谱面修订号;0 表示尚未绑定 Revision 的新建或异常记录。 + public int chartRevision; + + /// 当前 Revision 下的历史最高连击数。 + public int maxCombo; + public BeatmapSave() { @@ -21,10 +36,83 @@ namespace Ichni.RhythmGame this.isFullCombo = isFullCombo; this.isAllPerfect = isAllPerfect; } + + /// + /// 令存档成绩匹配当前谱面 Revision。 + /// 仅当 Note、Timing 或判定规则改变、导致成绩不再可比较时才递增 Revision。 + /// 未绑定 Revision 的记录会保留已有成绩并绑定到当前 Revision;之后出现真正的 Revision + /// 变化时,才仅重置该谱面的可比成绩。 + /// + public bool EnsureChartRevision(int currentChartRevision) + { + currentChartRevision = Mathf.Max(1, currentChartRevision); + + if (chartRevision <= 0) + { + chartRevision = currentChartRevision; + return true; + } + + if (chartRevision == currentChartRevision) + { + return false; + } + + accuracy = 0f; + isFullCombo = false; + isAllPerfect = false; + maxCombo = 0; + chartRevision = currentChartRevision; + return true; + } + + /// + /// 合并一次完整演奏的成绩。 + /// 在持久化记录中始终使用 0 到 1 的归一化值, + /// 而 的运行时显示值为 0 到 100。 + /// 写入规则是“最高 Accuracy / 最高 Max Combo / FC 与 AP 成就粘性保留”,而不是保存最近一次成绩。 + /// + public bool ApplyPlayResult(PlayingRecorder recorder, int currentChartRevision) + { + if (recorder == null || recorder.totalCount <= 0) + { + return false; + } + + bool changed = EnsureChartRevision(currentChartRevision); + float normalizedAccuracy = Mathf.Clamp01(recorder.accuracy / 100f); + if (normalizedAccuracy > accuracy) + { + accuracy = normalizedAccuracy; + changed = true; + } + + bool achievedFullCombo = recorder.isFullCombo || recorder.isAllPerfect; + if (achievedFullCombo && !isFullCombo) + { + isFullCombo = true; + changed = true; + } + + if (recorder.isAllPerfect && !isAllPerfect) + { + isAllPerfect = true; + changed = true; + } + + if (recorder.maxCombo > maxCombo) + { + maxCombo = recorder.maxCombo; + changed = true; + } + + return changed; + } + /// 用于章节完成度统计;Revision 本身不代表玩家已经游玩过该谱面。 public bool IsEmpty() { - return accuracy == 0f && !isFullCombo && !isAllPerfect; + return accuracy == 0f && !isFullCombo && !isAllPerfect && maxCombo == 0; } } } diff --git a/Assets/Scripts/Saving/GameSaveManager.cs b/Assets/Scripts/Saving/GameSaveManager.cs index 467869a8..b901d196 100644 --- a/Assets/Scripts/Saving/GameSaveManager.cs +++ b/Assets/Scripts/Saving/GameSaveManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using Ichni.Menu; @@ -9,11 +8,19 @@ using UnityEngine; namespace Ichni { + /// + /// 全局存档入口。 + /// 负责可重复游玩的歌曲成绩与谱面记录; + /// 负责章节、歌曲及未来内容共用的 Offline 解锁 Key; + /// 负责章节故事树、对话选项和 Yarn 变量。 + /// 两套数据使用不同文件和不同生命周期,修改任一侧时不要把另一侧的字段或迁移规则混入其中。 + /// public class GameSaveManager : SerializedMonoBehaviour { public static GameSaveManager instance; public SongSaveModule SongSaveModule; + public UnlockSaveModule UnlockSaveModule; public StorySaveModule StorySaveModule; private void Awake() @@ -31,10 +38,15 @@ namespace Ichni private void Start() { + // 菜单与章节 UI 会在启动后立刻读取歌曲记录,因此游戏记录必须先加载。 SongSaveModule = new SongSaveModule(); SongSaveModule.LoadSongStatuses(); - SongSaveModule.LoadStoryUnlockKeys(); + // 内容解锁既不属于歌曲成绩也不属于 Yarn 剧情树。菜单在首次显示前必须先得到它的状态。 + UnlockSaveModule = new UnlockSaveModule(); + UnlockSaveModule.LoadUnlockKeys(); + + // 剧情变量属于独立存档,不参与歌曲成绩的 Schema 或 Chart Revision 迁移。 StorySaveModule = new StorySaveModule(); StorySaveModule.LoadVariables(); } @@ -42,106 +54,82 @@ namespace Ichni public partial class SongSaveModule { - public HashSet storyUnlockKeys; - public HashSet paymentUnlockKeys; + // ES3 数据 Key。一旦发布,不要重命名;重命名等同于修改存档格式,需要 Schema Migration。 + private const string SongSavesKey = "SongSaves"; + + // 与歌曲记录存放在同一文件。当前项目尚未正式发布,不兼容的旧版本会直接重置为新的 v1 记录。 + private const string SongSaveSchemaVersionKey = "SongSaveSchemaVersion"; + + // v1 使用 Dictionary 保存成绩,彻底与选曲 UI 的列表位置分离。 + // 谱面内容改变应递增 Chart Revision,而不是此值。正式发布后升级 Schema 时必须恢复非破坏性迁移策略。 + private const int CurrentSongSaveSchemaVersion = 1; + public Dictionary songStatusSaves; + // 歌曲成绩与 Schema Version 的唯一持久化文件;剧情存档不使用此路径。 private string songSavePath => Application.persistentDataPath + "/GameSaves/SongSaves.json"; - private string storyUnlockKeysPath => Application.persistentDataPath + "/GameSaves/UnlockKeys.json"; - private string paymentUnlockKeysPath => Application.persistentDataPath + "/GameSaves/PaymentUnlockKeys.json"; public SongSaveModule() { songStatusSaves = new Dictionary(); - storyUnlockKeys = new HashSet(); - paymentUnlockKeys = new HashSet(); //Debug.Log("Song save path: " + songSavePath); } } public partial class SongSaveModule { - public void SaveStoryUnlockKeys() + /// + /// 使已加载的歌曲记录与当前 Chapter 配置保持一致。 + /// 会为新增难度建立以稳定 saveDifficultyId 为 Key 的记录,并检查每个谱面的 Chart Revision。 + /// 已删除难度的旧 Key 不自动删除:它不会参与当前 UI 或完成度统计,且可在内容恢复时保留历史成绩。 + /// Revision 不一致时只重置该谱面的可比成绩,不回退歌曲完成状态、剧情进度或解锁状态。 + /// + private static void EnsureSongStatusMatchesDefinition(SongStatusSave songStatus, SongItemData song) { - ES3.Save("UnlockKeys", storyUnlockKeys, storyUnlockKeysPath); - } - - public void LoadStoryUnlockKeys() - { - if (ES3.FileExists(storyUnlockKeysPath)) + songStatus.additionalInfo ??= string.Empty; + songStatus.beatmapSavesByDifficultyId ??= new Dictionary(); + + foreach (DifficultyData difficulty in song.difficultyDataList) { - storyUnlockKeys = ES3.Load>("UnlockKeys", storyUnlockKeysPath); - } - else - { - storyUnlockKeys = new HashSet(); - SaveStoryUnlockKeys(); + if (difficulty == null || difficulty.saveDifficultyId < 0) + { + continue; + } + + BeatmapSave beatmapSave = songStatus.GetOrCreateBeatmapSave(difficulty.saveDifficultyId); + beatmapSave.EnsureChartRevision(difficulty.GetChartRevision()); } } - - public bool CheckStoryKey(string key) + + /// + /// 为新加入首发内容的歌曲创建空记录,并立即按当前难度定义初始化 Revision。 + /// + private static SongStatusSave CreateSongStatus(SongItemData song) { - return key == string.Empty || storyUnlockKeys.Contains(key); + SongStatusSave songStatus = new SongStatusSave(false, string.Empty, new Dictionary()); + EnsureSongStatusMatchesDefinition(songStatus, song); + return songStatus; } - public void ClearStoryKeys() - { - storyUnlockKeys.Clear(); - SaveStoryUnlockKeys(); - } - } - - public partial class SongSaveModule - { - public void SavePaymentUnlockKeys() - { - ES3.Save("UnlockKeys", paymentUnlockKeys, paymentUnlockKeysPath); - } - - public void LoadPaymentUnlockKeys() - { - if (ES3.FileExists(paymentUnlockKeysPath)) - { - paymentUnlockKeys = ES3.Load>("UnlockKeys", paymentUnlockKeysPath); - } - else - { - paymentUnlockKeys = new HashSet(); - SavePaymentUnlockKeys(); - } - } - - public bool CheckPaymentKey(string key) - { - return key == string.Empty || paymentUnlockKeys.Contains(key); - } - - public void ClearPaymentKeys() - { - paymentUnlockKeys.Clear(); - SavePaymentUnlockKeys(); - } - } - - public partial class SongSaveModule - { + /// + /// 首次启动、尚无歌曲存档时调用。以当前 Chapter 配置生成完整的空记录集合。 + /// private void InitializeSongStatuses() { + songStatusSaves = new Dictionary(); foreach (ChapterSelectionUnit chapter in ChapterSelectionManager.instance.chapters) { foreach (SongItemData song in chapter.songs) { - SongStatusSave songStatus = new SongStatusSave(false, string.Empty, new List()); - foreach (DifficultyData difficulty in song.difficultyDataList) - { - songStatus.beatmapSaves.Add(new BeatmapSave(0f,false, false)); - } - - songStatusSaves.Add(song.songName, songStatus); + songStatusSaves[song.songName] = CreateSongStatus(song); } } SaveSongStatuses(); } + /// + /// 每次加载后执行的内容对账与 Revision 检查。 + /// 这是新增/删除歌曲、增加难度或递增 Chart Revision 后自动修正旧存档的入口。 + /// private void CheckSongStatuses() { foreach (ChapterSelectionUnit chapter in ChapterSelectionManager.instance.chapters) @@ -150,27 +138,17 @@ namespace Ichni { if (songStatusSaves.TryGetValue(song.songName, out SongStatusSave songStatus)) { - int difficultiesCount = song.difficultyDataList.Count; - if (songStatus.beatmapSaves.Count < difficultiesCount) + if (songStatus == null) { - for (int i = songStatus.beatmapSaves.Count; i < difficultiesCount; i++) - { - songStatus.beatmapSaves.Add(new BeatmapSave(0f, false, false)); - } - } - else if (songStatus.beatmapSaves.Count > difficultiesCount) - { - songStatus.beatmapSaves.RemoveRange(difficultiesCount, songStatus.beatmapSaves.Count - difficultiesCount); + songStatusSaves[song.songName] = CreateSongStatus(song); + continue; } + + EnsureSongStatusMatchesDefinition(songStatus, song); } else { - songStatus = new SongStatusSave(false, string.Empty, new List()); - foreach (DifficultyData difficulty in song.difficultyDataList) - { - songStatus.beatmapSaves.Add(new BeatmapSave(0f, false, false)); - } - songStatusSaves.Add(song.songName, songStatus); + songStatusSaves[song.songName] = CreateSongStatus(song); } } } @@ -179,16 +157,86 @@ namespace Ichni } [Button] + /// + /// 将歌曲记录与当前 Schema Version 一起写入 ES3。 + /// 不要单独保存成绩而遗漏 Version;否则下次启动会把同一份数据错误识别为旧 Schema。 + /// public void SaveSongStatuses() { - ES3.Save("SongSaves", songStatusSaves, songSavePath); + ES3.Save(SongSavesKey, songStatusSaves, songSavePath); + ES3.Save(SongSaveSchemaVersionKey, CurrentSongSaveSchemaVersion, songSavePath); } + /// + /// 将一次已完成演奏写入游戏记录存档。 + /// 每个谱面保留当前 Chart Revision 下的最高 Accuracy、最高 Max Combo,以及曾达成的 FC/AP; + /// 后续较差成绩不会覆盖既有纪录。本函数不修改剧情树、Yarn 变量或歌曲解锁。 + /// + /// SongStatusSave 的稳定歌曲 Key。 + /// 存档 Key,必须对应 DifficultyData.saveDifficultyId。 + /// 当前谱面的 Revision;不一致时由 BeatmapSave 重置该谱面成绩。 + /// 仅本局有效的运行时判定记录。 + public bool RecordPlayResult(string songName, int saveDifficultyId, int currentChartRevision, PlayingRecorder recorder) + { + if (string.IsNullOrEmpty(songName)) + { + Debug.LogWarning("Cannot save play result without a song name."); + return false; + } + + if (saveDifficultyId < 0) + { + Debug.LogWarning($"Cannot save play result for '{songName}' with an invalid save difficulty ID: {saveDifficultyId}."); + return false; + } + + if (recorder == null || recorder.totalCount <= 0) + { + Debug.LogWarning($"Skipped saving play result for '{songName}' because no judgments were recorded."); + return false; + } + + songStatusSaves ??= new Dictionary(); + if (!songStatusSaves.TryGetValue(songName, out SongStatusSave songStatus) || songStatus == null) + { + songStatus = new SongStatusSave(false, string.Empty, new Dictionary()); + songStatusSaves[songName] = songStatus; + } + + songStatus.additionalInfo ??= string.Empty; + BeatmapSave beatmapSave = songStatus.GetOrCreateBeatmapSave(saveDifficultyId); + + bool recordChanged = beatmapSave.ApplyPlayResult(recorder, currentChartRevision); + bool completionChanged = !songStatus.isCompleted; + songStatus.isCompleted = true; + + SaveSongStatuses(); + return recordChanged || completionChanged; + } + + /// + /// 读取歌曲记录并执行内容对账和 Chart Revision 检查。 + /// 当前为正式发布前的重置策略:任何非 v1 的旧测试存档都会直接被新 v1 结构覆盖, + /// 因此开发者需要自行接受成绩清空。正式发布后不得沿用此策略,应改回显式迁移。 + /// public void LoadSongStatuses() { - if (ES3.FileExists(songSavePath)) + if (ES3.FileExists(songSavePath) && ES3.KeyExists(SongSavesKey, songSavePath)) { - songStatusSaves = ES3.Load>("SongSaves", songSavePath); + int loadedSchemaVersion = ES3.KeyExists(SongSaveSchemaVersionKey, songSavePath) + ? ES3.Load(SongSaveSchemaVersionKey, songSavePath) + : 0; + + if (loadedSchemaVersion != CurrentSongSaveSchemaVersion) + { + Debug.LogWarning($"Resetting pre-release song save schema v{loadedSchemaVersion} to v{CurrentSongSaveSchemaVersion}."); + ES3.DeleteFile(songSavePath); + InitializeSongStatuses(); + return; + } + + songStatusSaves = ES3.Load>(SongSavesKey, songSavePath) ?? + new Dictionary(); CheckSongStatuses(); } else @@ -198,21 +246,38 @@ namespace Ichni } [Button] + /// + /// 仅清除每张谱面的可比成绩(Accuracy、FC、AP、Max Combo)。 + /// 保留 Chart Revision、歌曲完成状态、剧情进度与解锁状态,适合测试成绩而不破坏流程存档。 + /// public void ClearBeatmapRecords() { - foreach (var songStatus in songStatusSaves.Values) + foreach (SongStatusSave songStatus in songStatusSaves.Values) { - foreach (var beatmapSave in songStatus.beatmapSaves) + if (songStatus?.beatmapSavesByDifficultyId == null) { + continue; + } + + foreach (BeatmapSave beatmapSave in songStatus.beatmapSavesByDifficultyId.Values) + { + if (beatmapSave == null) + { + continue; + } beatmapSave.accuracy = 0.0f; beatmapSave.isFullCombo = false; beatmapSave.isAllPerfect = false; + beatmapSave.maxCombo = 0; } } SaveSongStatuses(); } + /// + /// 按歌曲 Key 获取内存中的游戏记录;该方法不触发磁盘读取或创建新记录。 + /// public SongStatusSave GetSongStatusSave(string songName) { if (songStatusSaves.TryGetValue(songName, out SongStatusSave save)) @@ -226,4 +291,4 @@ namespace Ichni } // StorySaveModule 已迁移至 NewStorySystem/Save/StorySaveModule.cs(按章节存树/选项,全局存变量) -} \ No newline at end of file +} diff --git a/Assets/Scripts/Saving/SongStatusSave.cs b/Assets/Scripts/Saving/SongStatusSave.cs index 791fe54c..ae415c64 100644 --- a/Assets/Scripts/Saving/SongStatusSave.cs +++ b/Assets/Scripts/Saving/SongStatusSave.cs @@ -2,24 +2,69 @@ using System.Collections; using System.Collections.Generic; using UnityEngine; +using System.Collections.Generic; + namespace Ichni.RhythmGame { + /// + /// 单首歌曲的持久化容器。 + /// 只表示玩家曾完成过该歌曲;具体难度的成绩、Revision 与 Max Combo + /// 由 中对应稳定难度 ID 的 管理。 + /// public class SongStatusSave { + /// 至少有一次有效结算完成;谱面 Revision 变化不会回退该流程状态。 public bool isCompleted; + + /// 预留的歌曲级扩展字段;赋予新语义前须评估 Schema Version 是否需要递增。 public string additionalInfo; - public List beatmapSaves; + + /// + /// 以 DifficultyData.saveDifficultyId 为 Key 的谱面成绩。 + /// Key 不依赖选曲列表位置,因此重排、隐藏或删除中间难度不会让其他难度的成绩错位。 + /// 被内容定义移除的 Key 暂时保留在存档中,但 UI 与章节完成度只读取当前仍存在的难度。 + /// + public Dictionary beatmapSavesByDifficultyId = new(); public SongStatusSave() { } - public SongStatusSave(bool isCompleted, string additionalInfo, List beatmapSaves) + public SongStatusSave(bool isCompleted, string additionalInfo, Dictionary beatmapSavesByDifficultyId) { this.isCompleted = isCompleted; this.additionalInfo = additionalInfo; - this.beatmapSaves = beatmapSaves; + this.beatmapSavesByDifficultyId = beatmapSavesByDifficultyId; + } + + /// 只读查询当前定义的难度成绩;不会因 UI 展示而创建新的存档记录。 + public bool TryGetBeatmapSave(int saveDifficultyId, out BeatmapSave beatmapSave) + { + beatmapSave = null; + return saveDifficultyId >= 0 && beatmapSavesByDifficultyId != null && + beatmapSavesByDifficultyId.TryGetValue(saveDifficultyId, out beatmapSave) && beatmapSave != null; + } + + /// + /// 获取或创建指定稳定难度 ID 的成绩容器。 + /// 仅存档写入/内容对账可调用此方法;选曲 UI 应使用 。 + /// + public BeatmapSave GetOrCreateBeatmapSave(int saveDifficultyId) + { + if (saveDifficultyId < 0) + { + return null; + } + + beatmapSavesByDifficultyId ??= new Dictionary(); + if (!beatmapSavesByDifficultyId.TryGetValue(saveDifficultyId, out BeatmapSave beatmapSave) || beatmapSave == null) + { + beatmapSave = new BeatmapSave(0f, false, false); + beatmapSavesByDifficultyId[saveDifficultyId] = beatmapSave; + } + + return beatmapSave; } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Saving/UnlockSaveModule.cs b/Assets/Scripts/Saving/UnlockSaveModule.cs new file mode 100644 index 00000000..723239d3 --- /dev/null +++ b/Assets/Scripts/Saving/UnlockSaveModule.cs @@ -0,0 +1,388 @@ +using System; +using System.Collections.Generic; +using Ichni.Menu; +using Sirenix.OdinInspector; +using UnityEngine; + +namespace Ichni +{ + /// + /// Offline 内容解锁的独立持久化模块。 + /// 本模块只保存“玩家已经获得了哪些解锁 Key”,不保存歌曲成绩、剧情树或 Yarn 变量。 + /// 章节、歌曲、教程及未来其它内容都通过 查询本模块, + /// 因而新增内容类型时不需要再建立一份平行的 Key 存档。 + /// 首发为纯 Offline:本地 Key 可被玩家修改,不把它作为反作弊或付费凭证使用。 + /// + public sealed class UnlockSaveModule + { + // 保留既有 ES3 Key 与文件名,使此前仅保存 HashSet 的预发布 UnlockKeys.json 可以无损补写 Schema v1。 + private const string UnlockKeysKey = "UnlockKeys"; + private const string UnlockSaveSchemaVersionKey = "UnlockSaveSchemaVersion"; + private const int CurrentUnlockSaveSchemaVersion = 1; + + /// + /// 当前玩家已获得的所有稳定解锁 Key。只能通过 / + /// 修改,避免调用方漏存盘或绕过格式校验。 + /// + private HashSet grantedKeys = new HashSet(); + + // 同一配置错误可能被 UI 每帧查询;仅记录一次,避免 Console 被重复警告淹没。 + private readonly HashSet _reportedInvalidRules = new HashSet(); + + private string UnlockSavePath => Application.persistentDataPath + "/GameSaves/UnlockKeys.json"; + + /// + /// 解锁状态变化事件。当前菜单重新打开时会重建列表;未来若需要在同一页面即时刷新, + /// UI 可订阅此事件后调用自己的刷新方法。 + /// + public event Action UnlockStateChanged; + + /// + /// 读取解锁 Key。旧预发布文件若只有 UnlockKeys 而没有 Schema,因数据布局相同会直接补写 v1; + /// 其它不兼容 Schema 仍按当前预发布策略重置。 + /// + public void LoadUnlockKeys() + { + grantedKeys = new HashSet(); + + if (!ES3.FileExists(UnlockSavePath) || !ES3.KeyExists(UnlockKeysKey, UnlockSavePath)) + { + SaveUnlockKeys(); + return; + } + + int loadedSchemaVersion = ES3.KeyExists(UnlockSaveSchemaVersionKey, UnlockSavePath) + ? ES3.Load(UnlockSaveSchemaVersionKey, UnlockSavePath) + : 0; + + // v0 是本项目此前的 HashSet 直接存档,字段布局与 v1 一致,因此可以原样保留。 + if (loadedSchemaVersion != 0 && loadedSchemaVersion != CurrentUnlockSaveSchemaVersion) + { + Debug.LogWarning($"Resetting pre-release unlock save schema v{loadedSchemaVersion} to v{CurrentUnlockSaveSchemaVersion}."); + ES3.DeleteFile(UnlockSavePath); + SaveUnlockKeys(); + return; + } + + grantedKeys = ES3.Load>(UnlockKeysKey, UnlockSavePath) ?? new HashSet(); + RemoveInvalidKeysLoadedFromDisk(); + + // 包括从 v0 补写 v1、以及剔除无效 Key 后的回存。 + SaveUnlockKeys(); + } + + /// + /// 立即写入全部已授予的 Key 与当前 Schema Version。 + /// 调用方不应单独操作 ES3,以保证 Key 集合与 Schema 始终成对更新。 + /// + public void SaveUnlockKeys() + { + ES3.Save(UnlockKeysKey, grantedKeys, UnlockSavePath); + ES3.Save(UnlockSaveSchemaVersionKey, CurrentUnlockSaveSchemaVersion, UnlockSavePath); + } + + /// + /// 判断玩家是否已持有一个有效 Key。空 Key 永远不代表已解锁; + /// 无条件开放应通过 为 null 表示。 + /// + public bool HasKey(string key) + { + return IsValidKey(key) && grantedKeys.Contains(key); + } + + /// + /// 授予一个内容解锁 Key,并在首次授予时立即保存。 + /// 返回 true 表示本次确实新增了 Key;已存在或格式非法时返回 false。 + /// + public bool GrantKey(string key) + { + if (!IsValidKey(key)) + { + Debug.LogWarning($"Cannot grant invalid unlock key '{key}'. {KeyNamingRule}"); + return false; + } + + if (!grantedKeys.Add(key)) + { + return false; + } + + SaveUnlockKeys(); + UnlockStateChanged?.Invoke(); + return true; + } + + /// + /// 仅供开发调试或“清空剧情进度”流程调用。它会删除所有由剧情授予的内容访问权, + /// 但不会影响歌曲成绩、Chart Revision 或 Yarn 变量。 + /// + public void ClearAllKeys() + { + if (grantedKeys.Count == 0) + { + return; + } + + grantedKeys.Clear(); + SaveUnlockKeys(); + UnlockStateChanged?.Invoke(); + } + + /// + /// 统一判断章节是否可访问。章节入口必须调用本方法,不能只依赖 Button.interactable 的视觉状态。 + /// + public bool CanAccessChapter(ChapterSelectionUnit chapter) + { + if (chapter == null) + { + Debug.LogWarning("Cannot evaluate a null chapter unlock requirement."); + return false; + } + + return IsRequirementSatisfied(chapter.unlockRequirement, $"chapter '{chapter.chapterIndex}'"); + } + + /// + /// 统一判断歌曲本身是否可访问。它不包含章节判断;需要真正开始游玩时请使用 。 + /// + public bool CanAccessSong(SongItemData song) + { + if (song == null) + { + Debug.LogWarning("Cannot evaluate a null song unlock requirement."); + return false; + } + + return IsRequirementSatisfied(song.unlockRequirement, $"song '{song.songName}'"); + } + + /// + /// 所有进入歌曲的最终授权入口:章节与歌曲本身都必须开放。 + /// 标准 Play、快速点击以及未来 Story Song Block 都应调用此方法,避免入口之间出现绕过。 + /// + public bool CanEnterSong(ChapterSelectionUnit chapter, SongItemData song) + { + return CanAccessChapter(chapter) && CanAccessSong(song); + } + + /// + /// 求值一个内容配置的解锁规则。null 规则或根节点为空均表示无条件开放; + /// 已配置但不完整的条件节点会失败关闭(保持锁定)并只记录一次警告。 + /// + public bool IsRequirementSatisfied(UnlockRequirement requirement, string ownerDescription) + { + return requirement == null || requirement.IsSatisfied(this, ownerDescription); + } + + /// + /// Key 命名规则:只能使用小写英文字母、数字和下划线;必须以字母开头,长度 1–96。 + /// 推荐按“来源_章节_事件_状态”命名,例如 story_ch0_prologue_completed。 + /// 不得使用空格、点号、连字符、显示名称、翻译文本或会频繁改名的 Yarn 标题。 + /// + public const string KeyNamingRule = + "Unlock Key must use lowercase letters, digits and underscores, start with a letter, and be 1-96 characters."; + + /// + /// 验证 Key 是否符合项目稳定命名约定。此方法不记录日志,便于 UI 的高频查询安全调用。 + /// + public static bool IsValidKey(string key) + { + if (string.IsNullOrEmpty(key) || key.Length > 96 || key[0] < 'a' || key[0] > 'z') + { + return false; + } + + for (int index = 1; index < key.Length; index++) + { + char character = key[index]; + bool isLowercaseLetter = character >= 'a' && character <= 'z'; + bool isDigit = character >= '0' && character <= '9'; + if (!isLowercaseLetter && !isDigit && character != '_') + { + return false; + } + } + + return true; + } + + /// + /// 由规则节点调用,确保无效配置只输出一次可定位的警告。 + /// + internal void ReportInvalidRuleOnce(string ownerDescription, string detail) + { + string reportKey = ownerDescription + ":" + detail; + if (_reportedInvalidRules.Add(reportKey)) + { + Debug.LogWarning($"Invalid unlock requirement on {ownerDescription}: {detail}. {KeyNamingRule}"); + } + } + + private void RemoveInvalidKeysLoadedFromDisk() + { + List invalidKeys = null; + foreach (string key in grantedKeys) + { + if (!IsValidKey(key)) + { + invalidKeys ??= new List(); + invalidKeys.Add(key); + } + } + + if (invalidKeys == null) + { + return; + } + + foreach (string key in invalidKeys) + { + grantedKeys.Remove(key); + } + + Debug.LogWarning($"Removed {invalidKeys.Count} invalid unlock key(s) while loading the pre-release save. {KeyNamingRule}"); + } + } + + /// + /// 可配置在章节或歌曲上的解锁规则。根节点为空表示无条件开放; + /// 若要锁定内容,请在 Inspector 中选择 Key / All Of / Any Of 节点并填写稳定 Key。 + /// + [InlineProperty] + [Serializable] + public sealed class UnlockRequirement + { + [HideLabel] + [SerializeReference] + [InfoBox("留空表示无条件开放。Key 使用小写字母、数字和下划线,例如 story_ch0_prologue_completed。可组合 All Of(AND) / Any Of(OR)。")] + public UnlockConditionNode root; + + /// + /// 使用当前玩家的 对规则树求值。 + /// + internal bool IsSatisfied(UnlockSaveModule unlockSaveModule, string ownerDescription) + { + return root == null || root.Evaluate(unlockSaveModule, ownerDescription); + } + } + + /// + /// 内容解锁条件树的基类。它只依赖已授予的 Key;不要把歌曲成绩、Yarn 变量或付费状态直接写入节点。 + /// 如需未来扩展条件来源,应在本树增加新的叶子节点,而不是让 UI 自行拼写判断。 + /// + [Serializable] + public abstract class UnlockConditionNode + { + /// + /// 对当前节点求值。返回 false 即表示内容保持锁定。 + /// + internal abstract bool Evaluate(UnlockSaveModule unlockSaveModule, string ownerDescription); + } + + /// + /// 叶子条件:玩家持有指定 Key 时满足。 + /// + [Serializable] + [LabelText("Key")] + [HideReferenceObjectPicker] + public sealed class UnlockKeyCondition : UnlockConditionNode + { + [HideLabel] + [InfoBox("只能使用小写字母、数字和下划线;例如 story_ch0_prologue_completed。Key 为空会使该内容保持锁定。")] + public string key; + + internal override bool Evaluate(UnlockSaveModule unlockSaveModule, string ownerDescription) + { + if (!UnlockSaveModule.IsValidKey(key)) + { + unlockSaveModule?.ReportInvalidRuleOnce(ownerDescription, $"key '{key}' is empty or malformed"); + return false; + } + + return unlockSaveModule != null && unlockSaveModule.HasKey(key); + } + } + + /// + /// 组合条件(AND):所有子条件都满足时开放。空列表或包含空节点视为配置错误并失败关闭。 + /// + [Serializable] + [LabelText("All Of (AND)")] + [HideReferenceObjectPicker] + public sealed class UnlockAllOfCondition : UnlockConditionNode + { + [HideLabel] + [SerializeReference] + [ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = true)] + public List conditions = new List(); + + internal override bool Evaluate(UnlockSaveModule unlockSaveModule, string ownerDescription) + { + if (conditions == null || conditions.Count == 0) + { + unlockSaveModule?.ReportInvalidRuleOnce(ownerDescription, "All Of has no child conditions"); + return false; + } + + foreach (UnlockConditionNode condition in conditions) + { + if (condition == null || !condition.Evaluate(unlockSaveModule, ownerDescription)) + { + if (condition == null) + { + unlockSaveModule?.ReportInvalidRuleOnce(ownerDescription, "All Of contains a null child condition"); + } + + return false; + } + } + + return true; + } + } + + /// + /// 组合条件(OR):任意子条件满足时开放。空列表或只包含空节点视为配置错误并失败关闭。 + /// + [Serializable] + [LabelText("Any Of (OR)")] + [HideReferenceObjectPicker] + public sealed class UnlockAnyOfCondition : UnlockConditionNode + { + [HideLabel] + [SerializeReference] + [ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = true)] + public List conditions = new List(); + + internal override bool Evaluate(UnlockSaveModule unlockSaveModule, string ownerDescription) + { + if (conditions == null || conditions.Count == 0) + { + unlockSaveModule?.ReportInvalidRuleOnce(ownerDescription, "Any Of has no child conditions"); + return false; + } + + bool hasValidChild = false; + foreach (UnlockConditionNode condition in conditions) + { + if (condition == null) + { + continue; + } + + hasValidChild = true; + if (condition.Evaluate(unlockSaveModule, ownerDescription)) + { + return true; + } + } + + if (!hasValidChild) + { + unlockSaveModule?.ReportInvalidRuleOnce(ownerDescription, "Any Of contains no valid child condition"); + } + + return false; + } + } +} diff --git a/Assets/Scripts/Saving/UnlockSaveModule.cs.meta b/Assets/Scripts/Saving/UnlockSaveModule.cs.meta new file mode 100644 index 00000000..073005a0 --- /dev/null +++ b/Assets/Scripts/Saving/UnlockSaveModule.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35e319c0c2854f0fb9c59af946ee8318 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UI/ChapterSelection/ChapterSelectionUI.cs b/Assets/Scripts/UI/ChapterSelection/ChapterSelectionUI.cs index b7f282db..8195bf8f 100644 --- a/Assets/Scripts/UI/ChapterSelection/ChapterSelectionUI.cs +++ b/Assets/Scripts/UI/ChapterSelection/ChapterSelectionUI.cs @@ -24,6 +24,11 @@ namespace Ichni.UI public ChapterSelectionUnit connectedChapter; public string chapterName; + /// + /// 当前章节的运行时锁定状态。仅用于 UI 表现;真正进入剧情或选曲前仍会重新调用 + /// ,不能把本字段作为授权依据。 + /// + public bool isLocked; public bool isExpanded; public bool isDuringAnimation; @@ -59,6 +64,7 @@ namespace Ichni.UI { connectedChapter = chapter; expansionRipple.material = new Material(rippleMaterial); + RefreshUnlockState(); expandButton.onClick.AddListener(() => { @@ -83,6 +89,11 @@ namespace Ichni.UI { return; } + + if (!CanEnterChapter()) + { + return; + } ChapterSelectionManager.instance.currentChapter = connectedChapter; AudioManager.SetSwitch(connectedChapter.chapterSwitch); @@ -97,6 +108,11 @@ namespace Ichni.UI { return; } + + if (!CanEnterChapter()) + { + return; + } ChapterSelectionManager.instance.currentChapter = connectedChapter; AudioManager.SetSwitch(connectedChapter.chapterSwitch); @@ -112,6 +128,36 @@ namespace Ichni.UI allPerfectText.text = allPerfectCount.ToString(); beatmapProgressText.text = $"{finishedBeatmapCount}/{beatmapCount}"; } + + /// + /// 根据统一内容解锁服务刷新章节入口的可交互状态。 + /// 当前 Prefab 尚无专用锁定美术,因此先禁用两个实际入口;后续补充锁图标时, + /// 只需读取 ,无需复制解锁判断。 + /// + private void RefreshUnlockState() + { + UnlockSaveModule unlockSaveModule = GameSaveManager.instance?.UnlockSaveModule; + isLocked = unlockSaveModule == null || !unlockSaveModule.CanAccessChapter(connectedChapter); + + if (enterStorylineButton != null) + { + enterStorylineButton.interactable = !isLocked; + } + + if (enterSongSelectionButton != null) + { + enterSongSelectionButton.interactable = !isLocked; + } + } + + /// + /// 章节的最终入口检查。即使 UI Button 状态被其它脚本意外恢复,也不能绕过解锁规则。 + /// + private bool CanEnterChapter() + { + RefreshUnlockState(); + return !isLocked; + } private void Expand() { @@ -197,4 +243,4 @@ namespace Ichni.UI shrinkSequence.Play(); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/UI/Elements/SelectionBox.cs b/Assets/Scripts/UI/Elements/SelectionBox.cs new file mode 100644 index 00000000..c882ce8d --- /dev/null +++ b/Assets/Scripts/UI/Elements/SelectionBox.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using DG.Tweening; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace Ichni.Menu.UI +{ + /// + /// 可复用的多选弹窗元素。调用方提供标题、内容与任意数量的 , + /// 本类动态实例化对应按钮,并确保一次显示最多只会执行一个选项回调。 + /// + public class SelectionBox : MonoBehaviour + { + [Header("Selection Box")] + public CanvasGroup canvasGroup; + public TMP_Text titleText; + public TMP_Text contentText; + public RectTransform optionContainer; + public SelectionBoxButton optionButtonPrefab; + + private readonly List _generatedButtons = new(); + private bool _isResolved; + + /// + /// 选项被选择并且自身淡出结束后触发。页面容器使用它销毁本次生成的 SelectionBox。 + /// + public event Action Closed; + + private void Awake() + { + canvasGroup ??= GetComponent(); + } + + /// + /// 根据运行时选项初始化弹窗。至少需要一个选项;失败时返回 false,且不会显示半配置的弹窗。 + /// + public bool SetUp(string title, string content, IReadOnlyList options) + { + if (canvasGroup == null || optionContainer == null || optionButtonPrefab == null || + options == null || options.Count == 0) + { + Debug.LogWarning("[SelectionBox] 缺少 CanvasGroup、选项容器、选项按钮预制体,或未提供任何选项。"); + return false; + } + + ClearGeneratedButtons(); + _isResolved = false; + + if (titleText != null) titleText.text = title ?? string.Empty; + if (contentText != null) contentText.text = content ?? string.Empty; + + foreach (SelectionBoxOption option in options) + { + SelectionBoxButton button = Instantiate(optionButtonPrefab, optionContainer); + _generatedButtons.Add(button); + button.SetUp(option.label, () => Select(option.action)); + } + + gameObject.SetActive(true); + canvasGroup.alpha = 0f; + canvasGroup.interactable = true; + canvasGroup.blocksRaycasts = true; + canvasGroup.DOFade(1f, 0.2f).Play(); + return true; + } + + private void Select(UnityAction action) + { + if (_isResolved) return; + _isResolved = true; + + // 先禁用所有输入,再执行回调。回调可以触发存档、场景切换或打开下一层 UI, + // 因而不能让同一帧内的其它按钮再次触发。 + canvasGroup.interactable = false; + canvasGroup.blocksRaycasts = false; + foreach (SelectionBoxButton button in _generatedButtons) + { + if (button?.button != null) + button.button.interactable = false; + } + + action?.Invoke(); + canvasGroup.DOFade(0f, 0.2f).OnComplete(() => Closed?.Invoke(this)).Play(); + } + + private void ClearGeneratedButtons() + { + foreach (SelectionBoxButton button in _generatedButtons) + { + if (button != null) + Destroy(button.gameObject); + } + + _generatedButtons.Clear(); + } + } + + /// + /// SelectionBox 的单个运行时选项。它不序列化到场景或存档;显示时由调用方临时创建。 + /// + public readonly struct SelectionBoxOption + { + public readonly string label; + public readonly UnityAction action; + + public SelectionBoxOption(string label, UnityAction action) + { + this.label = label; + this.action = action; + } + } +} diff --git a/Assets/Scripts/UI/Elements/SelectionBox.cs.meta b/Assets/Scripts/UI/Elements/SelectionBox.cs.meta new file mode 100644 index 00000000..78617c10 --- /dev/null +++ b/Assets/Scripts/UI/Elements/SelectionBox.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5ac6b73d890a4a7aaed8e2fa64001764 diff --git a/Assets/Scripts/UI/Elements/SelectionBoxButton.cs b/Assets/Scripts/UI/Elements/SelectionBoxButton.cs new file mode 100644 index 00000000..bfabece3 --- /dev/null +++ b/Assets/Scripts/UI/Elements/SelectionBoxButton.cs @@ -0,0 +1,40 @@ +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; + +namespace Ichni.Menu.UI +{ + /// + /// SelectionBox 动态生成的单个选项按钮。 + /// 预制体只需提供 Button 和 TMP 文本;运行时由 写入显示文字与点击回调。 + /// + public class SelectionBoxButton : MonoBehaviour + { + public Button button; + public TMP_Text labelText; + + /// + /// 重新绑定按钮文字与本次 SelectionBox 的选择回调。 + /// 每次生成时都会清空旧监听,防止对象复用或预制体事件造成重复触发。 + /// + public void SetUp(string label, UnityAction onSelected) + { + button ??= GetComponent