From 855659bf78651824db66a3ab0929c227cc7f792f Mon Sep 17 00:00:00 2001 From: "JSD\\13999" <1399945104@qq.com> Date: Tue, 23 Jun 2026 14:13:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(lucky-dice):=20=E6=8F=90=E4=BA=A4=20demo?= =?UTF-8?q?=20=E5=89=8D=E6=A0=B8=E5=BF=83=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 29 + FishDice/.gitignore | 101 ++ FishDice/Assets/FishDice.meta | 9 + FishDice/Assets/FishDice/Runtime.meta | 9 + .../FishDice/Runtime/FishDice.Runtime.asmdef | 14 + .../Runtime/FishDice.Runtime.asmdef.meta | 8 + .../Assets/FishDice/Runtime/LuckyDice.meta | 9 + .../FishDice/Runtime/LuckyDice/Actions.meta | 9 + .../Actions/DefaultRollActionExecutors.cs | 39 + .../DefaultRollActionExecutors.cs.meta | 12 + .../LuckyDice/Actions/IRollActionExecutor.cs | 9 + .../Actions/IRollActionExecutor.cs.meta | 12 + .../LuckyDice/Actions/RollActionDispatcher.cs | 58 ++ .../Actions/RollActionDispatcher.cs.meta | 12 + .../Actions/RollActionExecutorRegistry.cs | 17 + .../RollActionExecutorRegistry.cs.meta | 12 + .../FishDice/Runtime/LuckyDice/Config.meta | 9 + .../LuckyDice/Config/ComboRuleDefinition.cs | 33 + .../Config/ComboRuleDefinition.cs.meta | 12 + .../Config/ComboRuleDefinitionLoader.cs | 69 ++ .../Config/ComboRuleDefinitionLoader.cs.meta | 12 + .../Config/InvalidDiceSetException.cs | 15 + .../Config/InvalidDiceSetException.cs.meta | 11 + .../Config/InvalidLuckyDiceConfigException.cs | 25 + .../InvalidLuckyDiceConfigException.cs.meta | 11 + .../Config/LuckyDiceCandidateConfig.cs | 62 ++ .../Config/LuckyDiceCandidateConfig.cs.meta | 11 + .../Config/LuckyDiceCandidateDefinition.cs | 21 + .../LuckyDiceCandidateDefinition.cs.meta | 12 + .../LuckyDiceConfigValidationContext.cs | 28 + .../LuckyDiceConfigValidationContext.cs.meta | 11 + .../Config/LuckyDiceConfigValidator.cs | 254 +++++ .../Config/LuckyDiceConfigValidator.cs.meta | 11 + .../Config/LuckyDiceDefaultConfig.cs | 238 +++++ .../Config/LuckyDiceDefaultConfig.cs.meta | 12 + .../Runtime/LuckyDice/Config/LuckyDiceKeys.cs | 30 + .../LuckyDice/Config/LuckyDiceKeys.cs.meta | 12 + .../FishDice/Runtime/LuckyDice/CoreFlow.meta | 9 + .../CoreFlow/LuckyDiceRollFlowService.cs | 112 ++ .../CoreFlow/LuckyDiceRollFlowService.cs.meta | 12 + .../CoreFlow/LuckyDiceRuntimeFactory.cs | 59 ++ .../CoreFlow/LuckyDiceRuntimeFactory.cs.meta | 11 + .../FishDice/Runtime/LuckyDice/Data.meta | 9 + .../Runtime/LuckyDice/Data/ComboFacts.cs | 55 + .../Runtime/LuckyDice/Data/ComboFacts.cs.meta | 12 + .../LuckyDice/Data/ComboMatcherSpec.cs | 55 + .../LuckyDice/Data/ComboMatcherSpec.cs.meta | 12 + .../Runtime/LuckyDice/Data/ComboRule.cs | 33 + .../Runtime/LuckyDice/Data/ComboRule.cs.meta | 12 + .../Runtime/LuckyDice/Data/ComboRuleMatch.cs | 37 + .../LuckyDice/Data/ComboRuleMatch.cs.meta | 12 + .../Runtime/LuckyDice/Data/DiceFace.cs | 35 + .../Runtime/LuckyDice/Data/DiceFace.cs.meta | 12 + .../Runtime/LuckyDice/Data/DiceRollResult.cs | 81 ++ .../LuckyDice/Data/DiceRollResult.cs.meta | 12 + .../Runtime/LuckyDice/Data/DiceSetConfig.cs | 48 + .../LuckyDice/Data/DiceSetConfig.cs.meta | 12 + .../Runtime/LuckyDice/Data/DiceSetType.cs | 8 + .../LuckyDice/Data/DiceSetType.cs.meta | 12 + .../Runtime/LuckyDice/Data/LuckyDiceModels.cs | 127 +++ .../LuckyDice/Data/LuckyDiceModels.cs.meta | 12 + .../LuckyDice/Data/RollActionContext.cs | 20 + .../LuckyDice/Data/RollActionContext.cs.meta | 12 + .../LuckyDice/Data/RollActionResult.cs | 49 + .../LuckyDice/Data/RollActionResult.cs.meta | 12 + .../Runtime/LuckyDice/Data/RollActionSpec.cs | 26 + .../LuckyDice/Data/RollActionSpec.cs.meta | 12 + .../Runtime/LuckyDice/Data/RollEffects.cs | 67 ++ .../LuckyDice/Data/RollEffects.cs.meta | 12 + .../Runtime/LuckyDice/Data/RollFlowRequest.cs | 39 + .../LuckyDice/Data/RollFlowRequest.cs.meta | 12 + .../Runtime/LuckyDice/Data/RollFlowResult.cs | 49 + .../LuckyDice/Data/RollFlowResult.cs.meta | 12 + .../Runtime/LuckyDice/LuckySelection.meta | 9 + .../LuckySelection/LuckyDiceFlowService.cs | 76 ++ .../LuckyDiceFlowService.cs.meta | 12 + .../FishDice/Runtime/LuckyDice/Rules.meta | 9 + .../LuckyDice/Rules/ComboRuleMatcher.cs | 66 ++ .../LuckyDice/Rules/ComboRuleMatcher.cs.meta | 12 + .../Runtime/LuckyDice/Rules/Matchers.meta | 9 + .../Rules/Matchers/ComboMatcherRegistry.cs | 23 + .../Matchers/ComboMatcherRegistry.cs.meta | 12 + .../LuckyDice/Rules/Matchers/ComboMatchers.cs | 131 +++ .../Rules/Matchers/ComboMatchers.cs.meta | 12 + .../LuckyDice/Rules/Matchers/IComboMatcher.cs | 9 + .../Rules/Matchers/IComboMatcher.cs.meta | 12 + .../FishDice/Runtime/LuckyDice/Services.meta | 9 + .../LuckyDice/Services/ComboFactBuilder.cs | 55 + .../Services/ComboFactBuilder.cs.meta | 12 + .../LuckyDice/Services/DiceRollService.cs | 62 ++ .../Services/DiceRollService.cs.meta | 12 + .../Runtime/LuckyDice/TargetMode.meta | 9 + .../TargetMode/TargetModeResolver.cs | 33 + .../TargetMode/TargetModeResolver.cs.meta | 12 + .../FishDice/Runtime/LuckyDice/Trace.meta | 9 + .../Runtime/LuckyDice/Trace/RollTrace.cs | 121 +++ .../Runtime/LuckyDice/Trace/RollTrace.cs.meta | 12 + FishDice/Assets/FishDice/Tests.meta | 9 + FishDice/Assets/FishDice/Tests/EditMode.meta | 9 + .../EditMode/FishDice.Tests.EditMode.asmdef | 22 + .../FishDice.Tests.EditMode.asmdef.meta | 8 + .../FishDice/Tests/EditMode/LuckyDice.meta | 9 + .../LuckyDice/LuckyDiceCoreFlowTests.cs | 623 ++++++++++++ .../LuckyDice/LuckyDiceCoreFlowTests.cs.meta | 11 + .../LuckyDice/LuckyDiceSlice1Tests.cs | 255 +++++ .../LuckyDice/LuckyDiceSlice1Tests.cs.meta | 12 + ...y-dice-core-flow-architecture-blueprint.md | 487 +++++++++ FishDice/Docs/Decisions/README.md | 20 + ...001-separate-normal-and-lucky-dice-sets.md | 116 +++ ...-build-combo-facts-before-rule-matching.md | 117 +++ ...adr-003-dispatch-actions-by-action-type.md | 116 +++ ...004-inject-random-source-for-lucky-dice.md | 113 ++ FishDice/Docs/PRD/lucky-dice-core-flow-prd.md | 144 +++ .../lucky-dice-core-flow-feature-breakdown.md | 532 ++++++++++ .../lucky-dice-core-flow-requirements.md | 354 +++++++ .../Docs/Specs/lucky-dice-core-flow-spec.md | 765 ++++++++++++++ FishDice/Packages/manifest.json | 41 + FishDice/Packages/packages-lock.json | 382 +++++++ FishDice/ProjectSettings/AudioManager.asset | 19 + .../ProjectSettings/ClusterInputManager.asset | 6 + .../ProjectSettings/DynamicsManager.asset | 34 + .../ProjectSettings/EditorBuildSettings.asset | 11 + FishDice/ProjectSettings/EditorSettings.asset | 30 + .../ProjectSettings/GraphicsSettings.asset | 63 ++ FishDice/ProjectSettings/InputManager.asset | 295 ++++++ FishDice/ProjectSettings/MemorySettings.asset | 35 + FishDice/ProjectSettings/NavMeshAreas.asset | 91 ++ .../PackageManagerSettings.asset | 36 + .../Settings.json | 5 + .../ProjectSettings/Physics2DSettings.asset | 56 + FishDice/ProjectSettings/PresetManager.asset | 7 + .../ProjectSettings/ProjectSettings.asset | 961 ++++++++++++++++++ FishDice/ProjectSettings/ProjectVersion.txt | 2 + .../ProjectSettings/QualitySettings.asset | 234 +++++ FishDice/ProjectSettings/TagManager.asset | 43 + FishDice/ProjectSettings/TimeManager.asset | 9 + .../UnityConnectSettings.asset | 38 + FishDice/ProjectSettings/VFXManager.asset | 12 + .../VersionControlSettings.asset | 8 + FishDice/ProjectSettings/XRSettings.asset | 10 + 140 files changed, 9267 insertions(+) create mode 100644 .gitignore create mode 100644 FishDice/.gitignore create mode 100644 FishDice/Assets/FishDice.meta create mode 100644 FishDice/Assets/FishDice/Runtime.meta create mode 100644 FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef create mode 100644 FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/DefaultRollActionExecutors.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/DefaultRollActionExecutors.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/IRollActionExecutor.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/IRollActionExecutor.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionDispatcher.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionDispatcher.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionExecutorRegistry.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionExecutorRegistry.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinition.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinition.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinitionLoader.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinitionLoader.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidDiceSetException.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidDiceSetException.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidLuckyDiceConfigException.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidLuckyDiceConfigException.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateConfig.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateConfig.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateDefinition.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateDefinition.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidationContext.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidationContext.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidator.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidator.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceDefaultConfig.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceDefaultConfig.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceKeys.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceKeys.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRollFlowService.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRollFlowService.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRuntimeFactory.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRuntimeFactory.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboFacts.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboFacts.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboMatcherSpec.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboMatcherSpec.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRule.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRule.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRuleMatch.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRuleMatch.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceFace.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceFace.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceRollResult.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceRollResult.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetConfig.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetConfig.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetType.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetType.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/LuckyDiceModels.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/LuckyDiceModels.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionContext.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionContext.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionResult.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionResult.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionSpec.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionSpec.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollEffects.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollEffects.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowRequest.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowRequest.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowResult.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowResult.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection/LuckyDiceFlowService.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection/LuckyDiceFlowService.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/ComboRuleMatcher.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/ComboRuleMatcher.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatcherRegistry.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatcherRegistry.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatchers.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatchers.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/IComboMatcher.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/IComboMatcher.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Services.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Services/ComboFactBuilder.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Services/ComboFactBuilder.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Services/DiceRollService.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Services/DiceRollService.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode/TargetModeResolver.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode/TargetModeResolver.cs.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Trace.meta create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Trace/RollTrace.cs create mode 100644 FishDice/Assets/FishDice/Runtime/LuckyDice/Trace/RollTrace.cs.meta create mode 100644 FishDice/Assets/FishDice/Tests.meta create mode 100644 FishDice/Assets/FishDice/Tests/EditMode.meta create mode 100644 FishDice/Assets/FishDice/Tests/EditMode/FishDice.Tests.EditMode.asmdef create mode 100644 FishDice/Assets/FishDice/Tests/EditMode/FishDice.Tests.EditMode.asmdef.meta create mode 100644 FishDice/Assets/FishDice/Tests/EditMode/LuckyDice.meta create mode 100644 FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceCoreFlowTests.cs create mode 100644 FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceCoreFlowTests.cs.meta create mode 100644 FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceSlice1Tests.cs create mode 100644 FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceSlice1Tests.cs.meta create mode 100644 FishDice/Docs/Architecture/lucky-dice-core-flow-architecture-blueprint.md create mode 100644 FishDice/Docs/Decisions/README.md create mode 100644 FishDice/Docs/Decisions/adr-001-separate-normal-and-lucky-dice-sets.md create mode 100644 FishDice/Docs/Decisions/adr-002-build-combo-facts-before-rule-matching.md create mode 100644 FishDice/Docs/Decisions/adr-003-dispatch-actions-by-action-type.md create mode 100644 FishDice/Docs/Decisions/adr-004-inject-random-source-for-lucky-dice.md create mode 100644 FishDice/Docs/PRD/lucky-dice-core-flow-prd.md create mode 100644 FishDice/Docs/Plans/lucky-dice-core-flow-feature-breakdown.md create mode 100644 FishDice/Docs/Requirements/lucky-dice-core-flow-requirements.md create mode 100644 FishDice/Docs/Specs/lucky-dice-core-flow-spec.md create mode 100644 FishDice/Packages/manifest.json create mode 100644 FishDice/Packages/packages-lock.json create mode 100644 FishDice/ProjectSettings/AudioManager.asset create mode 100644 FishDice/ProjectSettings/ClusterInputManager.asset create mode 100644 FishDice/ProjectSettings/DynamicsManager.asset create mode 100644 FishDice/ProjectSettings/EditorBuildSettings.asset create mode 100644 FishDice/ProjectSettings/EditorSettings.asset create mode 100644 FishDice/ProjectSettings/GraphicsSettings.asset create mode 100644 FishDice/ProjectSettings/InputManager.asset create mode 100644 FishDice/ProjectSettings/MemorySettings.asset create mode 100644 FishDice/ProjectSettings/NavMeshAreas.asset create mode 100644 FishDice/ProjectSettings/PackageManagerSettings.asset create mode 100644 FishDice/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json create mode 100644 FishDice/ProjectSettings/Physics2DSettings.asset create mode 100644 FishDice/ProjectSettings/PresetManager.asset create mode 100644 FishDice/ProjectSettings/ProjectSettings.asset create mode 100644 FishDice/ProjectSettings/ProjectVersion.txt create mode 100644 FishDice/ProjectSettings/QualitySettings.asset create mode 100644 FishDice/ProjectSettings/TagManager.asset create mode 100644 FishDice/ProjectSettings/TimeManager.asset create mode 100644 FishDice/ProjectSettings/UnityConnectSettings.asset create mode 100644 FishDice/ProjectSettings/VFXManager.asset create mode 100644 FishDice/ProjectSettings/VersionControlSettings.asset create mode 100644 FishDice/ProjectSettings/XRSettings.asset diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b817ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +Confs +Client/ServerData +/assetbundles +.DS_Store +.vscode/ +/cfgPack/encryptcfg +.gemini/ +GEMINI.md +CLAUDE.md +AGENTS.md +QWEN.md +Client/.claude/ +dev-ab-svr/debugconfig.json +doc/ + +# Codex/AI local runtime and generated artifacts. +# CasualFishingSkills is the shared source repo; this game repo only keeps local installs/outputs. +/.agents/ +/.worktrees/ +/CodexAgent/ +/CodexOutput/ +/skills-lock.json + +# Legacy local output roots kept ignored while workflows migrate to CodexOutput. +/output/ +/outputs/ +/temp/ +/tmp/ +/FishingFrenzy/output/ diff --git a/FishDice/.gitignore b/FishDice/.gitignore new file mode 100644 index 0000000..fa4f46b --- /dev/null +++ b/FishDice/.gitignore @@ -0,0 +1,101 @@ +# This .gitignore file should be placed at the root of your Unity project directory +# +# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore +# +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Uu]ser[Ss]ettings/ +/[Bb]in/ +/SerializedBuildInfo/ + +# ignore all files and folders ends with "~" +*~ + +# MemoryCaptures can get excessive in size. +# They also could contain extremely sensitive data +/[Mm]emoryCaptures/ + +# Asset meta data should only be ignored when the corresponding asset is also ignored +!/[Aa]ssets/**/*.meta + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +/[Aa]ssets/Plugins/Editor/JetBrains* + +# Visual Studio cache directory +.vs/ + +# Gradle cache directory +.gradle/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db +.vsconfig +.vshistory +.vscode/ + +# Unity3D generated meta files +*.pidb.meta +*.pdb.meta +*.mdb.meta + +# Unity3D generated file on crash reports +sysinfo.txt + +# Builds +*.apk +*.unitypackage + +# Crashlytics generated file +crashlytics-build.properties + + +# ignore Test Scripts & resources in test folder +/[Aa]ssets/[Tt]emp[Tt]est/ +/[Aa]ssets/[Tt]emp[Tt]est +/[Aa]ssets/[Tt]emp[Tt]est.meta + + +# ignore build info from reporter +/[Aa]ssets/StreamingAssets/build_info/ +/[Aa]ssets/StreamingAssets/build_info +/[Aa]ssets/StreamingAssets/build_info.meta + +## ignore generated playfab async scripts +#/LocalPackages/asap.playfab.async/Runtime/Gen.meta +#/LocalPackages/asap.playfab.async/Runtime/Gen/ +#/LocalPackages/asap.playfab.async/Runtime/Gen + +# ignore core source code +/LocalPackages/GameCore/ +/LocalPackages/asap.core/ + +# HybridCLR Temp project +/HybridCLRData/StrippedAOTDllsTempProj + +# ignore idea files +.idea/ + +# ignore new slnx files +*.slnx diff --git a/FishDice/Assets/FishDice.meta b/FishDice/Assets/FishDice.meta new file mode 100644 index 0000000..4dbf16f --- /dev/null +++ b/FishDice/Assets/FishDice.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 5f266ba0b7dd4949b3e14abe82b5fc32 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime.meta b/FishDice/Assets/FishDice/Runtime.meta new file mode 100644 index 0000000..89c867e --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 678f1a9001a34e9eb1a79bcc58b8f347 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef b/FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef new file mode 100644 index 0000000..b6eb528 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef @@ -0,0 +1,14 @@ +{ + "name": "FishDice.Runtime", + "rootNamespace": "FishDice", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": true +} diff --git a/FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef.meta b/FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef.meta new file mode 100644 index 0000000..57567cb --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ea1dba60786d4669be927ea3ec48f6f3 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice.meta new file mode 100644 index 0000000..fa90dbb --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 62d522d99c944966acf214919da3046a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions.meta new file mode 100644 index 0000000..40a98e1 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 64a35bdcdd7447d49db30885a065cc3d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/DefaultRollActionExecutors.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/DefaultRollActionExecutors.cs new file mode 100644 index 0000000..fe7ee18 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/DefaultRollActionExecutors.cs @@ -0,0 +1,39 @@ +namespace FishDice.LuckyDice +{ + public sealed class GrantRewardExecutor : IRollActionExecutor + { + public string ActionType => LuckyDiceKeys.GrantRewardAction; + + public RollActionResult Execute(RollActionContext context, RollActionSpec action) + { + return RollActionResult.Succeeded( + action.ActionType, + new RollEffect[] { new RewardEffect() }); + } + } + + public sealed class AddMultiplierExecutor : IRollActionExecutor + { + public string ActionType => LuckyDiceKeys.AddMultiplierAction; + + public RollActionResult Execute(RollActionContext context, RollActionSpec action) + { + action.Params.TryGetValue("reason", out var reason); + return RollActionResult.Succeeded( + action.ActionType, + new RollEffect[] { new MultiplierEffect(reason) }); + } + } + + public sealed class TriggerLuckyDiceExecutor : IRollActionExecutor + { + public string ActionType => LuckyDiceKeys.TriggerLuckyDiceAction; + + public RollActionResult Execute(RollActionContext context, RollActionSpec action) + { + return RollActionResult.Succeeded( + action.ActionType, + new RollEffect[] { new LuckyDiceRequestedEffect() }); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/DefaultRollActionExecutors.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/DefaultRollActionExecutors.cs.meta new file mode 100644 index 0000000..f92fc12 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/DefaultRollActionExecutors.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: d2973688c08e47babf53588cd7d3e72c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/IRollActionExecutor.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/IRollActionExecutor.cs new file mode 100644 index 0000000..e4eb68b --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/IRollActionExecutor.cs @@ -0,0 +1,9 @@ +namespace FishDice.LuckyDice +{ + public interface IRollActionExecutor + { + string ActionType { get; } + + RollActionResult Execute(RollActionContext context, RollActionSpec action); + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/IRollActionExecutor.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/IRollActionExecutor.cs.meta new file mode 100644 index 0000000..fc9576f --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/IRollActionExecutor.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: a96e9c7c4cd442fa9e4f74d201c9cbdf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionDispatcher.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionDispatcher.cs new file mode 100644 index 0000000..491dc0c --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionDispatcher.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class RollActionDispatcher + { + private readonly Dictionary _executors; + + public RollActionDispatcher(IEnumerable executors) + { + if (executors == null) + { + throw new ArgumentNullException(nameof(executors)); + } + + _executors = executors.ToDictionary(executor => executor.ActionType, executor => executor); + } + + public IReadOnlyList Execute( + RollActionContext context, + IReadOnlyList actions) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (actions == null) + { + return Array.Empty(); + } + + var results = new List(actions.Count); + foreach (var action in actions) + { + var result = ExecuteSingle(context, action); + results.Add(result); + context.Trace.RecordActionResult(result); + } + + return results; + } + + private RollActionResult ExecuteSingle(RollActionContext context, RollActionSpec action) + { + if (!_executors.TryGetValue(action.ActionType, out var executor)) + { + return RollActionResult.Failure( + action.ActionType, + "No executor registered for action type: " + action.ActionType); + } + + return executor.Execute(context, action); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionDispatcher.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionDispatcher.cs.meta new file mode 100644 index 0000000..2f242b1 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionDispatcher.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 771e68c81ca14d2fb5998ac049f9ca5a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionExecutorRegistry.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionExecutorRegistry.cs new file mode 100644 index 0000000..0d32a51 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionExecutorRegistry.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace FishDice.LuckyDice +{ + public static class RollActionExecutorRegistry + { + public static IReadOnlyList CreateDefaultExecutors() + { + return new IRollActionExecutor[] + { + new GrantRewardExecutor(), + new AddMultiplierExecutor(), + new TriggerLuckyDiceExecutor() + }; + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionExecutorRegistry.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionExecutorRegistry.cs.meta new file mode 100644 index 0000000..beb891f --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Actions/RollActionExecutorRegistry.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 6bc162f33bbf4660b1599dce107dc223 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config.meta new file mode 100644 index 0000000..fb74c7e --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: d1eeebb9f2ae47058ce0524e8539b8cc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinition.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinition.cs new file mode 100644 index 0000000..6255540 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinition.cs @@ -0,0 +1,33 @@ +namespace FishDice.LuckyDice +{ + public sealed class ComboRuleDefinition + { + public string RuleId; + public int Priority; + public string MatcherType; + public string ComboKey; + public string FaceKey; + public int? RequiredCount; + public int? MinCount; + public int? MaxCount; + public int? DiceCount; + public int? OfAKindCount; + public int? MinSum; + public int? MaxSum; + public bool UseOrderedKey; + public bool StopAfterMatched = true; + public RollActionDefinition[] Actions; + } + + public sealed class RollActionDefinition + { + public string ActionType; + public ActionParamDefinition[] Params; + } + + public sealed class ActionParamDefinition + { + public string Key; + public string Value; + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinition.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinition.cs.meta new file mode 100644 index 0000000..934ff55 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinition.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: f1f14033988649cebb5429c594a53de9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinitionLoader.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinitionLoader.cs new file mode 100644 index 0000000..dfed7de --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinitionLoader.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public static class ComboRuleDefinitionLoader + { + public static IReadOnlyList Load(IReadOnlyList definitions) + { + if (definitions == null) + { + return Array.Empty(); + } + + return definitions.Select(ToRuntimeRule).ToArray(); + } + + private static ComboRule ToRuntimeRule(ComboRuleDefinition definition) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition)); + } + + return new ComboRule( + definition.RuleId, + definition.Priority, + new ComboMatcherSpec( + definition.MatcherType, + comboKey: definition.ComboKey, + faceKey: definition.FaceKey, + requiredCount: definition.RequiredCount, + minCount: definition.MinCount, + maxCount: definition.MaxCount, + diceCount: definition.DiceCount, + ofAKindCount: definition.OfAKindCount, + minSum: definition.MinSum, + maxSum: definition.MaxSum, + useOrderedKey: definition.UseOrderedKey), + ToRuntimeActions(definition.Actions), + definition.StopAfterMatched); + } + + private static IReadOnlyList ToRuntimeActions(IReadOnlyList definitions) + { + if (definitions == null) + { + return Array.Empty(); + } + + return definitions.Select(ToRuntimeAction).ToArray(); + } + + private static RollActionSpec ToRuntimeAction(RollActionDefinition definition) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition)); + } + + var parameters = definition.Params == null + ? null + : definition.Params.ToDictionary(param => param.Key, param => param.Value); + + return new RollActionSpec(definition.ActionType, parameters); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinitionLoader.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinitionLoader.cs.meta new file mode 100644 index 0000000..37900fd --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/ComboRuleDefinitionLoader.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: d8b93de4100240c792be3c214c06218c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidDiceSetException.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidDiceSetException.cs new file mode 100644 index 0000000..0f2d080 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidDiceSetException.cs @@ -0,0 +1,15 @@ +using System; + +namespace FishDice.LuckyDice +{ + public sealed class InvalidDiceSetException : Exception + { + public InvalidDiceSetException(string diceSetId) + : base("Invalid dice set id: " + diceSetId) + { + DiceSetId = diceSetId; + } + + public string DiceSetId { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidDiceSetException.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidDiceSetException.cs.meta new file mode 100644 index 0000000..d79d6e8 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidDiceSetException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b6f07727f4e84a88a31eb2d4caec3dc6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidLuckyDiceConfigException.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidLuckyDiceConfigException.cs new file mode 100644 index 0000000..ab81cb8 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidLuckyDiceConfigException.cs @@ -0,0 +1,25 @@ +using System; + +namespace FishDice.LuckyDice +{ + public sealed class InvalidLuckyDiceConfigException : Exception + { + public InvalidLuckyDiceConfigException(ConfigValidationResult validationResult) + : base(BuildMessage(validationResult)) + { + ValidationResult = validationResult; + } + + public ConfigValidationResult ValidationResult { get; } + + private static string BuildMessage(ConfigValidationResult validationResult) + { + if (validationResult == null || validationResult.Errors.Count == 0) + { + return "Invalid Lucky Dice config."; + } + + return "Invalid Lucky Dice config: " + validationResult.Errors[0].Code; + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidLuckyDiceConfigException.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidLuckyDiceConfigException.cs.meta new file mode 100644 index 0000000..c7598c0 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/InvalidLuckyDiceConfigException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56d5d29378624c30b6510846b4fd45e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateConfig.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateConfig.cs new file mode 100644 index 0000000..6af704b --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateConfig.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace FishDice.LuckyDice +{ + public sealed class LuckyDiceCandidateConfig + { + public LuckyDiceCandidateConfig( + string resultKey, + string targetKey, + int? resultSlotCount, + int weight, + bool enabled, + int minLevel, + IReadOnlyList sourceFilter, + bool isDefault) + { + ResultKey = resultKey; + TargetKey = targetKey; + ResultSlotCount = resultSlotCount; + Weight = weight; + Enabled = enabled; + MinLevel = minLevel; + SourceFilter = sourceFilter == null + ? Array.Empty() + : new List(sourceFilter).AsReadOnly(); + IsDefault = isDefault; + } + + public string ResultKey { get; } + + public string TargetKey { get; } + + public int? ResultSlotCount { get; } + + public int Weight { get; } + + public bool Enabled { get; } + + public int MinLevel { get; } + + public IReadOnlyList SourceFilter { get; } + + public bool IsDefault { get; } + } + + public sealed class TargetModeConfig + { + public TargetModeConfig(string resultKey, string targetKey, string modeKey) + { + ResultKey = resultKey; + TargetKey = targetKey; + ModeKey = modeKey; + } + + public string ResultKey { get; } + + public string TargetKey { get; } + + public string ModeKey { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateConfig.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateConfig.cs.meta new file mode 100644 index 0000000..0226549 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d22247243d584a8fa28b45e708517c1d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateDefinition.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateDefinition.cs new file mode 100644 index 0000000..765ac29 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateDefinition.cs @@ -0,0 +1,21 @@ +namespace FishDice.LuckyDice +{ + public sealed class LuckyDiceCandidateDefinition + { + public string ResultKey; + public string TargetKey; + public int? ResultSlotCount; + public int Weight; + public bool Enabled = true; + public int MinLevel; + public string[] SourceFilter; + public bool IsDefault; + } + + public sealed class TargetModeDefinition + { + public string ResultKey; + public string TargetKey; + public string ModeKey; + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateDefinition.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateDefinition.cs.meta new file mode 100644 index 0000000..031121b --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceCandidateDefinition.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 98f58d378176481380ec6413753c5717 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidationContext.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidationContext.cs new file mode 100644 index 0000000..4b0b80c --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidationContext.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class LuckyDiceConfigValidationContext + { + public LuckyDiceConfigValidationContext( + IReadOnlyList matchers, + IReadOnlyList actionExecutors) + { + Matchers = matchers == null ? Array.Empty() : matchers.ToArray(); + ActionExecutors = actionExecutors == null ? Array.Empty() : actionExecutors.ToArray(); + } + + public IReadOnlyList Matchers { get; } + + public IReadOnlyList ActionExecutors { get; } + + public static LuckyDiceConfigValidationContext CreateDefault() + { + return new LuckyDiceConfigValidationContext( + ComboMatcherRegistry.CreateDefaultMatchers(), + RollActionExecutorRegistry.CreateDefaultExecutors()); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidationContext.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidationContext.cs.meta new file mode 100644 index 0000000..fdf2ed8 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidationContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2bd13dcfd4a54d9fa90b11b60df1a7e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidator.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidator.cs new file mode 100644 index 0000000..94f4624 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidator.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public static class LuckyDiceConfigValidator + { + public static ConfigValidationResult Validate( + IReadOnlyList rules, + IReadOnlyList candidates, + IReadOnlyList targetModes, + IReadOnlyList matchers, + IReadOnlyList actionExecutors) + { + var errors = new List(); + var matcherTypes = ValidateMatchers(errors, matchers); + var actionTypes = ValidateActionExecutors(errors, actionExecutors); + var ruleIds = new HashSet(); + + foreach (var rule in rules ?? Array.Empty()) + { + if (rule == null) + { + errors.Add(new ConfigValidationError("NullRule", "Rule cannot be null.")); + continue; + } + + if (string.IsNullOrEmpty(rule.RuleId)) + { + errors.Add(new ConfigValidationError("EmptyRuleId", "Rule id is required.")); + } + else if (!ruleIds.Add(rule.RuleId)) + { + errors.Add(new ConfigValidationError("DuplicateRuleId", "Duplicate rule id: " + rule.RuleId)); + } + + if (!matcherTypes.Contains(rule.MatcherType)) + { + errors.Add(new ConfigValidationError("UnknownMatcher", "Unknown matcher: " + rule.MatcherType)); + } + + foreach (var action in rule.Actions ?? Array.Empty()) + { + if (action == null) + { + errors.Add(new ConfigValidationError("NullAction", "Action cannot be null.")); + continue; + } + + if (!actionTypes.Contains(action.ActionType)) + { + errors.Add(new ConfigValidationError("UnknownAction", "Unknown action: " + action.ActionType)); + } + + ValidateActionParams(errors, action.Params); + } + } + + if ((candidates ?? Array.Empty()).All(candidate => candidate == null || !candidate.IsDefault)) + { + errors.Add(new ConfigValidationError("MissingDefaultCandidate", "Lucky Dice needs one default candidate.")); + } + + var targetModeKeys = new HashSet(); + foreach (var targetMode in targetModes ?? Array.Empty()) + { + if (targetMode == null) + { + errors.Add(new ConfigValidationError("NullTargetMode", "Target mode cannot be null.")); + continue; + } + + if (string.IsNullOrEmpty(targetMode.ResultKey)) + { + errors.Add(new ConfigValidationError("EmptyTargetModeResultKey", "Target mode result key is required.")); + } + + if (string.IsNullOrEmpty(targetMode.TargetKey)) + { + errors.Add(new ConfigValidationError("EmptyTargetModeTargetKey", "Target mode target key is required.")); + } + + if (string.IsNullOrEmpty(targetMode.ModeKey)) + { + errors.Add(new ConfigValidationError("EmptyTargetModeModeKey", "Target mode mode key is required.")); + } + + var targetModeKey = targetMode.ResultKey + "|" + targetMode.TargetKey; + if (!targetModeKeys.Add(targetModeKey)) + { + errors.Add(new ConfigValidationError("DuplicateTargetMode", "Duplicate target mode: " + targetModeKey)); + } + } + + var candidateKeys = new HashSet(); + foreach (var candidate in candidates ?? Array.Empty()) + { + if (candidate == null) + { + errors.Add(new ConfigValidationError("NullCandidate", "Candidate cannot be null.")); + continue; + } + + if (string.IsNullOrEmpty(candidate.ResultKey)) + { + errors.Add(new ConfigValidationError("EmptyCandidateResultKey", "Candidate result key is required.")); + } + + if (string.IsNullOrEmpty(candidate.TargetKey)) + { + errors.Add(new ConfigValidationError("EmptyCandidateTargetKey", "Candidate target key is required.")); + } + + var candidateKey = candidate.ResultKey + "|" + candidate.TargetKey; + if (!candidateKeys.Add(candidateKey)) + { + errors.Add(new ConfigValidationError("DuplicateCandidate", "Duplicate candidate: " + candidateKey)); + } + + if (candidate.Weight < 0) + { + errors.Add(new ConfigValidationError("NegativeCandidateWeight", "Candidate weight cannot be negative: " + candidate.ResultKey)); + } + + if (candidate.ResultSlotCount.HasValue && candidate.ResultSlotCount.Value <= 0) + { + errors.Add(new ConfigValidationError("InvalidResultSlotCount", "Result slot count must be greater than zero: " + candidate.ResultKey)); + } + + if (candidate.IsDefault && !candidate.Enabled) + { + errors.Add(new ConfigValidationError("DisabledDefaultCandidate", "Default candidate cannot be disabled: " + candidate.ResultKey)); + } + + var hasMode = (targetModes ?? Array.Empty()) + .Any(mode => mode != null && mode.ResultKey == candidate.ResultKey && mode.TargetKey == candidate.TargetKey); + if (!hasMode) + { + errors.Add(new ConfigValidationError("MissingTargetMode", "Missing target mode for result: " + candidate.ResultKey)); + } + } + + return new ConfigValidationResult(errors); + } + + private static HashSet ValidateMatchers( + List errors, + IReadOnlyList matchers) + { + var matcherTypes = new HashSet(); + foreach (var matcher in matchers ?? Array.Empty()) + { + if (matcher == null) + { + errors.Add(new ConfigValidationError("NullMatcher", "Matcher cannot be null.")); + continue; + } + + if (string.IsNullOrEmpty(matcher.MatcherType)) + { + errors.Add(new ConfigValidationError("EmptyMatcherType", "Matcher type is required.")); + continue; + } + + if (!matcherTypes.Add(matcher.MatcherType)) + { + errors.Add(new ConfigValidationError("DuplicateMatcherType", "Duplicate matcher type: " + matcher.MatcherType)); + } + } + + return matcherTypes; + } + + private static HashSet ValidateActionExecutors( + List errors, + IReadOnlyList actionExecutors) + { + var actionTypes = new HashSet(); + foreach (var executor in actionExecutors ?? Array.Empty()) + { + if (executor == null) + { + errors.Add(new ConfigValidationError("NullActionExecutor", "Action executor cannot be null.")); + continue; + } + + if (string.IsNullOrEmpty(executor.ActionType)) + { + errors.Add(new ConfigValidationError("EmptyActionExecutorType", "Action executor type is required.")); + continue; + } + + if (!actionTypes.Add(executor.ActionType)) + { + errors.Add(new ConfigValidationError("DuplicateActionExecutorType", "Duplicate action executor type: " + executor.ActionType)); + } + } + + return actionTypes; + } + + private static void ValidateActionParams( + List errors, + IReadOnlyList actionParams) + { + var paramKeys = new HashSet(); + foreach (var param in actionParams ?? Array.Empty()) + { + if (param == null) + { + errors.Add(new ConfigValidationError("NullActionParam", "Action param cannot be null.")); + continue; + } + + if (string.IsNullOrEmpty(param.Key)) + { + errors.Add(new ConfigValidationError("EmptyActionParamKey", "Action param key is required.")); + continue; + } + + if (!paramKeys.Add(param.Key)) + { + errors.Add(new ConfigValidationError("DuplicateActionParamKey", "Duplicate action param key: " + param.Key)); + } + } + } + } + + public sealed class ConfigValidationResult + { + public ConfigValidationResult(IReadOnlyList errors) + { + Errors = errors == null ? Array.Empty() : errors.ToArray(); + } + + public IReadOnlyList Errors { get; } + + public bool IsValid => Errors.Count == 0; + } + + public sealed class ConfigValidationError + { + public ConfigValidationError(string code, string message) + { + Code = code; + Message = message; + } + + public string Code { get; } + + public string Message { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidator.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidator.cs.meta new file mode 100644 index 0000000..38f8c1b --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceConfigValidator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 31bec0fb5e6c45ddadf3a0039a287423 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceDefaultConfig.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceDefaultConfig.cs new file mode 100644 index 0000000..6918c54 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceDefaultConfig.cs @@ -0,0 +1,238 @@ +using System.Collections.Generic; + +namespace FishDice.LuckyDice +{ + public sealed class LuckyDiceDefaultConfig + { + private readonly Dictionary _diceSets; + + private LuckyDiceDefaultConfig( + Dictionary diceSets, + IReadOnlyList comboRules, + IReadOnlyList luckyDiceCandidates, + IReadOnlyList targetModeDefinitions) + { + _diceSets = diceSets; + ComboRules = comboRules; + LuckyDiceCandidates = luckyDiceCandidates; + TargetModeDefinitions = targetModeDefinitions; + } + + public IReadOnlyList ComboRules { get; } + + public IReadOnlyList LuckyDiceCandidates { get; } + + public IReadOnlyList TargetModeDefinitions { get; } + + public DiceSetConfig GetDiceSet(string diceSetId) + { + DiceSetConfig diceSet; + if (!_diceSets.TryGetValue(diceSetId, out diceSet)) + { + throw new InvalidDiceSetException(diceSetId); + } + + return diceSet; + } + + public static LuckyDiceDefaultConfig Create() + { + return CreateValidated(LuckyDiceConfigValidationContext.CreateDefault()); + } + + public static LuckyDiceDefaultConfig CreateValidated(LuckyDiceConfigValidationContext validationContext) + { + return CreateValidated( + DefaultComboRuleDefinitions, + DefaultLuckyDiceCandidateDefinitions, + DefaultTargetModeDefinitions, + validationContext); + } + + public static LuckyDiceDefaultConfig CreateValidated( + IReadOnlyList ruleDefinitions, + IReadOnlyList candidateDefinitions, + IReadOnlyList targetModeDefinitions) + { + return CreateValidated( + ruleDefinitions, + candidateDefinitions, + targetModeDefinitions, + LuckyDiceConfigValidationContext.CreateDefault()); + } + + public static LuckyDiceDefaultConfig CreateValidated( + IReadOnlyList ruleDefinitions, + IReadOnlyList candidateDefinitions, + IReadOnlyList targetModeDefinitions, + LuckyDiceConfigValidationContext validationContext) + { + validationContext = validationContext ?? LuckyDiceConfigValidationContext.CreateDefault(); + var validation = LuckyDiceConfigValidator.Validate( + ruleDefinitions, + candidateDefinitions, + targetModeDefinitions, + validationContext.Matchers, + validationContext.ActionExecutors); + if (!validation.IsValid) + { + throw new InvalidLuckyDiceConfigException(validation); + } + + var normalSet = new DiceSetConfig( + LuckyDiceKeys.NormalDiceSetId, + DiceSetType.Normal, + diceCount: 2, + faces: new[] + { + DiceFace.NormalNumber(2), + DiceFace.NormalNumber(3), + DiceFace.NormalNumber(4), + DiceFace.NormalNumber(5), + DiceFace.NormalNumber(6), + DiceFace.NormalClover() + }, + defaultResultSlotCount: null); + + var luckySet = new DiceSetConfig( + LuckyDiceKeys.LuckyDiceSetId, + DiceSetType.Lucky, + diceCount: 1, + faces: new[] + { + DiceFace.LuckyResult(LuckyDiceKeys.RocketResultKey), + DiceFace.LuckyResult(LuckyDiceKeys.ThiefResultKey) + }, + defaultResultSlotCount: 3); + + return new LuckyDiceDefaultConfig( + new Dictionary + { + { normalSet.DiceSetId, normalSet }, + { luckySet.DiceSetId, luckySet } + }, + ComboRuleDefinitionLoader.Load(ruleDefinitions), + ToCandidateConfigs(candidateDefinitions), + ToTargetModeConfigs(targetModeDefinitions)); + } + + private static LuckyDiceCandidateConfig[] ToCandidateConfigs(IReadOnlyList definitions) + { + var result = new LuckyDiceCandidateConfig[definitions.Count]; + for (var i = 0; i < definitions.Count; i++) + { + var definition = definitions[i]; + result[i] = new LuckyDiceCandidateConfig( + definition.ResultKey, + definition.TargetKey, + definition.ResultSlotCount, + definition.Weight, + definition.Enabled, + definition.MinLevel, + definition.SourceFilter, + definition.IsDefault); + } + + return result; + } + + private static TargetModeConfig[] ToTargetModeConfigs(IReadOnlyList definitions) + { + var result = new TargetModeConfig[definitions.Count]; + for (var i = 0; i < definitions.Count; i++) + { + var definition = definitions[i]; + result[i] = new TargetModeConfig(definition.ResultKey, definition.TargetKey, definition.ModeKey); + } + + return result; + } + + private static readonly ComboRuleDefinition[] DefaultComboRuleDefinitions = + { + new ComboRuleDefinition + { + RuleId = "normal_clover_clover_lucky_dice", + Priority = 100, + MatcherType = LuckyDiceKeys.ExactComboMatcher, + ComboKey = "clover_clover", + Actions = new[] + { + new RollActionDefinition { ActionType = LuckyDiceKeys.TriggerLuckyDiceAction } + } + }, + new ComboRuleDefinition + { + RuleId = "normal_number_clover_bonus", + Priority = 50, + MatcherType = LuckyDiceKeys.ContainsFaceMatcher, + FaceKey = LuckyDiceKeys.CloverFaceKey, + RequiredCount = 1, + Actions = new[] + { + new RollActionDefinition { ActionType = LuckyDiceKeys.GrantRewardAction }, + new RollActionDefinition + { + ActionType = LuckyDiceKeys.AddMultiplierAction, + Params = new[] + { + new ActionParamDefinition { Key = "reason", Value = "clover_bonus" } + } + } + } + }, + new ComboRuleDefinition + { + RuleId = "normal_all_numbers_reward", + Priority = 10, + MatcherType = LuckyDiceKeys.AllNumbersMatcher, + Actions = new[] + { + new RollActionDefinition { ActionType = LuckyDiceKeys.GrantRewardAction } + } + } + }; + + private static readonly LuckyDiceCandidateDefinition[] DefaultLuckyDiceCandidateDefinitions = + { + new LuckyDiceCandidateDefinition + { + ResultKey = LuckyDiceKeys.RocketResultKey, + TargetKey = "slap_down", + ResultSlotCount = 3, + Weight = 1, + Enabled = true, + MinLevel = 0, + SourceFilter = new[] { LuckyDiceKeys.NormalRollTriggerSource }, + IsDefault = true + }, + new LuckyDiceCandidateDefinition + { + ResultKey = LuckyDiceKeys.ThiefResultKey, + TargetKey = "treasure_heist", + ResultSlotCount = 3, + Weight = 1, + Enabled = true, + MinLevel = 0, + SourceFilter = new[] { LuckyDiceKeys.NormalRollTriggerSource }, + IsDefault = false + } + }; + + private static readonly TargetModeDefinition[] DefaultTargetModeDefinitions = + { + new TargetModeDefinition + { + ResultKey = LuckyDiceKeys.RocketResultKey, + TargetKey = "slap_down", + ModeKey = "slap_down_normal" + }, + new TargetModeDefinition + { + ResultKey = LuckyDiceKeys.ThiefResultKey, + TargetKey = "treasure_heist", + ModeKey = "treasure_heist_normal" + } + }; + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceDefaultConfig.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceDefaultConfig.cs.meta new file mode 100644 index 0000000..140a590 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceDefaultConfig.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 2872fbab847848f9bf968e717a4acd70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceKeys.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceKeys.cs new file mode 100644 index 0000000..ed23436 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceKeys.cs @@ -0,0 +1,30 @@ +namespace FishDice.LuckyDice +{ + public static class LuckyDiceKeys + { + public const string NormalDiceSetId = "normal_main"; + public const string LuckyDiceSetId = "lucky_dice"; + + public const string CloverFaceKey = "clover"; + public const string RocketResultKey = "rocket"; + public const string ThiefResultKey = "thief"; + + public const string NormalRollTriggerSource = "normal_roll"; + + public const string ExactComboMatcher = "ExactCombo"; + public const string AllNumbersMatcher = "AllNumbers"; + public const string ContainsFaceMatcher = "ContainsFace"; + public const string FaceCountMatcher = "FaceCount"; + public const string FaceCountRangeMatcher = "FaceCountRange"; + public const string DiceCountMatcher = "DiceCount"; + public const string NumberPairMatcher = "NumberPair"; + public const string NumberOfAKindMatcher = "NumberOfAKind"; + public const string NumberSumRangeMatcher = "NumberSumRange"; + + public const string GrantRewardAction = "grant_reward"; + public const string AddMultiplierAction = "add_multiplier"; + public const string TriggerLuckyDiceAction = "trigger_lucky_dice"; + public const string EnterModeAction = "enter_mode"; + public const string ShowPopupAction = "show_popup"; + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceKeys.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceKeys.cs.meta new file mode 100644 index 0000000..3c4a1a0 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Config/LuckyDiceKeys.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 3a0439dddf734a14a1f49eeae48e349b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow.meta new file mode 100644 index 0000000..61e0bb9 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 2867533dcede4ee9b8222a2b4d63deab +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRollFlowService.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRollFlowService.cs new file mode 100644 index 0000000..c148414 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRollFlowService.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class LuckyDiceRollFlowService + { + private readonly LuckyDiceDefaultConfig _config; + private readonly DiceRollService _rollService; + private readonly ComboRuleMatcher _ruleMatcher; + private readonly RollActionDispatcher _actionDispatcher; + private readonly LuckyDiceFlowService _luckyDiceFlowService; + private readonly TargetModeResolver _targetModeResolver; + + public LuckyDiceRollFlowService( + LuckyDiceDefaultConfig config, + DiceRollService rollService, + ComboRuleMatcher ruleMatcher, + RollActionDispatcher actionDispatcher, + LuckyDiceFlowService luckyDiceFlowService, + TargetModeResolver targetModeResolver) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + _rollService = rollService ?? throw new ArgumentNullException(nameof(rollService)); + _ruleMatcher = ruleMatcher ?? throw new ArgumentNullException(nameof(ruleMatcher)); + _actionDispatcher = actionDispatcher ?? throw new ArgumentNullException(nameof(actionDispatcher)); + _luckyDiceFlowService = luckyDiceFlowService ?? throw new ArgumentNullException(nameof(luckyDiceFlowService)); + _targetModeResolver = targetModeResolver ?? throw new ArgumentNullException(nameof(targetModeResolver)); + } + + public RollFlowResult Execute(RollFlowRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.RollSessionId)) + { + throw new ArgumentException("Roll session id is required.", nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.DiceSetId)) + { + throw new ArgumentException("Dice set id is required.", nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.TriggerSource)) + { + throw new ArgumentException("Trigger source is required.", nameof(request)); + } + + var trace = new RollTrace(request.RollSessionId); + var rollResult = _rollService.Roll( + _config.GetDiceSet(request.DiceSetId), + request.RollSessionId, + request.TriggerSource); + var facts = ComboFactBuilder.Build(rollResult); + trace.RecordComboFacts(facts); + + var match = _ruleMatcher.Match(facts, _config.ComboRules); + trace.RecordRuleMatch(match); + + var actionContext = new RollActionContext(request.RollSessionId, facts.NormalizedKey, trace); + var actionResults = _actionDispatcher.Execute( + actionContext, + match.Matches.SelectMany(rule => rule.Actions).ToArray()); + var effects = actionResults.SelectMany(result => result.Effects).ToList(); + + LuckyDiceSelectionResult luckyDiceResult = null; + TargetModeEntry targetMode = null; + if (effects.OfType().Any()) + { + luckyDiceResult = _luckyDiceFlowService.Select( + _config.LuckyDiceCandidates, + request, + _config.GetDiceSet(LuckyDiceKeys.LuckyDiceSetId)); + trace.RecordLuckyDiceSelection(luckyDiceResult); + + var targetModeResult = _targetModeResolver.Resolve( + luckyDiceResult.Selected, + request, + rollResult.Multiplier, + _config.TargetModeDefinitions); + + if (targetModeResult.Success) + { + targetMode = targetModeResult.Entry; + trace.RecordTargetMode(targetMode); + effects.Add(new EnterTargetModeEffect(targetMode.TargetKey, targetMode.ModeKey)); + } + else + { + trace.RecordTargetModeFailure(targetModeResult.FailureReason); + effects.Add(new TargetModeFailedEffect(targetModeResult.FailureReason, targetModeResult.FallbackMode)); + } + } + + return new RollFlowResult( + request.RollSessionId, + rollResult, + facts, + match.Matches, + actionResults, + effects, + luckyDiceResult, + targetMode, + trace); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRollFlowService.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRollFlowService.cs.meta new file mode 100644 index 0000000..4ff804c --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRollFlowService.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 1934c9ee9d2f40d897afde268a169778 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRuntimeFactory.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRuntimeFactory.cs new file mode 100644 index 0000000..d14108d --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRuntimeFactory.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace FishDice.LuckyDice +{ + public sealed class LuckyDiceRuntimeFactory + { + private readonly LuckyDiceConfigValidationContext _context; + private readonly IRandomSource _random; + + public LuckyDiceRuntimeFactory( + LuckyDiceConfigValidationContext context, + IRandomSource random) + { + _context = context ?? LuckyDiceConfigValidationContext.CreateDefault(); + _random = random ?? throw new ArgumentNullException(nameof(random)); + } + + public static LuckyDiceRuntimeFactory CreateDefault(IRandomSource random) + { + return new LuckyDiceRuntimeFactory(LuckyDiceConfigValidationContext.CreateDefault(), random); + } + + public LuckyDiceRollFlowService CreateDefaultFlow() + { + return CreateFlow(LuckyDiceDefaultConfig.CreateValidated(_context)); + } + + public LuckyDiceRollFlowService CreateFlow( + IReadOnlyList ruleDefinitions, + IReadOnlyList candidateDefinitions, + IReadOnlyList targetModeDefinitions) + { + var config = LuckyDiceDefaultConfig.CreateValidated( + ruleDefinitions, + candidateDefinitions, + targetModeDefinitions, + _context); + + return CreateFlow(config); + } + + public LuckyDiceRollFlowService CreateFlow(LuckyDiceDefaultConfig config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + return new LuckyDiceRollFlowService( + config, + new DiceRollService(_random), + new ComboRuleMatcher(_context.Matchers), + new RollActionDispatcher(_context.ActionExecutors), + new LuckyDiceFlowService(_random), + new TargetModeResolver()); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRuntimeFactory.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRuntimeFactory.cs.meta new file mode 100644 index 0000000..331e886 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/CoreFlow/LuckyDiceRuntimeFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19d0cc9a1e284be7a23c2ee1c6b7bd21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data.meta new file mode 100644 index 0000000..d82078c --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 1ff1f71d729f48438f1a81077076ca5a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboFacts.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboFacts.cs new file mode 100644 index 0000000..5457271 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboFacts.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class ComboFacts + { + public ComboFacts( + string rollSessionId, + DiceSetType diceSetType, + int diceCount, + IReadOnlyList faces, + IReadOnlyDictionary faceCounts, + int numberSum, + IReadOnlyList numberValues, + int cloverCount, + string normalizedKey, + string orderedKey) + { + RollSessionId = rollSessionId; + DiceSetType = diceSetType; + DiceCount = diceCount; + Faces = faces.ToArray(); + FaceCounts = faceCounts.ToDictionary(pair => pair.Key, pair => pair.Value); + NumberSum = numberSum; + NumberValues = numberValues.ToArray(); + CloverCount = cloverCount; + HasClover = cloverCount > 0; + NormalizedKey = normalizedKey; + OrderedKey = orderedKey; + } + + public string RollSessionId { get; } + + public DiceSetType DiceSetType { get; } + + public int DiceCount { get; } + + public IReadOnlyList Faces { get; } + + public IReadOnlyDictionary FaceCounts { get; } + + public int NumberSum { get; } + + public IReadOnlyList NumberValues { get; } + + public int CloverCount { get; } + + public bool HasClover { get; } + + public string NormalizedKey { get; } + + public string OrderedKey { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboFacts.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboFacts.cs.meta new file mode 100644 index 0000000..dc00d77 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboFacts.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 74faceaa58a144008abe8f08f3b3db27 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboMatcherSpec.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboMatcherSpec.cs new file mode 100644 index 0000000..28cc8d0 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboMatcherSpec.cs @@ -0,0 +1,55 @@ +using System; + +namespace FishDice.LuckyDice +{ + public sealed class ComboMatcherSpec + { + public ComboMatcherSpec( + string matcherType, + string comboKey = null, + string faceKey = null, + int? requiredCount = null, + int? minCount = null, + int? maxCount = null, + int? diceCount = null, + int? ofAKindCount = null, + int? minSum = null, + int? maxSum = null, + bool useOrderedKey = false) + { + MatcherType = matcherType ?? throw new ArgumentNullException(nameof(matcherType)); + ComboKey = comboKey; + FaceKey = faceKey; + RequiredCount = requiredCount; + MinCount = minCount; + MaxCount = maxCount; + DiceCount = diceCount; + OfAKindCount = ofAKindCount; + MinSum = minSum; + MaxSum = maxSum; + UseOrderedKey = useOrderedKey; + } + + public string MatcherType { get; } + + public string ComboKey { get; } + + public string FaceKey { get; } + + public int? RequiredCount { get; } + + public int? MinCount { get; } + + public int? MaxCount { get; } + + public int? DiceCount { get; } + + public int? OfAKindCount { get; } + + public int? MinSum { get; } + + public int? MaxSum { get; } + + public bool UseOrderedKey { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboMatcherSpec.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboMatcherSpec.cs.meta new file mode 100644 index 0000000..0dda45f --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboMatcherSpec.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 11a380073489461c997f9ce85c45d607 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRule.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRule.cs new file mode 100644 index 0000000..8a918ee --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRule.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class ComboRule + { + public ComboRule( + string ruleId, + int priority, + ComboMatcherSpec matcher, + IReadOnlyList actions, + bool stopAfterMatched) + { + RuleId = ruleId ?? throw new ArgumentNullException(nameof(ruleId)); + Priority = priority; + Matcher = matcher ?? throw new ArgumentNullException(nameof(matcher)); + Actions = actions == null ? Array.Empty() : actions.ToArray(); + StopAfterMatched = stopAfterMatched; + } + + public string RuleId { get; } + + public int Priority { get; } + + public ComboMatcherSpec Matcher { get; } + + public IReadOnlyList Actions { get; } + + public bool StopAfterMatched { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRule.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRule.cs.meta new file mode 100644 index 0000000..89beef9 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRule.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: a94f5fa117fb4bada6467d3d7d0c31d2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRuleMatch.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRuleMatch.cs new file mode 100644 index 0000000..350448d --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRuleMatch.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class ComboRuleMatch + { + public ComboRuleMatch(ComboRule rule) + { + RuleId = rule.RuleId; + Priority = rule.Priority; + MatcherType = rule.Matcher.MatcherType; + Actions = rule.Actions; + } + + public string RuleId { get; } + + public int Priority { get; } + + public string MatcherType { get; } + + public IReadOnlyList Actions { get; } + } + + public sealed class ComboRuleMatchResult + { + public ComboRuleMatchResult(IReadOnlyList matches) + { + Matches = matches == null ? Array.Empty() : matches.ToArray(); + } + + public IReadOnlyList Matches { get; } + + public bool HasMatch => Matches.Count > 0; + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRuleMatch.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRuleMatch.cs.meta new file mode 100644 index 0000000..55eac75 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/ComboRuleMatch.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: d9df28fce97b4d4fa5cf29a5b6f38d23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceFace.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceFace.cs new file mode 100644 index 0000000..879deed --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceFace.cs @@ -0,0 +1,35 @@ +using System; + +namespace FishDice.LuckyDice +{ + public readonly struct DiceFace + { + public DiceFace(DiceSetType setType, string key, int? numberValue) + { + SetType = setType; + Key = key ?? throw new ArgumentNullException(nameof(key)); + NumberValue = numberValue; + } + + public DiceSetType SetType { get; } + + public string Key { get; } + + public int? NumberValue { get; } + + public static DiceFace NormalNumber(int value) + { + return new DiceFace(DiceSetType.Normal, value.ToString(), value); + } + + public static DiceFace NormalClover() + { + return new DiceFace(DiceSetType.Normal, LuckyDiceKeys.CloverFaceKey, null); + } + + public static DiceFace LuckyResult(string key) + { + return new DiceFace(DiceSetType.Lucky, key, null); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceFace.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceFace.cs.meta new file mode 100644 index 0000000..a7c2379 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceFace.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9175dd08e215408faa01a3ee1cc86a13 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceRollResult.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceRollResult.cs new file mode 100644 index 0000000..453511d --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceRollResult.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class DiceRollResult + { + private DiceRollResult( + string rollSessionId, + string diceSetId, + DiceSetType setType, + int diceCount, + IReadOnlyList faces, + int multiplier, + string triggerSource) + { + RollSessionId = rollSessionId; + DiceSetId = diceSetId; + SetType = setType; + DiceCount = diceCount; + Faces = faces.ToArray(); + Multiplier = multiplier; + TriggerSource = triggerSource; + } + + public string RollSessionId { get; } + + public string DiceSetId { get; } + + public DiceSetType SetType { get; } + + public int DiceCount { get; } + + public IReadOnlyList Faces { get; } + + public int Multiplier { get; } + + public string TriggerSource { get; } + + public static DiceRollResult Create( + string rollSessionId, + string diceSetId, + DiceSetType setType, + IReadOnlyList faces, + int multiplier, + string triggerSource) + { + if (faces == null) + { + throw new ArgumentNullException(nameof(faces)); + } + + return new DiceRollResult( + rollSessionId, + diceSetId, + setType, + faces.Count, + faces, + multiplier, + triggerSource); + } + + public static DiceRollResult FromConfig( + string rollSessionId, + DiceSetConfig config, + IReadOnlyList faces, + int multiplier, + string triggerSource) + { + return new DiceRollResult( + rollSessionId, + config.DiceSetId, + config.SetType, + config.DiceCount, + faces, + multiplier, + triggerSource); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceRollResult.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceRollResult.cs.meta new file mode 100644 index 0000000..ba08810 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceRollResult.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 839ecc30eed6498d93ce34d9ffe847eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetConfig.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetConfig.cs new file mode 100644 index 0000000..99dc67f --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetConfig.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class DiceSetConfig + { + public DiceSetConfig( + string diceSetId, + DiceSetType setType, + int diceCount, + IReadOnlyList faces, + int? defaultResultSlotCount) + { + if (string.IsNullOrWhiteSpace(diceSetId)) + { + throw new ArgumentException("Dice set id is required.", nameof(diceSetId)); + } + + if (diceCount <= 0) + { + throw new ArgumentOutOfRangeException(nameof(diceCount), "Dice count must be greater than zero."); + } + + if (faces == null || faces.Count == 0) + { + throw new ArgumentException("At least one face is required.", nameof(faces)); + } + + DiceSetId = diceSetId; + SetType = setType; + DiceCount = diceCount; + Faces = faces.ToArray(); + DefaultResultSlotCount = defaultResultSlotCount; + } + + public string DiceSetId { get; } + + public DiceSetType SetType { get; } + + public int DiceCount { get; } + + public IReadOnlyList Faces { get; } + + public int? DefaultResultSlotCount { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetConfig.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetConfig.cs.meta new file mode 100644 index 0000000..9f05956 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetConfig.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 6c890293bb854609a2b916d1d35e6063 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetType.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetType.cs new file mode 100644 index 0000000..5c2e047 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetType.cs @@ -0,0 +1,8 @@ +namespace FishDice.LuckyDice +{ + public enum DiceSetType + { + Normal, + Lucky + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetType.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetType.cs.meta new file mode 100644 index 0000000..661696e --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/DiceSetType.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 0147bf70503045fbb65baec35ba74936 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/LuckyDiceModels.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/LuckyDiceModels.cs new file mode 100644 index 0000000..0507fdd --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/LuckyDiceModels.cs @@ -0,0 +1,127 @@ +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class LuckyDiceSelectedResult + { + internal LuckyDiceSelectedResult(string resultKey, string targetKey, int slotCount) + { + ResultKey = resultKey; + TargetKey = targetKey; + SlotCount = slotCount; + } + + public string ResultKey { get; } + + public string TargetKey { get; } + + public int SlotCount { get; } + } + + public sealed class LuckyDiceSelectionResult + { + internal LuckyDiceSelectionResult( + LuckyDiceCandidateConfig selectedCandidate, + int initialCandidateCount, + int filteredCandidateCount, + int? randomValue, + string fallbackType, + int slotCount) + { + InitialCandidateCount = initialCandidateCount; + FilteredCandidateCount = filteredCandidateCount; + RandomValue = randomValue; + FallbackType = fallbackType; + SlotCount = selectedCandidate == null ? 0 : slotCount; + Selected = selectedCandidate == null + ? null + : new LuckyDiceSelectedResult(selectedCandidate.ResultKey, selectedCandidate.TargetKey, SlotCount); + SlotResultKeys = selectedCandidate == null ? null : Enumerable.Repeat(selectedCandidate.ResultKey, SlotCount).ToArray(); + } + + public LuckyDiceSelectedResult Selected { get; } + + public int InitialCandidateCount { get; } + + public int FilteredCandidateCount { get; } + + public int? RandomValue { get; } + + public string FallbackType { get; } + + public int SlotCount { get; } + + public string[] SlotResultKeys { get; } + } + + public sealed class TargetModeEntry + { + private TargetModeEntry( + string targetKey, + string modeKey, + string resultKey, + int multiplier, + string triggerSource, + string rollSessionId) + { + TargetKey = targetKey; + ModeKey = modeKey; + ResultKey = resultKey; + Multiplier = multiplier; + TriggerSource = triggerSource; + RollSessionId = rollSessionId; + } + + public string TargetKey { get; } + + public string ModeKey { get; } + + public string ResultKey { get; } + + public int Multiplier { get; } + + public string TriggerSource { get; } + + public string RollSessionId { get; } + + internal static TargetModeEntry From(TargetModeConfig definition, RollFlowRequest request, int multiplier) + { + return new TargetModeEntry( + definition.TargetKey, + definition.ModeKey, + definition.ResultKey, + multiplier, + request.TriggerSource, + request.RollSessionId); + } + } + + public sealed class TargetModeResolveResult + { + private TargetModeResolveResult(bool success, TargetModeEntry entry, string failureReason, string fallbackMode) + { + Success = success; + Entry = entry; + FailureReason = failureReason; + FallbackMode = fallbackMode; + } + + public bool Success { get; } + + public TargetModeEntry Entry { get; } + + public string FailureReason { get; } + + public string FallbackMode { get; } + + public static TargetModeResolveResult Resolved(TargetModeEntry entry) + { + return new TargetModeResolveResult(true, entry, null, null); + } + + public static TargetModeResolveResult Failed(string failureReason, string fallbackMode = null) + { + return new TargetModeResolveResult(false, null, failureReason, fallbackMode); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/LuckyDiceModels.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/LuckyDiceModels.cs.meta new file mode 100644 index 0000000..032ce4f --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/LuckyDiceModels.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 2e21edd33e2d469d9736fa7ce87409f4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionContext.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionContext.cs new file mode 100644 index 0000000..bed6019 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionContext.cs @@ -0,0 +1,20 @@ +using System; + +namespace FishDice.LuckyDice +{ + public sealed class RollActionContext + { + public RollActionContext(string rollSessionId, string comboNormalizedKey, RollTrace trace) + { + RollSessionId = rollSessionId; + ComboNormalizedKey = comboNormalizedKey; + Trace = trace ?? throw new ArgumentNullException(nameof(trace)); + } + + public string RollSessionId { get; } + + public string ComboNormalizedKey { get; } + + public RollTrace Trace { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionContext.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionContext.cs.meta new file mode 100644 index 0000000..6802c4b --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionContext.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 8b28add99b6540b19611b2e1ad429bae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionResult.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionResult.cs new file mode 100644 index 0000000..4d17fb4 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionResult.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class RollActionResult + { + private RollActionResult( + string actionType, + bool success, + IReadOnlyList effects, + string outcome, + string error) + { + ActionType = actionType; + Success = success; + Effects = effects == null ? Array.Empty() : effects.ToArray(); + Outcome = outcome; + Error = error; + } + + public string ActionType { get; } + + public bool Success { get; } + + public IReadOnlyList Effects { get; } + + public string Outcome { get; } + + public string Error { get; } + + public static RollActionResult Succeeded(string actionType, string outcome) + { + return new RollActionResult(actionType, true, Array.Empty(), outcome, null); + } + + public static RollActionResult Succeeded(string actionType, IReadOnlyList effects) + { + var outcome = effects == null || effects.Count == 0 ? null : effects[effects.Count - 1].EffectType; + return new RollActionResult(actionType, true, effects, outcome, null); + } + + public static RollActionResult Failure(string actionType, string error) + { + return new RollActionResult(actionType, false, Array.Empty(), null, error); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionResult.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionResult.cs.meta new file mode 100644 index 0000000..512c380 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionResult.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 218cb0b26b39475fb0b5eff1b7f939fd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionSpec.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionSpec.cs new file mode 100644 index 0000000..37d5cef --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionSpec.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class RollActionSpec + { + public RollActionSpec(string actionType) + : this(actionType, null) + { + } + + public RollActionSpec(string actionType, IReadOnlyDictionary parameters) + { + ActionType = actionType ?? throw new ArgumentNullException(nameof(actionType)); + Params = parameters == null + ? new Dictionary() + : parameters.ToDictionary(pair => pair.Key, pair => pair.Value); + } + + public string ActionType { get; } + + public IReadOnlyDictionary Params { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionSpec.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionSpec.cs.meta new file mode 100644 index 0000000..f64bc2e --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollActionSpec.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 6f8ccfed08184b1b9f553c455e747c10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollEffects.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollEffects.cs new file mode 100644 index 0000000..1bf761f --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollEffects.cs @@ -0,0 +1,67 @@ +namespace FishDice.LuckyDice +{ + public abstract class RollEffect + { + protected RollEffect(string effectType) + { + EffectType = effectType; + } + + public string EffectType { get; } + } + + public sealed class RewardEffect : RollEffect + { + public RewardEffect() + : base("Reward") + { + } + } + + public sealed class MultiplierEffect : RollEffect + { + public MultiplierEffect(string reason) + : base("Multiplier") + { + Reason = reason; + } + + public string Reason { get; } + } + + public sealed class LuckyDiceRequestedEffect : RollEffect + { + public LuckyDiceRequestedEffect() + : base("LuckyDiceRequested") + { + } + } + + public sealed class EnterTargetModeEffect : RollEffect + { + public EnterTargetModeEffect(string targetKey, string modeKey) + : base("EnterTargetMode") + { + TargetKey = targetKey; + ModeKey = modeKey; + } + + public string TargetKey { get; } + + public string ModeKey { get; } + } + + public sealed class TargetModeFailedEffect : RollEffect + { + public TargetModeFailedEffect(string failureReason, string fallbackMode) + : base("TargetModeFailed") + { + FailureReason = failureReason; + FallbackMode = fallbackMode; + } + + public string FailureReason { get; } + + public string FallbackMode { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollEffects.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollEffects.cs.meta new file mode 100644 index 0000000..8472c27 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollEffects.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5524489d53e74a4c88a00bba1de976b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowRequest.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowRequest.cs new file mode 100644 index 0000000..cdeb36b --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowRequest.cs @@ -0,0 +1,39 @@ +namespace FishDice.LuckyDice +{ + public sealed class RollFlowRequest + { + public RollFlowRequest(string rollSessionId) + : this( + rollSessionId, + LuckyDiceKeys.NormalDiceSetId, + LuckyDiceKeys.NormalRollTriggerSource, + playerLevel: 0, + isTutorial: false) + { + } + + public RollFlowRequest( + string rollSessionId, + string diceSetId, + string triggerSource, + int playerLevel, + bool isTutorial) + { + RollSessionId = rollSessionId; + DiceSetId = diceSetId; + TriggerSource = triggerSource; + PlayerLevel = playerLevel; + IsTutorial = isTutorial; + } + + public string RollSessionId { get; } + + public string DiceSetId { get; } + + public string TriggerSource { get; } + + public int PlayerLevel { get; } + + public bool IsTutorial { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowRequest.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowRequest.cs.meta new file mode 100644 index 0000000..32cb7b8 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowRequest.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 2cb236f5ac214d77aae6c15c3c8429d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowResult.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowResult.cs new file mode 100644 index 0000000..9ed0d9d --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowResult.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class RollFlowResult + { + public RollFlowResult( + string rollSessionId, + DiceRollResult rollResult, + ComboFacts comboFacts, + IReadOnlyList matchedRules, + IReadOnlyList actionResults, + IReadOnlyList effects, + LuckyDiceSelectionResult luckyDiceResult, + TargetModeEntry targetMode, + RollTrace trace) + { + RollSessionId = rollSessionId; + RollResult = rollResult; + ComboFacts = comboFacts; + MatchedRules = matchedRules == null ? Array.Empty() : matchedRules.ToArray(); + ActionResults = actionResults == null ? Array.Empty() : actionResults.ToArray(); + Effects = effects == null ? Array.Empty() : effects.ToArray(); + LuckyDiceResult = luckyDiceResult; + TargetMode = targetMode; + Trace = trace; + } + + public string RollSessionId { get; } + + public DiceRollResult RollResult { get; } + + public ComboFacts ComboFacts { get; } + + public IReadOnlyList MatchedRules { get; } + + public IReadOnlyList ActionResults { get; } + + public IReadOnlyList Effects { get; } + + public LuckyDiceSelectionResult LuckyDiceResult { get; } + + public TargetModeEntry TargetMode { get; } + + public RollTrace Trace { get; } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowResult.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowResult.cs.meta new file mode 100644 index 0000000..295c405 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Data/RollFlowResult.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: ebb54a7e748c4b838f46a5d51e00e89f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection.meta new file mode 100644 index 0000000..38e5af5 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 92ce07ad741540bbbaee1c4c8b349ad7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection/LuckyDiceFlowService.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection/LuckyDiceFlowService.cs new file mode 100644 index 0000000..809fcea --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection/LuckyDiceFlowService.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class LuckyDiceFlowService + { + private readonly IRandomSource _random; + + public LuckyDiceFlowService(IRandomSource random) + { + _random = random ?? throw new ArgumentNullException(nameof(random)); + } + + public LuckyDiceSelectionResult Select( + IReadOnlyList candidates, + RollFlowRequest request, + DiceSetConfig luckyDiceSetConfig) + { + var source = candidates ?? Array.Empty(); + var filtered = source + .Where(candidate => candidate.Enabled) + .Where(candidate => request.PlayerLevel >= candidate.MinLevel) + .Where(candidate => candidate.SourceFilter == null || + candidate.SourceFilter.Count == 0 || + candidate.SourceFilter.Contains(request.TriggerSource)) + .ToArray(); + + var fallbackType = "None"; + int? randomValue = null; + var selected = PickWeighted(filtered, out randomValue); + + if (selected == null) + { + selected = filtered.FirstOrDefault(candidate => candidate.IsDefault); + fallbackType = selected == null ? "NormalRewardFallback" : "DefaultResult"; + } + + var slotCount = selected == null ? 0 : selected.ResultSlotCount ?? luckyDiceSetConfig.DefaultResultSlotCount ?? 3; + + return new LuckyDiceSelectionResult( + selected, + source.Count, + filtered.Length, + randomValue, + fallbackType, + slotCount); + } + + private LuckyDiceCandidateConfig PickWeighted(IReadOnlyList candidates, out int? randomValue) + { + var weighted = candidates.Where(candidate => candidate.Weight > 0).ToArray(); + var totalWeight = weighted.Sum(candidate => candidate.Weight); + if (totalWeight <= 0) + { + randomValue = null; + return null; + } + + var roll = _random.Next(0, totalWeight); + randomValue = roll; + var cursor = 0; + foreach (var candidate in weighted) + { + cursor += candidate.Weight; + if (roll < cursor) + { + return candidate; + } + } + + return weighted[weighted.Length - 1]; + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection/LuckyDiceFlowService.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection/LuckyDiceFlowService.cs.meta new file mode 100644 index 0000000..d5c0b74 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/LuckySelection/LuckyDiceFlowService.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: e6956f55ed064f2d872f52a9c9eba554 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules.meta new file mode 100644 index 0000000..6a1f4fe --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: ac0d6f0f00124234809917e89e30eba3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/ComboRuleMatcher.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/ComboRuleMatcher.cs new file mode 100644 index 0000000..b99e995 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/ComboRuleMatcher.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class ComboRuleMatcher + { + private readonly IReadOnlyDictionary _matchers; + + public ComboRuleMatcher() + : this(ComboMatcherRegistry.CreateDefaultMatchers()) + { + } + + public ComboRuleMatcher(IEnumerable matchers) + { + if (matchers == null) + { + throw new ArgumentNullException(nameof(matchers)); + } + + _matchers = matchers.ToDictionary(matcher => matcher.MatcherType, matcher => matcher); + } + + public ComboRuleMatchResult Match(ComboFacts facts, IReadOnlyList rules) + { + if (facts == null) + { + throw new ArgumentNullException(nameof(facts)); + } + + if (rules == null) + { + throw new ArgumentNullException(nameof(rules)); + } + + var matches = new List(); + foreach (var rule in rules.OrderByDescending(rule => rule.Priority)) + { + if (!MatchesRule(facts, rule.Matcher)) + { + continue; + } + + matches.Add(new ComboRuleMatch(rule)); + if (rule.StopAfterMatched) + { + break; + } + } + + return new ComboRuleMatchResult(matches); + } + + private bool MatchesRule(ComboFacts facts, ComboMatcherSpec matcher) + { + if (!_matchers.TryGetValue(matcher.MatcherType, out var registeredMatcher)) + { + return false; + } + + return registeredMatcher.IsMatch(facts, matcher); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/ComboRuleMatcher.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/ComboRuleMatcher.cs.meta new file mode 100644 index 0000000..90cee3e --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/ComboRuleMatcher.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5ffdaec353584cbf8006d846816af810 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers.meta new file mode 100644 index 0000000..d318b46 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: f02fda27051d4ff8b44d459bd69c176b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatcherRegistry.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatcherRegistry.cs new file mode 100644 index 0000000..41b1e69 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatcherRegistry.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace FishDice.LuckyDice +{ + public static class ComboMatcherRegistry + { + public static IReadOnlyList CreateDefaultMatchers() + { + return new IComboMatcher[] + { + new ExactComboMatcher(), + new AllNumbersMatcher(), + new ContainsFaceMatcher(), + new FaceCountMatcher(), + new FaceCountRangeMatcher(), + new DiceCountMatcher(), + new NumberPairMatcher(), + new NumberOfAKindMatcher(), + new NumberSumRangeMatcher() + }; + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatcherRegistry.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatcherRegistry.cs.meta new file mode 100644 index 0000000..b6272a8 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatcherRegistry.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 024047cd3f684a5a813d1abe30fc350a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatchers.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatchers.cs new file mode 100644 index 0000000..986b748 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatchers.cs @@ -0,0 +1,131 @@ +using System; +using System.Linq; + +namespace FishDice.LuckyDice +{ + internal sealed class ExactComboMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.ExactComboMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return string.Equals( + spec.UseOrderedKey ? facts.OrderedKey : facts.NormalizedKey, + spec.ComboKey, + StringComparison.Ordinal); + } + } + + internal sealed class AllNumbersMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.AllNumbersMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return facts.NumberValues.Count == facts.DiceCount; + } + } + + internal sealed class ContainsFaceMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.ContainsFaceMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return ComboMatcherFacts.FaceCount(facts, spec.FaceKey) >= (spec.RequiredCount ?? 1); + } + } + + internal sealed class FaceCountMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.FaceCountMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return ComboMatcherFacts.FaceCount(facts, spec.FaceKey) == (spec.RequiredCount ?? 1); + } + } + + internal sealed class FaceCountRangeMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.FaceCountRangeMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return ComboMatcherFacts.IsInRange( + ComboMatcherFacts.FaceCount(facts, spec.FaceKey), + spec.MinCount, + spec.MaxCount); + } + } + + internal sealed class DiceCountMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.DiceCountMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return !spec.DiceCount.HasValue || facts.DiceCount == spec.DiceCount.Value; + } + } + + internal sealed class NumberPairMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.NumberPairMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return facts.NumberValues.Count == 2 && facts.NumberValues[0] == facts.NumberValues[1]; + } + } + + internal sealed class NumberOfAKindMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.NumberOfAKindMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return facts.NumberValues + .GroupBy(value => value) + .Any(group => group.Count() >= (spec.OfAKindCount ?? 2)); + } + } + + internal sealed class NumberSumRangeMatcher : IComboMatcher + { + public string MatcherType => LuckyDiceKeys.NumberSumRangeMatcher; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return ComboMatcherFacts.IsInRange(facts.NumberSum, spec.MinSum, spec.MaxSum); + } + } + + internal static class ComboMatcherFacts + { + public static int FaceCount(ComboFacts facts, string faceKey) + { + if (string.IsNullOrEmpty(faceKey)) + { + return 0; + } + + return facts.FaceCounts.TryGetValue(faceKey, out var count) ? count : 0; + } + + public static bool IsInRange(int value, int? min, int? max) + { + if (min.HasValue && value < min.Value) + { + return false; + } + + if (max.HasValue && value > max.Value) + { + return false; + } + + return true; + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatchers.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatchers.cs.meta new file mode 100644 index 0000000..292eccb --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/ComboMatchers.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 670344b8b7c74696baef9fe7a61ffc34 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/IComboMatcher.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/IComboMatcher.cs new file mode 100644 index 0000000..be95625 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/IComboMatcher.cs @@ -0,0 +1,9 @@ +namespace FishDice.LuckyDice +{ + public interface IComboMatcher + { + string MatcherType { get; } + + bool IsMatch(ComboFacts facts, ComboMatcherSpec spec); + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/IComboMatcher.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/IComboMatcher.cs.meta new file mode 100644 index 0000000..82da76d --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Rules/Matchers/IComboMatcher.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5252627bba5a463bb7006295c672a164 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Services.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services.meta new file mode 100644 index 0000000..3365e22 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 32a9a61586304026ad6aabe05b1316c2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/ComboFactBuilder.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/ComboFactBuilder.cs new file mode 100644 index 0000000..fda654a --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/ComboFactBuilder.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public static class ComboFactBuilder + { + public static ComboFacts Build(DiceRollResult result) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + if (result.SetType != DiceSetType.Normal) + { + throw new InvalidOperationException("Only normal dice rolls can be converted to combo facts."); + } + + if (result.DiceCount != result.Faces.Count) + { + throw new InvalidOperationException("Dice count does not match rolled face count."); + } + + if (result.Faces.Any(face => face.SetType != DiceSetType.Normal)) + { + throw new InvalidOperationException("Combo facts cannot include lucky dice result faces."); + } + + var faceCounts = result.Faces + .GroupBy(face => face.Key) + .ToDictionary(group => group.Key, group => group.Count()); + var numberValues = result.Faces + .Where(face => face.NumberValue.HasValue) + .Select(face => face.NumberValue.Value) + .ToArray(); + var cloverCount = faceCounts.TryGetValue(LuckyDiceKeys.CloverFaceKey, out var count) ? count : 0; + var orderedKey = string.Join("_", result.Faces.Select(face => face.Key)); + var normalizedKey = string.Join("_", result.Faces.Select(face => face.Key).OrderBy(key => key, StringComparer.Ordinal)); + + return new ComboFacts( + result.RollSessionId, + result.SetType, + result.DiceCount, + result.Faces, + faceCounts, + numberValues.Sum(), + numberValues, + cloverCount, + normalizedKey, + orderedKey); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/ComboFactBuilder.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/ComboFactBuilder.cs.meta new file mode 100644 index 0000000..1f08c00 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/ComboFactBuilder.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: fb0a92fa21434dc6b216eaf039c8eb86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/DiceRollService.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/DiceRollService.cs new file mode 100644 index 0000000..0b7715a --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/DiceRollService.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace FishDice.LuckyDice +{ + public interface IRandomSource + { + int Next(int minInclusive, int maxExclusive); + } + + public sealed class SystemRandomSource : IRandomSource + { + private readonly Random _random; + + public SystemRandomSource() + : this(new Random()) + { + } + + public SystemRandomSource(Random random) + { + _random = random ?? throw new ArgumentNullException(nameof(random)); + } + + public int Next(int minInclusive, int maxExclusive) + { + return _random.Next(minInclusive, maxExclusive); + } + } + + public sealed class DiceRollService + { + private readonly IRandomSource _random; + + public DiceRollService(IRandomSource random) + { + _random = random ?? throw new ArgumentNullException(nameof(random)); + } + + public DiceRollResult Roll(DiceSetConfig config, string rollSessionId, string triggerSource) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + var rolledFaces = new List(config.DiceCount); + for (var i = 0; i < config.DiceCount; i++) + { + var index = _random.Next(0, config.Faces.Count); + rolledFaces.Add(config.Faces[index]); + } + + return DiceRollResult.FromConfig( + rollSessionId, + config, + rolledFaces, + multiplier: 1, + triggerSource: triggerSource); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/DiceRollService.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/DiceRollService.cs.meta new file mode 100644 index 0000000..f713de5 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Services/DiceRollService.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 8a9d95f67cf84b62b960aae1b964e9db +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode.meta new file mode 100644 index 0000000..df2594b --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 48e76359cf424f43b578f3098179d64f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode/TargetModeResolver.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode/TargetModeResolver.cs new file mode 100644 index 0000000..cfe1acb --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode/TargetModeResolver.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class TargetModeResolver + { + public TargetModeResolveResult Resolve( + LuckyDiceSelectedResult selected, + RollFlowRequest request, + int multiplier, + IReadOnlyList definitions) + { + if (selected == null) + { + return TargetModeResolveResult.Failed("MissingLuckyDiceCandidate"); + } + + var definition = (definitions ?? Array.Empty()) + .FirstOrDefault(item => + item.ResultKey == selected.ResultKey && + item.TargetKey == selected.TargetKey); + + if (definition == null) + { + return TargetModeResolveResult.Failed("MissingTargetMode"); + } + + return TargetModeResolveResult.Resolved(TargetModeEntry.From(definition, request, multiplier)); + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode/TargetModeResolver.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode/TargetModeResolver.cs.meta new file mode 100644 index 0000000..1ebbc7d --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/TargetMode/TargetModeResolver.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: c8560c502d0b42cd856b23761cf7ea4b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace.meta new file mode 100644 index 0000000..1520e91 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: d407eb4e046b45ff99f112e6646ab1d0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace/RollTrace.cs b/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace/RollTrace.cs new file mode 100644 index 0000000..a609728 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace/RollTrace.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FishDice.LuckyDice +{ + public sealed class RollTrace + { + private readonly List _executedActions = new List(); + private readonly List _matchedRules = new List(); + private readonly List _actionResults = new List(); + + public RollTrace(string rollSessionId) + { + RollSessionId = rollSessionId; + } + + public string RollSessionId { get; } + + public IReadOnlyList NormalFaces { get; private set; } + + public int NormalDiceCount { get; private set; } + + public string ComboNormalizedKey { get; private set; } + + public string MatchedRuleId { get; private set; } + + public int? MatchedRulePriority { get; private set; } + + public IReadOnlyList MatchedRules => _matchedRules; + + public IReadOnlyList ExecutedActions => _executedActions; + + public IReadOnlyList ActionResults => _actionResults; + + public int LuckyDiceInitialCandidateCount { get; private set; } + + public int LuckyDiceFilteredCandidateCount { get; private set; } + + public string LuckyDiceFallbackType { get; private set; } + + public int? LuckyDiceRandomValue { get; private set; } + + public string LuckyDiceSelectedResultKey { get; private set; } + + public string LuckyDiceSelectedTargetKey { get; private set; } + + public int LuckyDiceResultSlotCount { get; private set; } + + public IReadOnlyList LuckyDiceSlotResultKeys { get; private set; } + + public string ResolvedTargetKey { get; private set; } + + public string ResolvedModeKey { get; private set; } + + public string TargetModeFailureReason { get; private set; } + + public string FinalOutcome { get; private set; } + + public void RecordComboFacts(ComboFacts facts) + { + NormalFaces = facts.Faces.Select(face => face.Key).ToArray(); + NormalDiceCount = facts.DiceCount; + ComboNormalizedKey = facts.NormalizedKey; + } + + public void RecordRuleMatch(ComboRuleMatchResult matchResult) + { + _matchedRules.Clear(); + _matchedRules.AddRange(matchResult.Matches); + + var first = matchResult.Matches.FirstOrDefault(); + if (first == null) + { + return; + } + + MatchedRuleId = first.RuleId; + MatchedRulePriority = first.Priority; + } + + public void RecordActionResult(RollActionResult result) + { + _executedActions.Add(result.ActionType); + _actionResults.Add(result); + if (result.Success && !string.IsNullOrEmpty(result.Outcome)) + { + FinalOutcome = result.Outcome; + } + } + + public void RecordLuckyDiceSelection(LuckyDiceSelectionResult result) + { + LuckyDiceInitialCandidateCount = result.InitialCandidateCount; + LuckyDiceFilteredCandidateCount = result.FilteredCandidateCount; + LuckyDiceFallbackType = result.FallbackType; + LuckyDiceRandomValue = result.RandomValue; + LuckyDiceSelectedResultKey = result.Selected == null ? null : result.Selected.ResultKey; + LuckyDiceSelectedTargetKey = result.Selected == null ? null : result.Selected.TargetKey; + LuckyDiceResultSlotCount = result.SlotCount; + LuckyDiceSlotResultKeys = result.SlotResultKeys; + } + + public void RecordTargetMode(TargetModeEntry entry) + { + if (entry == null) + { + return; + } + + ResolvedTargetKey = entry.TargetKey; + ResolvedModeKey = entry.ModeKey; + FinalOutcome = "TargetModeResolved"; + } + + public void RecordTargetModeFailure(string failureReason) + { + TargetModeFailureReason = failureReason; + FinalOutcome = "TargetModeFailed"; + } + } +} diff --git a/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace/RollTrace.cs.meta b/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace/RollTrace.cs.meta new file mode 100644 index 0000000..ddb4d77 --- /dev/null +++ b/FishDice/Assets/FishDice/Runtime/LuckyDice/Trace/RollTrace.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: d2c57c4ac2f54a638326e104f5afac01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Tests.meta b/FishDice/Assets/FishDice/Tests.meta new file mode 100644 index 0000000..c4cdef6 --- /dev/null +++ b/FishDice/Assets/FishDice/Tests.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 243feaaf2c05440ebb1fff35768a653c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Tests/EditMode.meta b/FishDice/Assets/FishDice/Tests/EditMode.meta new file mode 100644 index 0000000..f8317be --- /dev/null +++ b/FishDice/Assets/FishDice/Tests/EditMode.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 020efc0ae0324888bb93be8b772cd5fd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Tests/EditMode/FishDice.Tests.EditMode.asmdef b/FishDice/Assets/FishDice/Tests/EditMode/FishDice.Tests.EditMode.asmdef new file mode 100644 index 0000000..0055ce4 --- /dev/null +++ b/FishDice/Assets/FishDice/Tests/EditMode/FishDice.Tests.EditMode.asmdef @@ -0,0 +1,22 @@ +{ + "name": "FishDice.Tests.EditMode", + "rootNamespace": "FishDice.Tests", + "references": [ + "FishDice.Runtime", + "FishDice.Demo" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false, + "optionalUnityReferences": [ + "TestAssemblies" + ] +} diff --git a/FishDice/Assets/FishDice/Tests/EditMode/FishDice.Tests.EditMode.asmdef.meta b/FishDice/Assets/FishDice/Tests/EditMode/FishDice.Tests.EditMode.asmdef.meta new file mode 100644 index 0000000..6ccafb3 --- /dev/null +++ b/FishDice/Assets/FishDice/Tests/EditMode/FishDice.Tests.EditMode.asmdef.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c807bf8363384e5a8ffb7ccd72e79729 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice.meta b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice.meta new file mode 100644 index 0000000..7e23515 --- /dev/null +++ b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 1e002e674a4847f996ba72226be6774e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceCoreFlowTests.cs b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceCoreFlowTests.cs new file mode 100644 index 0000000..3082c87 --- /dev/null +++ b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceCoreFlowTests.cs @@ -0,0 +1,623 @@ +using System.Linq; +using FishDice.LuckyDice; +using NUnit.Framework; + +namespace FishDice.Tests.LuckyDice +{ + public sealed class LuckyDiceCoreFlowTests + { + [Test] + public void RollFlowService_runs_normal_reward_chain_from_one_entry() + { + var flow = TestFlow(0, 1); + + var result = flow.Execute(new RollFlowRequest("session-flow-normal")); + + Assert.That(result.RollSessionId, Is.EqualTo("session-flow-normal")); + Assert.That(result.ComboFacts.NormalizedKey, Is.EqualTo("2_3")); + Assert.That(result.MatchedRules.Select(rule => rule.RuleId), Is.EqualTo(new[] { "normal_all_numbers_reward" })); + Assert.That(result.Effects.OfType().Count(), Is.EqualTo(1)); + Assert.That(result.Effects.OfType().Any(), Is.False); + Assert.That(result.LuckyDiceResult, Is.Null); + Assert.That(result.Trace.ActionResults.Select(action => action.ActionType), Is.EqualTo(new[] { "grant_reward" })); + } + + [Test] + public void RollFlowService_resolves_lucky_dice_to_target_mode_from_one_entry() + { + var flow = TestFlow(5, 5, 0); + + var result = flow.Execute(new RollFlowRequest("session-flow-lucky")); + + Assert.That(result.ComboFacts.NormalizedKey, Is.EqualTo("clover_clover")); + Assert.That(result.Effects.OfType().Count(), Is.EqualTo(1)); + Assert.That(result.LuckyDiceResult.Selected.ResultKey, Is.EqualTo("rocket")); + Assert.That(result.LuckyDiceResult.Selected, Is.TypeOf()); + Assert.That(typeof(LuckyDiceSelectedResult).GetProperty("Weight"), Is.Null); + Assert.That(result.LuckyDiceResult.SlotResultKeys, Is.EqualTo(new[] { "rocket", "rocket", "rocket" })); + Assert.That(result.TargetMode.TargetKey, Is.EqualTo("slap_down")); + Assert.That(result.TargetMode.ModeKey, Is.EqualTo("slap_down_normal")); + Assert.That(result.Effects.OfType().Single().ModeKey, Is.EqualTo("slap_down_normal")); + Assert.That(result.Trace.LuckyDiceInitialCandidateCount, Is.EqualTo(2)); + Assert.That(result.Trace.LuckyDiceFilteredCandidateCount, Is.EqualTo(2)); + Assert.That(result.Trace.LuckyDiceSelectedResultKey, Is.EqualTo("rocket")); + Assert.That(result.Trace.ResolvedModeKey, Is.EqualTo("slap_down_normal")); + } + + [Test] + public void RollFlowService_uses_injected_random_for_lucky_weighted_pick() + { + var flow = TestFlow(5, 5, 1); + + var result = flow.Execute(new RollFlowRequest("session-flow-thief")); + + Assert.That(result.LuckyDiceResult.Selected.ResultKey, Is.EqualTo("thief")); + Assert.That(result.TargetMode.TargetKey, Is.EqualTo("treasure_heist")); + Assert.That(result.TargetMode.ModeKey, Is.EqualTo("treasure_heist_normal")); + Assert.That(result.Trace.LuckyDiceRandomValue, Is.EqualTo(1)); + } + + [Test] + public void ConfigValidator_reports_unknown_matcher_and_action() + { + var rules = new[] + { + new ComboRuleDefinition + { + RuleId = "bad_rule", + Priority = 1, + MatcherType = "missing_matcher", + Actions = new[] + { + new RollActionDefinition { ActionType = "missing_action" } + } + } + }; + + var result = LuckyDiceConfigValidator.Validate( + rules, + ValidCandidateDefinitions(), + ValidTargetModeDefinitions(), + ComboMatcherRegistry.CreateDefaultMatchers(), + RollActionExecutorRegistry.CreateDefaultExecutors()); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("UnknownMatcher")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("UnknownAction")); + } + + [Test] + public void ConfigValidator_reports_null_child_items() + { + var rules = new ComboRuleDefinition[] + { + null, + new ComboRuleDefinition + { + RuleId = "rule_with_null_action", + Priority = 1, + MatcherType = LuckyDiceKeys.AllNumbersMatcher, + Actions = new RollActionDefinition[] { null } + } + }; + var candidates = new LuckyDiceCandidateDefinition[] + { + null, + new LuckyDiceCandidateDefinition + { + ResultKey = LuckyDiceKeys.RocketResultKey, + TargetKey = "slap_down", + ResultSlotCount = 3, + Weight = 1, + Enabled = true, + SourceFilter = new[] { LuckyDiceKeys.NormalRollTriggerSource }, + IsDefault = true + } + }; + var targetModes = new TargetModeDefinition[] + { + null, + new TargetModeDefinition + { + ResultKey = LuckyDiceKeys.RocketResultKey, + TargetKey = "slap_down", + ModeKey = "slap_down_normal" + } + }; + + var result = LuckyDiceConfigValidator.Validate( + rules, + candidates, + targetModes, + ComboMatcherRegistry.CreateDefaultMatchers(), + RollActionExecutorRegistry.CreateDefaultExecutors()); + + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullRule")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullAction")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullCandidate")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullTargetMode")); + } + + [Test] + public void ConfigValidator_reports_bad_extension_registry_shape() + { + var result = LuckyDiceConfigValidator.Validate( + new ComboRuleDefinition[0], + ValidCandidateDefinitions(), + ValidTargetModeDefinitions(), + new IComboMatcher[] + { + null, + new TypedMatcher(""), + new TypedMatcher("duplicate_matcher"), + new TypedMatcher("duplicate_matcher") + }, + new IRollActionExecutor[] + { + null, + new TypedActionExecutor(""), + new TypedActionExecutor("duplicate_action"), + new TypedActionExecutor("duplicate_action") + }); + + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullMatcher")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyMatcherType")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateMatcherType")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullActionExecutor")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyActionExecutorType")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateActionExecutorType")); + } + + [Test] + public void ConfigValidator_reports_bad_action_params() + { + var rules = new[] + { + new ComboRuleDefinition + { + RuleId = "rule_with_bad_params", + Priority = 1, + MatcherType = LuckyDiceKeys.AllNumbersMatcher, + Actions = new[] + { + new RollActionDefinition + { + ActionType = LuckyDiceKeys.GrantRewardAction, + Params = new ActionParamDefinition[] + { + null, + new ActionParamDefinition { Key = "", Value = "empty" }, + new ActionParamDefinition { Key = "duplicate", Value = "first" }, + new ActionParamDefinition { Key = "duplicate", Value = "second" } + } + } + } + } + }; + + var result = LuckyDiceConfigValidator.Validate( + rules, + ValidCandidateDefinitions(), + ValidTargetModeDefinitions(), + ComboMatcherRegistry.CreateDefaultMatchers(), + RollActionExecutorRegistry.CreateDefaultExecutors()); + + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NullActionParam")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyActionParamKey")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateActionParamKey")); + } + + [Test] + public void RuntimeFactory_rejects_bad_registry_before_runtime_dictionary_build() + { + var context = new LuckyDiceConfigValidationContext( + new IComboMatcher[] + { + null, + new TypedMatcher("duplicate_matcher"), + new TypedMatcher("duplicate_matcher") + }, + new IRollActionExecutor[] + { + null, + new TypedActionExecutor("duplicate_action"), + new TypedActionExecutor("duplicate_action") + }); + var factory = new LuckyDiceRuntimeFactory(context, new FixedRandomSource()); + + var exception = Assert.Throws(() => + factory.CreateFlow( + new ComboRuleDefinition[0], + ValidCandidateDefinitions(), + ValidTargetModeDefinitions())); + + Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("NullMatcher")); + Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("DuplicateMatcherType")); + Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("NullActionExecutor")); + Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("DuplicateActionExecutorType")); + } + + [Test] + public void DefaultConfig_rejects_bad_action_params_before_loader_dictionary_build() + { + var rules = new[] + { + new ComboRuleDefinition + { + RuleId = "rule_with_duplicate_param", + Priority = 1, + MatcherType = LuckyDiceKeys.AllNumbersMatcher, + Actions = new[] + { + new RollActionDefinition + { + ActionType = LuckyDiceKeys.GrantRewardAction, + Params = new[] + { + new ActionParamDefinition { Key = "duplicate", Value = "first" }, + new ActionParamDefinition { Key = "duplicate", Value = "second" } + } + } + } + } + }; + + var exception = Assert.Throws(() => + LuckyDiceDefaultConfig.CreateValidated( + rules, + ValidCandidateDefinitions(), + ValidTargetModeDefinitions())); + + Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("DuplicateActionParamKey")); + } + + [Test] + public void LuckyDice_fallback_does_not_bypass_progress_or_source_filters() + { + var service = new LuckyDiceFlowService(new FixedRandomSource()); + var candidates = new[] + { + new LuckyDiceCandidateConfig( + "locked_default", + "locked_target", + resultSlotCount: 3, + weight: 0, + enabled: true, + minLevel: 10, + sourceFilter: new[] { "other_source" }, + isDefault: true) + }; + var request = new RollFlowRequest( + "session-filter", + LuckyDiceKeys.NormalDiceSetId, + LuckyDiceKeys.NormalRollTriggerSource, + playerLevel: 0, + isTutorial: false); + + var result = service.Select(candidates, request, LuckyDiceDefaultConfig.Create().GetDiceSet(LuckyDiceKeys.LuckyDiceSetId)); + + Assert.That(result.Selected, Is.Null); + Assert.That(result.FallbackType, Is.EqualTo("NormalRewardFallback")); + } + + [Test] + public void TargetModeResolver_returns_failure_reason_when_mapping_is_missing() + { + var resolver = new TargetModeResolver(); + var request = new RollFlowRequest( + "session-mode-missing", + LuckyDiceKeys.NormalDiceSetId, + LuckyDiceKeys.NormalRollTriggerSource, + playerLevel: 0, + isTutorial: false); + var candidates = new[] + { + new LuckyDiceCandidateConfig( + "unknown_result", + "unknown_target", + resultSlotCount: 3, + weight: 1, + enabled: true, + minLevel: 0, + sourceFilter: null, + isDefault: true) + }; + var selection = new LuckyDiceFlowService(new FixedRandomSource(0)) + .Select(candidates, request, LuckyDiceDefaultConfig.Create().GetDiceSet(LuckyDiceKeys.LuckyDiceSetId)); + + var result = resolver.Resolve(selection.Selected, request, 1, LuckyDiceDefaultConfig.Create().TargetModeDefinitions); + + Assert.That(result.Success, Is.False); + Assert.That(result.FailureReason, Is.EqualTo("MissingTargetMode")); + Assert.That(result.FallbackMode, Is.Null); + } + + [Test] + public void ConfigValidator_reports_bad_candidate_shape() + { + var candidates = new[] + { + new LuckyDiceCandidateDefinition { ResultKey = "", TargetKey = "target", ResultSlotCount = 0, Weight = -1, Enabled = false, IsDefault = true }, + new LuckyDiceCandidateDefinition { ResultKey = "", TargetKey = "target", ResultSlotCount = 3, Weight = 1, Enabled = true } + }; + + var result = LuckyDiceConfigValidator.Validate( + new ComboRuleDefinition[0], + candidates, + ValidTargetModeDefinitions(), + ComboMatcherRegistry.CreateDefaultMatchers(), + RollActionExecutorRegistry.CreateDefaultExecutors()); + + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyCandidateResultKey")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateCandidate")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("NegativeCandidateWeight")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("InvalidResultSlotCount")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DisabledDefaultCandidate")); + } + + [Test] + public void ConfigValidator_reports_bad_target_mode_shape() + { + var targetModes = new[] + { + new TargetModeDefinition { ResultKey = "", TargetKey = "", ModeKey = "" }, + new TargetModeDefinition { ResultKey = "", TargetKey = "", ModeKey = "duplicate" } + }; + + var result = LuckyDiceConfigValidator.Validate( + new ComboRuleDefinition[0], + ValidCandidateDefinitions(), + targetModes, + ComboMatcherRegistry.CreateDefaultMatchers(), + RollActionExecutorRegistry.CreateDefaultExecutors()); + + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyTargetModeResultKey")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyTargetModeTargetKey")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("EmptyTargetModeModeKey")); + Assert.That(result.Errors.Select(error => error.Code), Does.Contain("DuplicateTargetMode")); + } + + [Test] + public void DefaultConfig_exposes_immutable_runtime_candidate_config() + { + var config = LuckyDiceDefaultConfig.Create(); + var candidate = config.LuckyDiceCandidates[0]; + + Assert.That(candidate, Is.TypeOf()); + Assert.That(typeof(LuckyDiceCandidateConfig).GetProperty("Weight").CanWrite, Is.False); + Assert.That(typeof(LuckyDiceCandidateConfig).GetProperty("Enabled").CanWrite, Is.False); + Assert.That(candidate.SourceFilter, Is.Not.TypeOf()); + } + + [Test] + public void DefaultConfig_create_validated_copies_definition_values() + { + var candidates = ValidCandidateDefinitions(); + var targetModes = ValidTargetModeDefinitions(); + var config = LuckyDiceDefaultConfig.CreateValidated( + new ComboRuleDefinition[0], + candidates, + targetModes); + + candidates[0].Weight = 999; + candidates[0].SourceFilter[0] = "mutated_source"; + targetModes[0].ModeKey = "mutated_mode"; + + Assert.That(config.LuckyDiceCandidates[0].Weight, Is.EqualTo(1)); + Assert.That(config.LuckyDiceCandidates[0].SourceFilter[0], Is.EqualTo(LuckyDiceKeys.NormalRollTriggerSource)); + Assert.That(config.TargetModeDefinitions[0].ModeKey, Is.EqualTo("slap_down_normal")); + } + + [Test] + public void DefaultConfig_create_validated_rejects_bad_config() + { + var exception = Assert.Throws(() => + LuckyDiceDefaultConfig.CreateValidated( + new ComboRuleDefinition[0], + new[] + { + new LuckyDiceCandidateDefinition + { + ResultKey = "bad", + TargetKey = "target", + Weight = -1, + Enabled = true, + IsDefault = true + } + }, + new[] + { + new TargetModeDefinition { ResultKey = "bad", TargetKey = "target", ModeKey = "mode" } + })); + + Assert.That(exception.ValidationResult.Errors.Select(error => error.Code), Does.Contain("NegativeCandidateWeight")); + } + + [Test] + public void DefaultConfig_create_validated_uses_supplied_validation_context() + { + var config = LuckyDiceDefaultConfig.CreateValidated( + new[] + { + new ComboRuleDefinition + { + RuleId = "custom_rule", + Priority = 1, + MatcherType = "custom_matcher", + Actions = new[] { new RollActionDefinition { ActionType = "custom_action" } } + } + }, + ValidCandidateDefinitions(), + ValidTargetModeDefinitions(), + new LuckyDiceConfigValidationContext( + new[] { new CustomMatcher() }, + new[] { new CustomActionExecutor() })); + + Assert.That(config.ComboRules.Single().Matcher.MatcherType, Is.EqualTo("custom_matcher")); + Assert.That(config.ComboRules.Single().Actions.Single().ActionType, Is.EqualTo("custom_action")); + } + + [Test] + public void RuntimeFactory_uses_same_context_for_validation_and_runtime_dispatch() + { + var context = new LuckyDiceConfigValidationContext( + new[] { new CustomMatcher() }, + new[] { new CustomActionExecutor() }); + var factory = new LuckyDiceRuntimeFactory(context, new FixedRandomSource(0, 1)); + + var flow = factory.CreateFlow( + new[] + { + new ComboRuleDefinition + { + RuleId = "custom_rule", + Priority = 1, + MatcherType = "custom_matcher", + Actions = new[] { new RollActionDefinition { ActionType = "custom_action" } } + } + }, + ValidCandidateDefinitions(), + ValidTargetModeDefinitions()); + + var result = flow.Execute(new RollFlowRequest("session-custom-flow")); + + Assert.That(result.MatchedRules.Select(rule => rule.RuleId), Is.EqualTo(new[] { "custom_rule" })); + Assert.That(result.ActionResults.Single().ActionType, Is.EqualTo("custom_action")); + Assert.That(result.ActionResults.Single().Outcome, Is.EqualTo("CustomAction")); + } + + [Test] + public void DefaultConfig_rejects_unknown_dice_set_with_clear_exception() + { + var config = LuckyDiceDefaultConfig.Create(); + + var exception = Assert.Throws(() => config.GetDiceSet("missing_set")); + + Assert.That(exception.DiceSetId, Is.EqualTo("missing_set")); + } + + [Test] + public void RollFlowService_rejects_invalid_requests() + { + var flow = TestFlow(0, 1); + + Assert.Throws(() => flow.Execute(null)); + Assert.Throws(() => flow.Execute(new RollFlowRequest("session-invalid", "", LuckyDiceKeys.NormalRollTriggerSource, 0, false))); + Assert.Throws(() => flow.Execute(new RollFlowRequest("session-invalid", LuckyDiceKeys.NormalDiceSetId, "", 0, false))); + Assert.Throws(() => flow.Execute(new RollFlowRequest("session-invalid", LuckyDiceKeys.NormalDiceSetId, null, 0, false))); + Assert.Throws(() => flow.Execute(new RollFlowRequest("session-invalid", "missing_set", LuckyDiceKeys.NormalRollTriggerSource, 0, false))); + } + + [Test] + public void Runtime_asmdef_disallows_engine_references() + { + var asmdef = System.IO.File.ReadAllText("FishDice/Assets/FishDice/Runtime/FishDice.Runtime.asmdef"); + + Assert.That(asmdef, Does.Contain("\"noEngineReferences\": true")); + } + + private static LuckyDiceRollFlowService TestFlow(params int[] randomValues) + { + var random = new FixedRandomSource(randomValues); + + return LuckyDiceRuntimeFactory.CreateDefault(random).CreateDefaultFlow(); + } + + private static LuckyDiceCandidateDefinition[] ValidCandidateDefinitions() + { + return new[] + { + new LuckyDiceCandidateDefinition + { + ResultKey = LuckyDiceKeys.RocketResultKey, + TargetKey = "slap_down", + ResultSlotCount = 3, + Weight = 1, + Enabled = true, + SourceFilter = new[] { LuckyDiceKeys.NormalRollTriggerSource }, + IsDefault = true + } + }; + } + + private static TargetModeDefinition[] ValidTargetModeDefinitions() + { + return new[] + { + new TargetModeDefinition + { + ResultKey = LuckyDiceKeys.RocketResultKey, + TargetKey = "slap_down", + ModeKey = "slap_down_normal" + } + }; + } + + private sealed class FixedRandomSource : IRandomSource + { + private readonly System.Collections.Generic.Queue _values; + + public FixedRandomSource(params int[] values) + { + _values = new System.Collections.Generic.Queue(values); + } + + public int Next(int minInclusive, int maxExclusive) + { + var value = _values.Dequeue(); + Assert.That(value, Is.InRange(minInclusive, maxExclusive - 1)); + return value; + } + } + + private sealed class CustomMatcher : IComboMatcher + { + public string MatcherType => "custom_matcher"; + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return true; + } + } + + private sealed class CustomActionExecutor : IRollActionExecutor + { + public string ActionType => "custom_action"; + + public RollActionResult Execute(RollActionContext context, RollActionSpec action) + { + return RollActionResult.Succeeded(ActionType, "CustomAction"); + } + } + + private sealed class TypedMatcher : IComboMatcher + { + public TypedMatcher(string matcherType) + { + MatcherType = matcherType; + } + + public string MatcherType { get; } + + public bool IsMatch(ComboFacts facts, ComboMatcherSpec spec) + { + return true; + } + } + + private sealed class TypedActionExecutor : IRollActionExecutor + { + public TypedActionExecutor(string actionType) + { + ActionType = actionType; + } + + public string ActionType { get; } + + public RollActionResult Execute(RollActionContext context, RollActionSpec action) + { + return RollActionResult.Succeeded(ActionType, "TypedAction"); + } + } + } +} diff --git a/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceCoreFlowTests.cs.meta b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceCoreFlowTests.cs.meta new file mode 100644 index 0000000..588ba25 --- /dev/null +++ b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceCoreFlowTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b5815ce58ce8c04c98bf59177e89acc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceSlice1Tests.cs b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceSlice1Tests.cs new file mode 100644 index 0000000..c31460f --- /dev/null +++ b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceSlice1Tests.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FishDice.LuckyDice; +using NUnit.Framework; + +namespace FishDice.Tests.LuckyDice +{ + public sealed class LuckyDiceSlice1Tests + { + [Test] + public void DefaultConfig_keeps_normal_and_lucky_sets_separate() + { + var config = LuckyDiceDefaultConfig.Create(); + + var normal = config.GetDiceSet(LuckyDiceKeys.NormalDiceSetId); + var lucky = config.GetDiceSet(LuckyDiceKeys.LuckyDiceSetId); + + Assert.That(normal.SetType, Is.EqualTo(DiceSetType.Normal)); + Assert.That(normal.DiceCount, Is.EqualTo(2)); + Assert.That(normal.Faces.Select(face => face.Key), Is.EqualTo(new[] { "2", "3", "4", "5", "6", "clover" })); + Assert.That(lucky.SetType, Is.EqualTo(DiceSetType.Lucky)); + Assert.That(lucky.DefaultResultSlotCount, Is.EqualTo(3)); + Assert.That(lucky.Faces.Select(face => face.Key), Is.EqualTo(new[] { "rocket", "thief" })); + } + + [Test] + public void Roll_uses_configured_dice_count_and_keeps_session_id() + { + var config = new DiceSetConfig( + "normal_three", + DiceSetType.Normal, + diceCount: 3, + faces: new[] + { + DiceFace.NormalNumber(2), + DiceFace.NormalNumber(3), + DiceFace.NormalClover() + }, + defaultResultSlotCount: null); + var service = new DiceRollService(new FixedRandomSource(0, 1, 2)); + + var result = service.Roll(config, "session-roll", "normal_roll"); + + Assert.That(result.RollSessionId, Is.EqualTo("session-roll")); + Assert.That(result.DiceCount, Is.EqualTo(3)); + Assert.That(result.Faces.Select(face => face.Key), Is.EqualTo(new[] { "2", "3", "clover" })); + Assert.That(result.Multiplier, Is.EqualTo(1)); + } + + [Test] + public void ComboFacts_normalize_faces_and_support_more_than_two_dice() + { + var result = DiceRollResult.Create( + "session-facts", + "normal_main", + DiceSetType.Normal, + new[] { DiceFace.NormalClover(), DiceFace.NormalNumber(2), DiceFace.NormalNumber(2) }, + multiplier: 1, + triggerSource: "normal_roll"); + + var facts = ComboFactBuilder.Build(result); + + Assert.That(facts.DiceCount, Is.EqualTo(3)); + Assert.That(facts.CloverCount, Is.EqualTo(1)); + Assert.That(facts.NumberSum, Is.EqualTo(4)); + Assert.That(facts.NumberValues, Is.EqualTo(new[] { 2, 2 })); + Assert.That(facts.NormalizedKey, Is.EqualTo("2_2_clover")); + Assert.That(facts.OrderedKey, Is.EqualTo("clover_2_2")); + Assert.That(facts.FaceCounts["2"], Is.EqualTo(2)); + } + + [Test] + public void ComboFacts_reject_lucky_dice_results() + { + var result = DiceRollResult.Create( + "session-lucky", + "lucky_dice", + DiceSetType.Lucky, + new[] { DiceFace.LuckyResult("rocket") }, + multiplier: 1, + triggerSource: "normal_roll"); + + Assert.Throws(() => ComboFactBuilder.Build(result)); + } + + [TestCase("clover_clover", "normal_clover_clover_lucky_dice", "trigger_lucky_dice")] + [TestCase("2_clover", "normal_number_clover_bonus", "grant_reward,add_multiplier")] + [TestCase("2_6", "normal_all_numbers_reward", "grant_reward")] + public void RuleMatcher_uses_priority_and_matcher_type(string normalizedKey, string expectedRuleId, string expectedActionsCsv) + { + var facts = TestFacts.FromNormalizedKey(normalizedKey); + var rules = LuckyDiceDefaultConfig.Create().ComboRules; + + var result = new ComboRuleMatcher().Match(facts, rules); + + Assert.That(result.Matches.Select(match => match.RuleId), Is.EqualTo(new[] { expectedRuleId })); + Assert.That( + result.Matches.Single().Actions.Select(action => action.ActionType), + Is.EqualTo(expectedActionsCsv.Split(','))); + } + + [Test] + public void ComboRuleDefinitionLoader_converts_flat_rule_rows_to_runtime_rules() + { + var definitions = new[] + { + new ComboRuleDefinition + { + RuleId = "test_clover_bonus", + Priority = 25, + MatcherType = LuckyDiceKeys.ContainsFaceMatcher, + FaceKey = LuckyDiceKeys.CloverFaceKey, + RequiredCount = 1, + Actions = new[] + { + new RollActionDefinition { ActionType = LuckyDiceKeys.GrantRewardAction }, + new RollActionDefinition + { + ActionType = LuckyDiceKeys.AddMultiplierAction, + Params = new[] + { + new ActionParamDefinition { Key = "reason", Value = "test_bonus" } + } + } + } + } + }; + + var rules = ComboRuleDefinitionLoader.Load(definitions); + + Assert.That(rules.Count, Is.EqualTo(1)); + Assert.That(rules[0].RuleId, Is.EqualTo("test_clover_bonus")); + Assert.That(rules[0].Matcher.MatcherType, Is.EqualTo(LuckyDiceKeys.ContainsFaceMatcher)); + Assert.That(rules[0].Matcher.FaceKey, Is.EqualTo(LuckyDiceKeys.CloverFaceKey)); + Assert.That(rules[0].Matcher.RequiredCount, Is.EqualTo(1)); + Assert.That(rules[0].Actions.Select(action => action.ActionType), Is.EqualTo(new[] { "grant_reward", "add_multiplier" })); + Assert.That(rules[0].Actions[1].Params["reason"], Is.EqualTo("test_bonus")); + } + + [Test] + public void ActionDispatcher_dispatches_by_action_type_and_records_unregistered_actions() + { + var trace = new RollTrace("session-actions"); + var reward = new RecordingActionExecutor("grant_reward"); + var lucky = new RecordingActionExecutor("trigger_lucky_dice"); + var dispatcher = new RollActionDispatcher(new IRollActionExecutor[] { reward, lucky }); + var context = new RollActionContext("session-actions", "2_3", trace); + + var results = dispatcher.Execute( + context, + new[] + { + new RollActionSpec("grant_reward"), + new RollActionSpec("missing_action"), + new RollActionSpec("trigger_lucky_dice") + }); + + Assert.That(reward.Calls, Is.EqualTo(1)); + Assert.That(lucky.Calls, Is.EqualTo(1)); + Assert.That(results.Select(result => result.ActionType), Is.EqualTo(new[] { "grant_reward", "missing_action", "trigger_lucky_dice" })); + Assert.That(results.Single(result => result.ActionType == "missing_action").Success, Is.False); + Assert.That(trace.ExecutedActions, Is.EqualTo(new[] { "grant_reward", "missing_action", "trigger_lucky_dice" })); + } + + [TestCase("2_3", "normal_all_numbers_reward", "grant_reward", "Reward")] + [TestCase("2_clover", "normal_number_clover_bonus", "grant_reward,add_multiplier", "Multiplier")] + [TestCase("clover_clover", "normal_clover_clover_lucky_dice", "trigger_lucky_dice", "LuckyDiceRequested")] + public void Slice1_chain_matches_rules_dispatches_actions_and_keeps_trace( + string normalizedKey, + string expectedRuleId, + string expectedActionsCsv, + string expectedOutcome) + { + var config = LuckyDiceDefaultConfig.Create(); + var trace = new RollTrace("session-chain"); + var facts = TestFacts.FromNormalizedKey(normalizedKey, "session-chain"); + trace.RecordComboFacts(facts); + + var match = new ComboRuleMatcher().Match(facts, config.ComboRules); + trace.RecordRuleMatch(match); + + var dispatcher = new RollActionDispatcher( + new IRollActionExecutor[] + { + new GrantRewardExecutor(), + new AddMultiplierExecutor(), + new TriggerLuckyDiceExecutor() + }); + var context = new RollActionContext(trace.RollSessionId, facts.NormalizedKey, trace); + var actionResults = dispatcher.Execute(context, match.Matches.Single().Actions); + + Assert.That(trace.RollSessionId, Is.EqualTo("session-chain")); + Assert.That(trace.ComboNormalizedKey, Is.EqualTo(normalizedKey)); + Assert.That(trace.MatchedRuleId, Is.EqualTo(expectedRuleId)); + Assert.That(actionResults.Select(result => result.ActionType), Is.EqualTo(expectedActionsCsv.Split(','))); + Assert.That(trace.FinalOutcome, Is.EqualTo(expectedOutcome)); + } + + private sealed class FixedRandomSource : IRandomSource + { + private readonly Queue _values; + + public FixedRandomSource(params int[] values) + { + _values = new Queue(values); + } + + public int Next(int minInclusive, int maxExclusive) + { + var value = _values.Dequeue(); + Assert.That(value, Is.InRange(minInclusive, maxExclusive - 1)); + return value; + } + } + + private sealed class RecordingActionExecutor : IRollActionExecutor + { + public RecordingActionExecutor(string actionType) + { + ActionType = actionType; + } + + public string ActionType { get; } + + public int Calls { get; private set; } + + public RollActionResult Execute(RollActionContext context, RollActionSpec action) + { + Calls++; + return RollActionResult.Succeeded(action.ActionType, "Recorded"); + } + } + + private static class TestFacts + { + public static ComboFacts FromNormalizedKey(string normalizedKey, string rollSessionId = "session-rules") + { + var faces = normalizedKey.Split('_') + .Select(key => key == LuckyDiceKeys.CloverFaceKey + ? DiceFace.NormalClover() + : DiceFace.NormalNumber(int.Parse(key))) + .ToArray(); + + return ComboFactBuilder.Build(DiceRollResult.Create( + rollSessionId, + LuckyDiceKeys.NormalDiceSetId, + DiceSetType.Normal, + faces, + multiplier: 1, + triggerSource: LuckyDiceKeys.NormalRollTriggerSource)); + } + } + } +} diff --git a/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceSlice1Tests.cs.meta b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceSlice1Tests.cs.meta new file mode 100644 index 0000000..c88d409 --- /dev/null +++ b/FishDice/Assets/FishDice/Tests/EditMode/LuckyDice/LuckyDiceSlice1Tests.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5a3c5af6cd9a43729f1fd1dc92a217a6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/FishDice/Docs/Architecture/lucky-dice-core-flow-architecture-blueprint.md b/FishDice/Docs/Architecture/lucky-dice-core-flow-architecture-blueprint.md new file mode 100644 index 0000000..cbcfe51 --- /dev/null +++ b/FishDice/Docs/Architecture/lucky-dice-core-flow-architecture-blueprint.md @@ -0,0 +1,487 @@ +# Lucky Dice 核心流程架构蓝图 + +## 文档信息 + +- 来源 PRD:[lucky-dice-core-flow-prd.md](../PRD/lucky-dice-core-flow-prd.md) +- 关联需求:[lucky-dice-core-flow-requirements.md](../Requirements/lucky-dice-core-flow-requirements.md) +- 关联规格:[lucky-dice-core-flow-spec.md](../Specs/lucky-dice-core-flow-spec.md) +- 适用项目:FishDice Unity 项目 +- Unity 版本:2022.3.62f2c1 +- 生成日期:2026-06-22 + +本文档定义 Lucky Dice 第一版核心闭环的架构蓝图。它不替代需求或接口规格,而是回答后续实现时最容易失控的几个问题: + +- 普通 Roll、组合规则、Lucky Dice、目标玩法入口之间怎么分层。 +- 哪些模块可以通过配置扩展,哪些模块才需要新增代码。 +- 数据从玩家 Roll 到最终玩法模式如何流动。 +- 追踪、兜底、测试和 Unity 落地目录应如何组织。 + +当前项目尚未落地生产 C# 玩法模块,因此本文档描述的是第一版建议架构,而不是对现有代码的复盘。 + +## 架构定位 + +Lucky Dice 不是普通骰子的二次 Roll,也不是一个独立短玩法本体。它在架构中承担的是“特殊玩法入口选择器”职责: + +```text +普通 Roll 负责产出组合事实 +组合规则负责决定业务行为 +Lucky Dice 负责从特殊结果池中选择目标入口 +目标玩法模块负责执行 Slap Down / Treasure Heist 等具体内容 +``` + +第一版架构应优先保证三件事: + +1. **可扩展**:新增组合、骰子数量、特殊结果、目标玩法模式时优先改配置。 +2. **可测试**:组合事实、规则匹配、筛选、权重随机都能用确定性输入测试。 +3. **可追踪**:一次 Roll 的所有关键决策能用同一个 `RollSessionId` 串起来。 + +## 架构原则 + +### 两套骰子集合隔离 + +普通骰子集合只表达主循环 Roll 的结果,例如 `2 / 3 / 4 / 5 / 6 / clover`。Lucky Dice 特殊结果集合只表达后续玩法入口,例如 `rocket / thief`。 + +两者不能混用同一套裸字符串,也不能让普通组合匹配器直接处理 Lucky Dice 特殊结果。 + +### 规则匹配先于行为执行 + +普通 Roll 结果必须先转换成结构化 `ComboFacts`,再交给规则匹配器。业务行为不应读取“左骰 / 右骰”这类固定位置状态。 + +```text +DiceRollResult +→ ComboFacts +→ ComboRule +→ RollActionSpec +→ IRollActionExecutor +``` + +### 配置增长优先于代码增长 + +组合数量增加时,应增加 `ComboRule` 配置。只有出现新的通用匹配能力时才新增 matcher,只有出现新的通用业务行为时才新增 executor。 + +禁止形成下面这种随组合数量膨胀的结构: + +```text +clover_clover -> TriggerLuckyDice() +2_clover -> GrantCloverReward() +3_clover -> GrantCloverReward3() +... +``` + +### 流程服务不依赖表现层 + +核心流程可以产出展示所需数据,例如 Lucky Dice 标题、倍率、结果槽和目标玩法反馈,但不能依赖 UI 动画完成时序来做核心决策。 + +第一版表现层只消费核心结果,不参与筛选、随机和模式解析。 + +## 总览图 + +```mermaid +flowchart TD + Player["玩家触发 Roll"] --> RollService["DiceRollService"] + RollService --> FactBuilder["ComboFactBuilder"] + FactBuilder --> RuleMatcher["ComboRuleMatcher"] + RuleMatcher --> ActionDispatcher["RollActionDispatcher"] + + ActionDispatcher --> Reward["RewardExecutor"] + ActionDispatcher --> Multiplier["MultiplierExecutor"] + ActionDispatcher --> LuckyTrigger["TriggerLuckyDiceExecutor"] + + LuckyTrigger --> LuckyFlow["LuckyDiceFlowService"] + LuckyFlow --> CandidateFilters["CandidateFilters"] + CandidateFilters --> WeightedPicker["WeightedPicker"] + WeightedPicker --> SlotBuilder["ResultSlotBuilder"] + SlotBuilder --> ModeResolver["TargetModeResolver"] + ModeResolver --> Launcher["TargetModeLauncher"] + Launcher --> TargetGameplay["Slap Down / Treasure Heist"] + TargetGameplay --> MainLoop["回到主循环"] + + RollService -.-> Trace["RollTrace"] + RuleMatcher -.-> Trace + ActionDispatcher -.-> Trace + LuckyFlow -.-> Trace + ModeResolver -.-> Trace +``` + +## 分层架构 + +| 层级 | 主要职责 | 允许依赖 | 禁止依赖 | +| --- | --- | --- | --- | +| 配置层 | 提供骰子集合、规则、候选结果、目标模式映射 | Unity 资源、本地 JSON、静态配置 | UI 动画状态、运行时随机结果 | +| Roll 层 | 根据 DiceSet 生成普通骰子结果 | 配置层、随机源 | 规则、奖励、玩法入口 | +| 组合事实层 | 把普通 Roll 结果转为统计事实 | Roll 层数据 | Lucky Dice 特殊结果、UI | +| 规则匹配层 | 按 matcher 和优先级命中规则 | 组合事实、规则配置 | 行为执行器、目标玩法 | +| 行为执行层 | 按 action type 分发通用行为 | 规则结果、上下文服务 | 组合 key 到独立方法映射 | +| Lucky Dice 层 | 候选池、筛选、权重随机、结果槽 | 候选配置、上下文、随机源 | 普通组合 matcher | +| 模式解析层 | `ResultKey -> TargetKey -> ModeKey` | Lucky Dice 结果、上下文、模式配置 | 短玩法内部实现 | +| 表现层 | 展示 Lucky Dice 标题、倍率、结果槽、跳转反馈 | 核心流程产物 | 核心规则决策 | +| 目标玩法层 | 执行 Slap Down / Treasure Heist 等玩法 | TargetModeEntry | Lucky Dice 随机细节 | +| 追踪层 | 汇总关键决策链路 | 所有核心层事件 | 改变玩法结果 | + +依赖方向应从上游输入流向下游结果。追踪层可以被各层写入,但不应反向影响流程决策。 + +## 组件职责 + +### DiceSetConfigProvider + +负责提供骰子集合配置,包括普通骰子面、普通骰子数量、Lucky Dice 特殊结果面、默认结果槽数量。 + +第一版可以使用 ScriptableObject、JSON 或硬编码本地配置启动,但调用方必须只通过 provider 读取,不把 `2` 颗骰子或 `3` 个结果槽写死到流程逻辑中。 + +### DiceRollService + +负责按骰子集合配置生成 Roll 结果。普通 Roll 会产出多个普通骰子面;Lucky Dice 第一版不需要真的 Roll 三颗特殊骰,而是由候选选择结果再生成展示槽。 + +该服务只负责“随机出可见骰子面”,不负责奖励、倍率或玩法入口。 + +### ComboFactBuilder + +负责把普通 Roll 结果转成 `ComboFacts`: + +- 原始面列表 +- 面数量统计 +- 数字列表 +- 点数和 +- Clover 数量 +- 无序归一化 key +- 有序 key + +它必须支持 N 颗骰子。`2 + clover` 与 `clover + 2` 的无序 key 必须一致。 + +### ComboRuleMatcher + +负责把 `ComboFacts` 与规则配置匹配。matcher 按类型注册,例如 `ExactCombo`、`AllNumbers`、`ContainsFace`、`FaceCountRange`。 + +第一版按优先级从高到低匹配,默认命中高优先级规则后停止继续匹配,除非规则配置声明可以继续匹配。 + +### RollActionDispatcher + +负责把命中规则中的 `RollActionSpec` 分发给对应 executor。 + +dispatcher 只能维护 `ActionType -> Executor` 注册表,不能维护组合 key 到方法的映射。 + +### LuckyDiceFlowService + +负责 Lucky Dice 的核心入口选择流程: + +```text +构建候选池 +→ 筛选候选池 +→ 处理空池兜底 +→ 权重随机 +→ 生成展示结果槽 +``` + +它不直接启动 Slap Down 或 Treasure Heist,而是把选中结果交给模式解析层。 + +### TargetModeResolver + +负责把 Lucky Dice 结果解析到具体目标玩法模式。 + +第一版映射: + +| ResultKey | TargetKey | ModeKey | +| --- | --- | --- | +| `rocket` | `slap_down` | `slap_down_normal` | +| `thief` | `treasure_heist` | `treasure_heist_normal` | + +后续教程模式、bonus 模式应通过 mode rule 扩展,而不是在 Lucky Dice 随机代码里写分支。 + +### TargetModeLauncher + +负责统一启动目标玩法。它接收 `TargetModeEntry`,把 Lucky Dice 的结果、倍率、触发来源、`RollSessionId` 传给目标玩法入口。 + +目标玩法结束后,通过明确的 completion 回调或流程事件回到主循环。 + +### RollTrace + +负责记录一次 Roll 的关键路径。追踪不是埋点管线本身,第一版可以是本地结构化日志或调试面板数据。 + +最小字段应覆盖: + +- 原始普通骰子结果 +- 组合事实和归一化 key +- 命中规则和执行行为 +- Lucky Dice 初始候选、过滤过程、兜底类型 +- 权重随机输入和选中结果 +- 解析出的 TargetKey 和 ModeKey +- 最终结果 + +## 核心数据流 + +### 普通 Roll + +```mermaid +sequenceDiagram + participant P as Player + participant R as DiceRollService + participant F as ComboFactBuilder + participant M as ComboRuleMatcher + participant A as RollActionDispatcher + participant T as RollTrace + + P->>R: Roll(normal_main) + R->>T: record normal faces + R->>F: DiceRollResult + F->>T: record ComboFacts + F->>M: ComboFacts + M->>T: record matched rule + M->>A: RollActionSpec list + A->>T: record executed actions +``` + +### Lucky Dice + +```mermaid +sequenceDiagram + participant A as TriggerLuckyDiceExecutor + participant L as LuckyDiceFlowService + participant F as CandidateFilters + participant W as WeightedPicker + participant S as ResultSlotBuilder + participant M as TargetModeResolver + participant T as RollTrace + + A->>L: LuckyDiceContext + L->>T: record initial candidates + L->>F: apply filters + F->>T: record filter decisions + F->>W: filtered candidates + W->>T: record random value and selected result + W->>S: selected candidate + S->>T: record result slots + S->>M: selected candidate + context + M->>T: record target mode +``` + +## 配置架构 + +第一版建议至少拆成四类配置: + +| 配置 | 示例内容 | 扩展方向 | +| --- | --- | --- | +| DiceSetConfig | `normal_main`、`lucky_dice`、骰子面、骰子数量、默认槽数 | 支持关卡自定义骰子数量 | +| ComboRuleConfig | 规则 id、优先级、matcher、actions | 策划新增组合语义 | +| LuckyDiceCandidateConfig | result、target、weight、enabled、slot count、default | 扩展特殊结果与概率 | +| TargetModeConfig | target、default mode、tutorial mode、bonus mode | 扩展玩法模式 | + +配置读取顺序建议: + +```text +本地默认配置 +→ 关卡或活动覆盖配置 +→ 运行时上下文修正 +``` + +远端热更不在第一版范围内,但数据结构应避免与 Unity 场景对象强绑定,给后续迁移到远端配置留出空间。 + +## Unity 落地目录建议 + +如果第一版开始实现 C# 模块,建议按运行时核心、配置、表现、测试分开: + +```text +FishDice/Assets/FishDice/ + Runtime/ + LuckyDice/ + Core/ + DiceRollService.cs + ComboFactBuilder.cs + ComboRuleMatcher.cs + RollActionDispatcher.cs + LuckyDiceFlowService.cs + TargetModeResolver.cs + Config/ + DiceSetConfig.cs + ComboRuleConfig.cs + LuckyDiceCandidateConfig.cs + TargetModeConfig.cs + Actions/ + GrantRewardExecutor.cs + AddMultiplierExecutor.cs + TriggerLuckyDiceExecutor.cs + EnterModeExecutor.cs + Filters/ + EnabledFilter.cs + ProgressFilter.cs + SourceFilter.cs + CooldownFilter.cs + TutorialFilter.cs + Presentation/ + LuckyDicePresenter.cs + LuckyDiceResultSlotView.cs + Trace/ + RollTrace.cs + RollTraceLogger.cs + Tests/ + EditMode/ + LuckyDice/ +``` + +第一版核心流程建议尽量放在纯 C# 类里,减少对 `MonoBehaviour` 的依赖。表现层再通过 Unity 组件消费核心流程产物。 + +## 错误处理与兜底 + +### 普通组合未命中 + +处理策略: + +1. 记录 `RollSessionId`、原始骰子面、`NormalizedKey`。 +2. 走普通奖励兜底,或在调试模式返回明确错误。 +3. 不进入 Lucky Dice。 + +### Lucky Dice 候选池为空 + +处理策略: + +1. 尝试使用配置的 default result。 +2. default result 不可用时,降级为普通奖励兜底。 +3. 记录每个 filter 过滤前后的候选数量和空池原因。 + +### 模式解析失败 + +处理策略: + +1. 记录 `ResultKey`、`TargetKey` 和上下文。 +2. 尝试使用目标玩法默认模式。 +3. 仍失败时降级为普通奖励兜底,并标记为配置错误。 + +兜底必须可观测,不能静默吞掉。否则后续配置错误会表现成“玩家只是没进 Lucky Dice”,很难定位。 + +## 测试架构 + +测试应围绕外部行为和完整链路,而不是私有实现细节。 + +### 单元测试 + +| 测试对象 | 重点 | +| --- | --- | +| ComboFactBuilder | N 骰统计、无序 key、有序 key、Clover 数量 | +| ComboRuleMatcher | 优先级、matcher 类型、停止继续匹配 | +| RollActionDispatcher | 按 action type 分发,不按 combo key 分发 | +| CandidateFilters | enabled、progress、source、cooldown、tutorial 过滤 | +| WeightedPicker | 固定随机源下结果可预测 | +| TargetModeResolver | rocket/thief 映射和默认模式 | + +### 链路测试 + +链路测试是第一版最重要的测试口: + +```text +给定 Roll 请求 +给定 DiceSet 配置 +给定 ComboRule 配置 +给定 Lucky Dice 候选池 +给定固定随机源 +断言最终 outcome +``` + +最小链路用例: + +- `2 + 3` 发普通奖励。 +- `2 + clover` 发普通奖励并提供 Clover bonus。 +- `clover + clover` 触发 Lucky Dice。 +- Lucky Dice 选中 `rocket` 后进入 `slap_down_normal`。 +- Lucky Dice 选中 `thief` 后进入 `treasure_heist_normal`。 +- 候选池为空时进入可观测兜底。 +- 普通骰子数量改成 3 后,组合事实和 matcher 仍可工作。 + +## 架构决策记录 + +完整 ADR 存放在 [Docs/Decisions](../Decisions/README.md)。本节只保留摘要,后续决策变更应优先新增或更新独立 ADR 文档。 + +### ADR-001:普通骰子和 Lucky Dice 特殊结果分成两套集合 + +[ADR-001](../Decisions/adr-001-separate-normal-and-lucky-dice-sets.md) + +上下文:普通骰子负责奖励组合,Lucky Dice 负责玩法入口。两者语义不同。 + +决策:使用 `DiceSetType.Normal` 和 `DiceSetType.Lucky` 区分集合,不让普通组合匹配器处理 Lucky Dice 特殊结果。 + +结果:避免数字结算和玩法入口混用。代价是配置和调试界面需要明确展示集合类型。 + +### ADR-002:Roll 结果先转组合事实,再匹配规则 + +[ADR-002](../Decisions/adr-002-build-combo-facts-before-rule-matching.md) + +上下文:第一版默认 2 颗骰子,但未来需要支持 3 颗、4 颗或关卡自定义骰子数量。 + +决策:所有普通 Roll 都先生成 `ComboFacts`,matcher 只依赖事实统计,不依赖固定位置。 + +结果:后续扩骰数量时风险更低。代价是第一版需要多写一层事实构建和测试。 + +### ADR-003:行为按 action type 注册 + +[ADR-003](../Decisions/adr-003-dispatch-actions-by-action-type.md) + +上下文:如果每个组合 key 绑定一个独立方法,组合增长会让执行层失控。 + +决策:规则只声明 action 列表,dispatcher 只按 `ActionType` 找 executor。 + +结果:新增组合多数情况下只改配置。代价是 action 参数需要有清晰校验。 + +### ADR-004:Lucky Dice 随机源可注入 + +[ADR-004](../Decisions/adr-004-inject-random-source-for-lucky-dice.md) + +上下文:权重随机必须可测试、可回放。 + +决策:Lucky Dice 权重随机通过 `IRandomSource` 或等价接口注入随机源。 + +结果:测试可预测,线上问题可复盘。代价是运行时需要统一管理随机源生命周期。 + +## 新功能开发蓝图 + +### 新增普通组合 + +1. 确认现有 matcher 能否表达该组合。 +2. 能表达时新增 `ComboRuleConfig`。 +3. 需要新业务效果时,优先复用现有 action。 +4. 只有确实出现新的通用行为时,新增 executor。 +5. 补一条规则匹配测试和一条链路测试。 + +### 新增 Lucky Dice 特殊结果 + +1. 在 Lucky Dice 特殊结果集合中新增 `ResultKey`。 +2. 在候选配置中声明 `TargetKey`、权重、启用条件、槽数、default 标记。 +3. 在目标模式配置中声明默认 `ModeKey`。 +4. 补候选筛选测试、权重随机测试和目标模式解析测试。 + +### 新增目标玩法模式 + +1. 在 `TargetModeConfig` 中增加模式映射。 +2. 如模式选择依赖上下文,新增 mode rule。 +3. 不修改 Lucky Dice 随机逻辑。 +4. 补 `TargetModeResolver` 测试。 + +### 接入表现动画 + +1. 核心流程先产出 `LuckyDiceResultSlots` 和 `TargetModeEntry`。 +2. 表现层播放标题、倍率、结果槽和跳转反馈。 +3. 动画完成后再调用 launcher,或先启动目标玩法再做过场,取决于产品节奏。 +4. 动画失败不能改变核心选择结果。 + +## 架构治理 + +实现和评审时重点检查以下规则: + +- 核心流程中没有固定读取第 0/1 颗骰子的业务判断。 +- 普通骰子和 Lucky Dice 特殊结果没有混用同一套集合。 +- 新增组合时没有新增组合 key 到独立方法的 map。 +- Lucky Dice 候选必须先筛选再随机。 +- 候选池为空、模式解析失败、未命中规则都有可观测兜底。 +- `RollSessionId` 贯穿普通 Roll、规则匹配、Lucky Dice、模式解析、目标玩法启动。 +- 表现层不参与规则匹配、候选筛选和权重随机。 + +## 第一版落地顺序 + +建议按以下顺序实现: + +1. 定义核心数据结构和本地默认配置。 +2. 实现普通 Roll、组合事实和规则匹配。 +3. 实现 action dispatcher 和普通奖励 / Clover bonus / trigger Lucky Dice 执行器。 +4. 实现 Lucky Dice 候选筛选、权重随机、结果槽生成。 +5. 实现 `rocket -> slap_down_normal` 和 `thief -> treasure_heist_normal` 模式解析。 +6. 实现最小表现层:标题、倍率、结果槽、目标玩法跳转反馈。 +7. 补齐链路测试和追踪字段。 + +这个顺序能先把核心闭环跑通,再逐步加表现和更多结果类型,避免一开始被动画和短玩法完整内容拖散。 diff --git a/FishDice/Docs/Decisions/README.md b/FishDice/Docs/Decisions/README.md new file mode 100644 index 0000000..951dd76 --- /dev/null +++ b/FishDice/Docs/Decisions/README.md @@ -0,0 +1,20 @@ +# Lucky Dice 架构决策记录 + +本目录记录 Lucky Dice 核心流程第一版中的重要架构决策。ADR 用来保存当时的背景、约束、备选方案和取舍结果,避免后续实现或扩展时重新讨论已经明确的架构边界。 + +## ADR 列表 + +| 编号 | 决策 | 状态 | +| --- | --- | --- | +| [ADR-001](adr-001-separate-normal-and-lucky-dice-sets.md) | 普通骰子和 Lucky Dice 特殊结果分成两套集合 | Accepted | +| [ADR-002](adr-002-build-combo-facts-before-rule-matching.md) | Roll 结果先转组合事实,再匹配规则 | Accepted | +| [ADR-003](adr-003-dispatch-actions-by-action-type.md) | 行为按 action type 注册和分发 | Accepted | +| [ADR-004](adr-004-inject-random-source-for-lucky-dice.md) | Lucky Dice 权重随机使用可注入随机源 | Accepted | + +## 状态说明 + +- `Accepted`:当前采用的决策。 +- `Superseded by ADR-XXX`:已经被后续 ADR 替代。 +- `Deprecated`:不再推荐,但未被某个明确新 ADR 替代。 + +ADR 不应删除。决策变化时新增一份 ADR,并在旧 ADR 的状态里标记替代关系。 diff --git a/FishDice/Docs/Decisions/adr-001-separate-normal-and-lucky-dice-sets.md b/FishDice/Docs/Decisions/adr-001-separate-normal-and-lucky-dice-sets.md new file mode 100644 index 0000000..f97bbd6 --- /dev/null +++ b/FishDice/Docs/Decisions/adr-001-separate-normal-and-lucky-dice-sets.md @@ -0,0 +1,116 @@ +# ADR-001: 普通骰子和 Lucky Dice 特殊结果分成两套集合 + +## Status + +Accepted + +## Date + +2026-06-22 + +## Context + +FishDice 第一版需要同时支持普通骰子主循环和 Lucky Dice 特殊分支。普通 Roll 的骰子面是 `2 / 3 / 4 / 5 / 6 / clover`,它们的职责是参与组合事实、规则匹配、普通奖励、Clover bonus 和触发 Lucky Dice。 + +Lucky Dice 进入后使用的是特殊结果,例如 `rocket`、`thief`。这些结果的职责不是点数结算,而是决定后续进入哪个短玩法,以及进入该玩法的哪个模式。 + +如果两类结果混用同一套裸字符串或同一个骰子集合,会带来几个风险: + +- 普通组合匹配器可能错误处理 Lucky Dice 特殊结果。 +- 策划配置容易把数字奖励结果和玩法入口结果混在一起。 +- 日志中很难判断某个 key 是普通骰子面还是 Lucky Dice 入口。 +- 后续扩展更多特殊结果时,会污染普通 Roll 的规则空间。 + +## Decision + +普通骰子和 Lucky Dice 特殊结果使用两套独立集合,并在数据模型中保留集合类型。 + +建议在代码中使用等价于以下概念的结构: + +```csharp +public enum DiceSetType +{ + Normal, + Lucky +} + +public readonly struct DiceFace +{ + public DiceSetType SetType { get; } + public string Key { get; } + public int? NumberValue { get; } +} +``` + +普通骰子集合只包含主循环 Roll 面。Lucky Dice 集合只包含特殊入口结果。普通组合事实构建器只接受 `DiceSetType.Normal` 的 Roll 结果,遇到 Lucky Dice 特殊结果时必须拒绝或返回结构化错误。 + +## Alternatives Considered + +### 使用同一个 DiceFace 字符串集合 + +优点: + +- 第一版实现最省事。 +- 配置表字段少。 + +缺点: + +- `clover`、`rocket`、`thief`、数字面都会落在同一个匹配空间。 +- 普通 matcher 需要不断写排除逻辑,防止特殊结果进入普通奖励结算。 +- 日志和调试工具无法从类型上识别语义边界。 + +结论:拒绝。它会把第一版的简单实现成本转化成后续规则扩展和排查成本。 + +### 使用同一个集合,但通过 key 前缀区分 + +示例:`normal_2`、`normal_clover`、`lucky_rocket`。 + +优点: + +- 比裸字符串更可读。 +- 不需要额外枚举字段。 + +缺点: + +- 类型边界依赖命名约定,配置错误时很难在编译或校验阶段发现。 +- matcher 仍然会看到同一批 key,需要人工遵守前缀规则。 +- 后续如果出现多套普通骰子或多套特殊结果,前缀会越来越复杂。 + +结论:拒绝。前缀可以作为日志展示或配置命名辅助,但不能替代结构化集合类型。 + +### 使用两套独立数据模型 + +普通骰子面和 Lucky Dice 特殊结果完全使用不同 class。 + +优点: + +- 类型隔离最强。 +- 不容易误用。 + +缺点: + +- Roll、展示槽、日志等通用结构需要重复建模。 +- 第一版通用流程会变得偏重。 + +结论:暂不采用。当前使用共同的 `DiceFace` 概念加 `DiceSetType` 已能满足隔离需求。 + +## Consequences + +正向影响: + +- 普通 Roll 和 Lucky Dice 的语义边界清晰。 +- 普通组合 matcher 不需要理解特殊玩法入口。 +- 日志可以明确记录本次处理的是 `Normal` 还是 `Lucky` 集合。 +- 后续新增特殊结果不会污染普通奖励规则。 + +代价和约束: + +- 配置工具和调试界面必须展示集合类型。 +- 配置加载需要校验普通集合不能包含 Lucky Dice 特殊结果。 +- 代码中不能到处裸写 key,需要集中定义 key 或配置入口。 + +## Related Documents + +- [Lucky Dice 核心流程 PRD](../PRD/lucky-dice-core-flow-prd.md) +- [Lucky Dice 核心流程规格](../Specs/lucky-dice-core-flow-spec.md) +- [Lucky Dice 核心流程架构蓝图](../Architecture/lucky-dice-core-flow-architecture-blueprint.md) diff --git a/FishDice/Docs/Decisions/adr-002-build-combo-facts-before-rule-matching.md b/FishDice/Docs/Decisions/adr-002-build-combo-facts-before-rule-matching.md new file mode 100644 index 0000000..ae72cb6 --- /dev/null +++ b/FishDice/Docs/Decisions/adr-002-build-combo-facts-before-rule-matching.md @@ -0,0 +1,117 @@ +# ADR-002: Roll 结果先转组合事实,再匹配规则 + +## Status + +Accepted + +## Date + +2026-06-22 + +## Context + +Lucky Dice 第一版普通 Roll 默认使用 2 颗骰子,但 PRD 明确要求骰子数量必须来自配置,并保留后续扩展到 3 颗、4 颗或特殊关卡自定义骰子数量的能力。 + +普通 Roll 的规则也不只包含精确组合。首批规则需要覆盖: + +- 双 Clover 触发 Lucky Dice。 +- 单 Clover 加数字发放普通奖励并提供 bonus。 +- 纯数字结果发放普通奖励。 + +后续还需要支持对子、N 连、指定面数量、点数和区间、骰子数量约束等统计型规则。 + +如果规则匹配直接读取固定位置,例如左骰和右骰,就会把第一版默认值 `2` 固化到系统里。后续扩展 3 骰时,规则、测试和日志都要大范围修改。 + +## Decision + +普通 Roll 结果必须先转换成结构化 `ComboFacts`,再进入规则匹配。 + +`ComboFacts` 至少包含: + +- `DiceSetType` +- `DiceCount` +- 原始 `Faces` +- `FaceCounts` +- `NumberValues` +- `NumberSum` +- `CloverCount` +- `HasClover` +- `NormalizedKey` +- `OrderedKey` + +普通规则默认使用无序事实。`2 + clover` 与 `clover + 2` 必须归一为同一个 `NormalizedKey`。只有规则明确声明需要顺序语义时,才允许读取 `OrderedKey`。 + +matcher 必须基于 `ComboFacts` 中的统计信息判断,不能依赖固定的左骰、右骰字段。 + +## Alternatives Considered + +### 在规则里直接读取骰子数组位置 + +示例:`faces[0] == clover && faces[1] == clover`。 + +优点: + +- 第一版双骰场景写起来最快。 +- 不需要单独的事实构建层。 + +缺点: + +- 默认写死 2 骰。 +- 顺序和无序语义容易混乱。 +- 后续 3 骰、4 骰会导致规则分支爆炸。 +- 测试会绑定实现细节,而不是外部行为。 + +结论:拒绝。它与可配置骰子数量的核心要求冲突。 + +### 只生成字符串组合 key + +示例:直接生成 `2_clover`、`clover_clover`,规则只匹配 key。 + +优点: + +- 对精确组合匹配足够简单。 +- 配置和日志易读。 + +缺点: + +- 难以表达统计型规则,例如至少 2 个 Clover、三连、点数和区间。 +- matcher 会被迫解析字符串,或者需要维护大量派生 key。 +- 原始骰子和统计信息缺失,不利于调试。 + +结论:拒绝作为唯一事实来源。`NormalizedKey` 可以作为 `ComboFacts` 的一个字段,但不能替代完整事实。 + +### 由每个 matcher 自己计算统计信息 + +优点: + +- 不需要统一事实模型。 +- matcher 内部可以自由处理自己的需求。 + +缺点: + +- 多个 matcher 重复遍历和统计。 +- 不同 matcher 对 key、顺序、数字值的理解可能不一致。 +- 日志难以记录统一的匹配输入。 + +结论:拒绝。统一事实模型更利于一致性和测试。 + +## Consequences + +正向影响: + +- 后续扩展骰子数量时,核心规则仍基于统计事实工作。 +- 规则匹配器可以用少量通用 matcher 覆盖多种组合语义。 +- 日志能记录原始结果、统计结果、无序 key 和有序 key。 +- 测试可以直接验证外部行为和事实结构。 + +代价和约束: + +- 第一版需要实现并测试 `ComboFactBuilder`。 +- `ComboFacts` 字段需要保持稳定,不能随意为某个规则塞临时字段。 +- Lucky Dice 特殊结果不能进入普通 `ComboFactBuilder`。 + +## Related Documents + +- [Lucky Dice 核心流程需求](../Requirements/lucky-dice-core-flow-requirements.md) +- [Lucky Dice 核心流程规格](../Specs/lucky-dice-core-flow-spec.md) +- [ADR-001](adr-001-separate-normal-and-lucky-dice-sets.md) diff --git a/FishDice/Docs/Decisions/adr-003-dispatch-actions-by-action-type.md b/FishDice/Docs/Decisions/adr-003-dispatch-actions-by-action-type.md new file mode 100644 index 0000000..71869d7 --- /dev/null +++ b/FishDice/Docs/Decisions/adr-003-dispatch-actions-by-action-type.md @@ -0,0 +1,116 @@ +# ADR-003: 行为按 action type 注册和分发 + +## Status + +Accepted + +## Date + +2026-06-22 + +## Context + +PRD 要求普通骰子的不同组合能触发不同业务行为,并且后续可以新增组合、骰子数量、特殊结果或目标玩法。第一版最小组合包括: + +- 数字 + 数字:普通奖励。 +- 数字 + Clover:普通奖励加 Clover bonus。 +- Clover + Clover:触发 Lucky Dice。 + +这些组合行为本质上由少量通用动作组合而成,例如发奖、增加倍率、触发 Lucky Dice、进入玩法模式、显示弹窗。 + +如果为每个组合 key 绑定一个独立方法,第一版可以很快写完,但后续新增组合时执行层会不断膨胀,形成难以维护的分支表。 + +## Decision + +规则配置只声明要执行的 action 列表。执行层通过 `ActionType` 注册和分发通用 executor。 + +示例: + +```text +grant_reward -> GrantRewardExecutor +add_multiplier -> AddMultiplierExecutor +trigger_lucky_dice -> TriggerLuckyDiceExecutor +enter_mode -> EnterModeExecutor +show_popup -> ShowPopupExecutor +``` + +`RollActionDispatcher` 只能维护 `ActionType -> IRollActionExecutor` 注册表,不能维护 `combo key -> method` 注册表。 + +组合规则增长时,优先新增或修改规则配置。只有出现新的通用行为类型时,才新增 executor。 + +## Alternatives Considered + +### 每个组合 key 绑定独立方法 + +示例: + +```text +clover_clover -> TriggerLuckyDice() +number_clover -> GrantCloverBonus() +number_number -> GrantNormalReward() +``` + +优点: + +- 第一版逻辑直观。 +- 单个组合方法容易断点调试。 + +缺点: + +- 组合数量增长会直接推动方法数量增长。 +- 相同行为会在多个方法里重复。 +- 策划新增组合含义时,工程通常必须新增方法。 +- 很难支持一个组合同时执行多个通用行为。 + +结论:拒绝。它正是 PRD 中要求避免的硬编码分支。 + +### 规则 matcher 直接执行业务行为 + +优点: + +- 少一层 dispatcher。 +- matcher 命中后可以立刻执行。 + +缺点: + +- matcher 同时承担判断和副作用,测试困难。 +- 同一个 matcher 无法复用于不同业务动作。 +- 规则优先级、继续匹配、行为顺序会混在一起。 + +结论:拒绝。匹配和执行必须分离。 + +### 用事件系统广播所有命中结果 + +优点: + +- 行为扩展灵活。 +- 多个系统可以监听同一个结果。 + +缺点: + +- 第一版流程需要确定性和可追踪,广播式事件容易让执行顺序不透明。 +- 兜底、失败和链路测试会更复杂。 +- 当前没有足够多的跨系统监听需求。 + +结论:暂不采用。后续如接入更多外围系统,可以在 executor 内部再发布领域事件,但核心流程仍由 action list 驱动。 + +## Consequences + +正向影响: + +- 新增组合多数情况下只改规则配置。 +- 通用行为可以复用和组合。 +- 链路测试可以断言 action list 和最终 outcome。 +- 未注册 action 可以返回结构化错误并写入追踪。 + +代价和约束: + +- action 参数必须有校验,避免配置错误运行到一半才失败。 +- executor 的职责边界需要清楚,不能把组合判断重新塞回 executor。 +- dispatcher 必须保留执行顺序和失败策略。 + +## Related Documents + +- [Lucky Dice 核心流程 PRD](../PRD/lucky-dice-core-flow-prd.md) +- [Lucky Dice 核心流程规格](../Specs/lucky-dice-core-flow-spec.md) +- [ADR-002](adr-002-build-combo-facts-before-rule-matching.md) diff --git a/FishDice/Docs/Decisions/adr-004-inject-random-source-for-lucky-dice.md b/FishDice/Docs/Decisions/adr-004-inject-random-source-for-lucky-dice.md new file mode 100644 index 0000000..0935db0 --- /dev/null +++ b/FishDice/Docs/Decisions/adr-004-inject-random-source-for-lucky-dice.md @@ -0,0 +1,113 @@ +# ADR-004: Lucky Dice 权重随机使用可注入随机源 + +## Status + +Accepted + +## Date + +2026-06-22 + +## Context + +Lucky Dice 需要在候选结果池筛选后按权重随机选择一个结果。第一版至少需要支持: + +- `rocket` 进入 `slap_down_normal`。 +- `thief` 进入 `treasure_heist_normal`。 +- 候选结果可按启用状态、玩家进度、触发来源、冷却状态和新手期规则筛选。 +- 候选池为空时有可观测兜底。 + +测试策略要求固定随机源下的权重随机结果可预测。PRD 还要求每次 Roll 都能通过同一个 `RollSessionId` 串起普通 Roll、规则匹配、Lucky Dice 结果和目标玩法启动,方便排查和回放。 + +如果 Lucky Dice 流程直接调用 Unity 或系统随机,会让链路测试不稳定,也难以复盘线上问题。 + +## Decision + +Lucky Dice 权重随机必须通过可注入随机源执行。 + +建议使用等价于以下概念的接口: + +```csharp +public interface IRandomSource +{ + int Range(int minInclusive, int maxExclusive); + float Value01(); +} +``` + +`WeightedPicker` 接收筛选后的候选列表、上下文和随机源。它需要记录: + +- 候选列表快照。 +- 每个候选的有效权重。 +- 随机值。 +- 最终选中的 `ResultKey`。 + +测试环境可以注入固定序列随机源。运行时环境可以注入 Unity 随机适配器或项目统一随机服务。 + +## Alternatives Considered + +### 直接使用 UnityEngine.Random + +优点: + +- Unity 项目中最容易使用。 +- 不需要额外接口。 + +缺点: + +- 测试难以稳定复现。 +- 多个系统共享全局随机状态,结果容易受调用顺序影响。 +- 回放和问题定位难度高。 + +结论:拒绝在核心逻辑中直接使用。可以通过适配器在运行时接入 Unity 随机。 + +### 直接使用 System.Random + +优点: + +- 可指定 seed。 +- 不依赖 Unity API,适合 EditMode 测试。 + +缺点: + +- 如果在核心服务内部自行 new,仍然无法由测试控制。 +- seed 生命周期和 RollSessionId 的关系不透明。 +- 后续如果项目已有统一随机服务,需要再改接口。 + +结论:不直接在核心流程中创建。可以作为 `IRandomSource` 的一个实现。 + +### 配置固定结果,不做随机 + +优点: + +- 第一版实现和测试最简单。 +- 结果完全可控。 + +缺点: + +- 不满足 Lucky Dice 候选结果支持权重的需求。 +- 后续接入概率策略时需要重构核心流程。 +- 无法提前验证候选筛选和权重随机之间的边界。 + +结论:拒绝。第一版可以配置极简权重,但流程必须保留权重随机。 + +## Consequences + +正向影响: + +- 链路测试可以稳定断言 Lucky Dice 最终结果。 +- 回放和问题排查可以复用相同随机输入。 +- `WeightedPicker` 不依赖 Unity API,更容易做纯 C# 测试。 +- 后续接入活动概率策略时,随机入口清晰。 + +代价和约束: + +- 需要实现至少两个随机源:运行时随机源和测试固定随机源。 +- 需要记录随机值和候选权重,否则可注入随机源的排查价值会下降。 +- 随机源生命周期需要统一管理,避免同一 Roll 中不同阶段使用不同来源。 + +## Related Documents + +- [Lucky Dice 核心流程 PRD](../PRD/lucky-dice-core-flow-prd.md) +- [Lucky Dice 核心流程规格](../Specs/lucky-dice-core-flow-spec.md) +- [ADR-003](adr-003-dispatch-actions-by-action-type.md) diff --git a/FishDice/Docs/PRD/lucky-dice-core-flow-prd.md b/FishDice/Docs/PRD/lucky-dice-core-flow-prd.md new file mode 100644 index 0000000..2eed0b1 --- /dev/null +++ b/FishDice/Docs/PRD/lucky-dice-core-flow-prd.md @@ -0,0 +1,144 @@ +# Lucky Dice 核心流程 PRD + +## 问题陈述 + +FishDice 需要一套可扩展的骰子主循环和 Lucky Dice 特殊分支。当前目标不是只做一次普通骰子结算,而是让普通骰子的不同组合能够触发不同业务行为,并在双四叶草等稀有组合出现时进入 Lucky Dice。Lucky Dice 需要使用独立的特殊结果池,经过筛选和随机后产出目标结果,再把玩家带入 Slap Down、Treasure Heist 等短玩法。 + +如果把每个组合直接绑定到一个独立执行方法,后续新增组合、骰子数量、特殊结果或目标玩法时,会形成越来越大的硬编码分支。产品和策划侧也需要能清楚表达“什么组合触发什么行为”“Lucky Dice 怎么筛选结果”“同一个目标玩法进入什么模式”,否则第一版完成后很难继续扩展活动玩法。 + +## 解决方案 + +建立一套分层的玩法规则系统。 + +普通 Roll 使用普通骰子集合。第一版默认骰子面为 `2 / 3 / 4 / 5 / 6 / Clover`,默认投 2 颗骰子,但骰子数量必须来自配置。Roll 结果先转换成结构化组合事实,再由规则匹配器按优先级匹配规则,并执行规则配置的行为列表。 + +Lucky Dice 使用独立的特殊结果集合,不与普通数字骰混用。它不是“普通骰子再摇一次”,而是一个特殊玩法入口选择流程:构建候选结果池,按玩家上下文筛选候选,按权重随机,生成结果槽展示,再解析目标玩法和具体进入模式。 + +第一版只保留核心闭环,忽略复杂动画表现。玩家只需要看到 Lucky Dice 标题、倍率、结果槽图标和目标玩法跳转。托盘、骰子飞入、粒子爆发、白烟转场等表现不进入第一版必做范围。 + +## 用户故事 + +1. 作为玩家,我希望普通掷骰能产出清晰结果,从而知道每次 Roll 都有意义。 +2. 作为玩家,我希望纯数字结果能发放普通奖励,从而保证普通流程有稳定收益。 +3. 作为玩家,我希望单个 Clover 也有轻微特殊反馈,从而让 Clover 即使不触发 Lucky Dice 也有价值。 +4. 作为玩家,我希望双 Clover 能触发 Lucky Dice,从而让稀有组合带来惊喜。 +5. 作为玩家,我希望 Lucky Dice 是一个独立流程,从而明确知道触发了特殊事件。 +6. 作为玩家,我希望 Lucky Dice 结果能清楚显示特殊图标,从而知道即将进入哪个短玩法。 +7. 作为玩家,我希望 Rocket 结果进入 Slap Down,从而让结果和后续玩法有明确绑定。 +8. 作为玩家,我希望 Thief 结果进入 Treasure Heist,从而让不同 Lucky Dice 结果有不同意义。 +9. 作为玩家,我希望目标玩法结束后能回到主循环,从而保持游戏流程连续。 +10. 作为策划,我希望普通骰子数量可配置,从而未来可以支持 3 颗、4 颗或特殊关卡自定义骰子数量。 +11. 作为策划,我希望 Lucky Dice 结果槽数量可配置,从而未来可以支持 2 格、3 格、4 格或按玩法自定义槽位。 +12. 作为策划,我希望组合规则数据化,从而新增组合含义时不需要每次都让工程新增独立方法。 +13. 作为策划,我希望组合匹配支持精确组合、面数量、对子、N 连、点数和区间等能力,从而覆盖简单和复杂规则。 +14. 作为策划,我希望组合规则支持优先级,从而让双 Clover 这类稀有触发优先于普通奖励规则。 +15. 作为策划,我希望组合行为由可复用行为组成,从而一个组合可以同时发奖、加倍率、展示反馈或进入 Lucky Dice。 +16. 作为策划,我希望 Lucky Dice 候选结果可筛选,从而未解锁、未开启或冷却中的目标玩法不会被抽中。 +17. 作为策划,我希望 Lucky Dice 候选结果支持权重,从而可以调节不同结果概率。 +18. 作为策划,我希望新手期规则可以影响 Lucky Dice 结果,从而更好地控制早期体验。 +19. 作为策划,我希望一个 Lucky Dice 结果先解析目标玩法,再解析具体模式,从而同一玩法可以支持教程、普通、bonus 等模式。 +20. 作为策划,我希望 Lucky Dice 候选池为空时有兜底行为,从而配置错误不会卡死玩家流程。 +21. 作为开发者,我希望普通骰子和 Lucky Dice 特殊结果分属两套骰子集合,从而避免数字结算和目标玩法入口混用。 +22. 作为开发者,我希望 Roll 结果先转成组合事实,从而规则可以基于数据匹配,而不是依赖固定位置判断。 +23. 作为开发者,我希望组合事实保留原始骰子、数量统计和归一化 key,从而便于调试和未来扩展。 +24. 作为开发者,我希望规则匹配只使用少量通用匹配器,从而系统优先通过配置扩展,而不是不断增加代码分支。 +25. 作为开发者,我希望行为分发按行为类型注册,从而不需要维护巨大的“组合 key 到方法”映射表。 +26. 作为开发者,我希望 Lucky Dice 随机源可注入,从而测试和回放可以稳定复现。 +27. 作为开发者,我希望每次 Roll 都携带同一个会话 id,从而普通 Roll、规则匹配、Lucky Dice 结果和目标玩法启动可以串起来排查。 +28. 作为测试人员,我希望组合事实和规则匹配有确定性测试,从而普通组合和稀有组合都稳定可靠。 +29. 作为测试人员,我希望兜底场景被覆盖,从而候选池为空、模式映射失败等问题可以被观测到。 +30. 作为产品负责人,我希望第一版范围足够收敛,从而先验证玩法闭环,再投入完整动画表现。 + +## 实现决策 + +- 使用两套骰子集合:普通 Roll 骰子和 Lucky Dice 特殊结果骰子。 +- 普通 Roll 第一版使用 `2 / 3 / 4 / 5 / 6 / Clover`。 +- 普通 Roll 第一版默认 2 颗骰子,但数量必须来自骰子集合配置。 +- Lucky Dice 第一版默认 3 个结果槽,但数量必须来自候选结果、目标玩法或骰子集合配置。 +- 普通 Roll 结果必须先转换成组合事实,再进入规则匹配。 +- 组合事实必须包含骰子数量、原始骰子面、面数量统计、数字列表、点数和、Clover 数量、无序归一化 key、有序 key。 +- 普通组合默认按无序组合匹配。 +- 只有规则明确声明需要顺序语义时,才使用有序 key。 +- 不允许每个组合 key 绑定一个独立方法。 +- 使用少量可复用匹配器注册表。 +- 首批匹配器包括精确组合、全数字、包含指定面、指定面数量、指定面数量区间、骰子数量、数字对子、N 连、点数和区间。 +- 规则必须支持优先级。 +- 第一版默认命中高优先级规则后停止继续匹配,除非规则配置另有声明。 +- 使用少量可复用行为执行器注册表。 +- 首批行为包括发放奖励、增加倍率、触发 Lucky Dice、进入玩法模式、显示弹窗。 +- 第一版双 Clover 触发 Lucky Dice。 +- 第一版单 Clover 加数字发放普通奖励并提供 Clover bonus。 +- 第一版纯数字结果发放普通奖励。 +- Lucky Dice 必须先构建候选池,再做随机。 +- Lucky Dice 必须先筛选候选池,再做随机。 +- 首批筛选器包括是否开启、玩家进度、触发来源、冷却状态、新手期控制。 +- Lucky Dice 筛选后按权重随机。 +- Lucky Dice 候选池为空时必须有可观测兜底。 +- 第一优先兜底是配置的默认结果。 +- 如果默认结果也不可用,则降级为普通奖励兜底。 +- Lucky Dice 随机结果按 `ResultKey → TargetKey → ModeKey` 解析。 +- Rocket 第一版解析到 Slap Down 普通模式。 +- Thief 第一版解析到 Treasure Heist 普通模式。 +- 教程模式和 bonus 模式作为扩展设计保留,第一版只要求普通模式可用。 +- 第一版表现层只需要 Lucky Dice 标题、倍率显示、可配置结果槽和目标玩法跳转反馈。 +- 每次 Roll 必须产出追踪信息,包括原始结果、命中规则、执行行为、Lucky Dice 候选决策、选中结果和最终模式。 + +## 测试决策 + +最高层测试口应放在完整 Roll 解析流程:给定 Roll 请求、骰子集合配置、组合规则、Lucky Dice 候选池和受控随机源,系统应产出最终结果,例如普通奖励、Lucky Dice 进入 Slap Down、Lucky Dice 进入 Treasure Heist。 + +低层测试仍然需要,但它们应服务于完整流程测试,而不是替代完整流程测试。 + +- 测试外部行为,不测试私有实现细节。 +- 测试可见骰子结果能正确生成组合事实。 +- 测试 `2 + Clover` 和 `Clover + 2` 都能归一为同一个无序组合。 +- 测试至少一个 3 骰组合事实,确保系统不写死 2 颗骰子。 +- 测试双 Clover 优先级高于泛用 Clover 行为。 +- 测试纯数字结果发放普通奖励。 +- 测试单 Clover 加数字发放普通奖励并提供 Clover bonus。 +- 测试行为分发按行为类型执行,而不是按组合 key 执行。 +- 测试双 Clover 能进入 Lucky Dice。 +- 测试 Lucky Dice 筛选器能过滤未开启、未解锁或来源不匹配的候选。 +- 测试固定随机源下的权重随机结果可预测。 +- 测试 Lucky Dice 候选池为空时进入兜底。 +- 测试 Rocket 解析到 Slap Down 普通模式。 +- 测试 Thief 解析到 Treasure Heist 普通模式。 +- 测试 Lucky Dice 结果槽数量来自配置。 +- 测试普通骰子数量从 2 改为 3 后,组合事实和匹配器仍按统计结果工作。 +- 测试追踪信息足够解释最终结果。 + +当前项目还没有既有玩法测试套件和生产 C# 模块,因此这些是第一版实现建议采用的测试口,而不是对现有测试的引用。 + +## 不在范围内 + +- 完整复刻参考 Lucky Dice 动画。 +- 托盘动画、骰子飞入、粒子爆发、白烟转场和完整表现节奏。 +- 完整实现 Slap Down 玩法。 +- 完整实现 Treasure Heist 玩法。 +- 实现所有 Lucky Dice 特殊结果类型。 +- 远端配置热更新。 +- 复杂运营活动概率策略。 +- 埋点管线集成,第一版只要求本地追踪字段。 +- 为每个组合 key 写独立业务方法。 +- 假设普通骰子永远只有 2 颗。 +- 假设 Lucky Dice 永远只有 3 个结果槽。 + +## 补充说明 + +本地 HTML 拆解中的竞品流程应作为产品参考,而不是第一版逐帧复刻目标。第一版核心价值是建立可扩展闭环: + +```text +普通 Roll +→ 组合事实 +→ 规则匹配 +→ 行为执行 +→ 触发 Lucky Dice +→ 候选筛选 +→ 权重随机 +→ 目标玩法模式解析 +→ 回到主循环 +``` + +第一版应优先保证清晰和可调试。每个关键决策都应能从追踪信息中解释:为什么命中某个组合、为什么进入 Lucky Dice、哪些候选被过滤、最终选中了什么结果、启动了哪个玩法模式。 + +`to-prd` 技能说明中提到需要发布到 issue tracker 并添加 `ready-for-agent` 标签。但当前项目没有暴露 issue tracker 集成或 triage label 配置,所以本 PRD 先作为本地项目文档落地。 diff --git a/FishDice/Docs/Plans/lucky-dice-core-flow-feature-breakdown.md b/FishDice/Docs/Plans/lucky-dice-core-flow-feature-breakdown.md new file mode 100644 index 0000000..02cfb6c --- /dev/null +++ b/FishDice/Docs/Plans/lucky-dice-core-flow-feature-breakdown.md @@ -0,0 +1,532 @@ +# Lucky Dice 核心流程功能划分 + +## 文档信息 + +- 父级 PRD:[lucky-dice-core-flow-prd.md](../PRD/lucky-dice-core-flow-prd.md) +- 需求文档:[lucky-dice-core-flow-requirements.md](../Requirements/lucky-dice-core-flow-requirements.md) +- 技术规格:[lucky-dice-core-flow-spec.md](../Specs/lucky-dice-core-flow-spec.md) +- 架构蓝图:[lucky-dice-core-flow-architecture-blueprint.md](../Architecture/lucky-dice-core-flow-architecture-blueprint.md) +- 架构决策:[Docs/Decisions](../Decisions/README.md) +- 生成日期:2026-06-22 + +本文档把 Lucky Dice 核心流程拆成可实现、可验收、可并行推进的功能包。它面向后续开发排期和任务拆分,不替代 PRD、需求或规格文档。 + +## 总目标 + +### 问题 + +FishDice 需要一套可扩展的普通骰子主循环和 Lucky Dice 特殊分支。第一版不能只写死几组骰子结果,也不能把每个组合绑定到独立业务方法,否则后续新增组合、骰子数量、特殊结果或目标玩法模式时会快速失控。 + +### 解决方案 + +把核心闭环拆成配置、普通 Roll、组合事实、规则匹配、行为执行、Lucky Dice 筛选随机、目标模式解析、最小表现和追踪测试几个功能包。每个功能包都有明确输入输出和验收口,优先保证玩法闭环、可配置、可测试和可排查。 + +### 第一版影响 + +- 玩家能完成普通 Roll,并看到普通奖励、Clover bonus 或 Lucky Dice 触发结果。 +- 策划能通过规则和候选配置表达基础组合和 Lucky Dice 入口。 +- 开发能在不写组合专用方法的前提下扩展新组合。 +- 测试能用固定输入和固定随机源验证完整链路。 + +## 用户角色 + +| 角色 | 关注点 | +| --- | --- | +| 玩家 | Roll 结果清晰,双 Clover 有惊喜,Lucky Dice 能看到目标玩法反馈 | +| 策划 | 骰子数量、组合规则、候选结果、权重和模式可配置 | +| 开发 | 模块边界清晰,新增组合不堆硬编码,随机和追踪可测试 | +| 测试 | 能覆盖组合事实、规则优先级、Lucky Dice 兜底和完整链路 | +| 产品负责人 | 第一版范围收敛,先验证闭环,不被复杂动画和完整短玩法拖散 | + +## 功能包总览 + +| 编号 | 功能包 | 优先级 | 目标 | 依赖 | +| --- | --- | --- | --- | --- | +| F0 | 本地配置与核心常量 | P0 | 提供第一版默认配置和 key 入口 | 无 | +| F1 | 普通 Roll 结果生成 | P0 | 根据 DiceSet 配置生成普通骰子结果 | F0 | +| F2 | 组合事实构建 | P0 | 把 Roll 结果转成可匹配事实 | F0, F1 | +| F3 | 组合规则匹配 | P0 | 按优先级命中普通组合规则 | F0, F2 | +| F4 | 行为执行与分发 | P0 | 按 action type 执行奖励、倍率和 Lucky Dice 触发 | F3 | +| F5 | Lucky Dice 候选筛选与权重随机 | P0 | 从特殊结果池选出目标结果 | F0, F4 | +| F6 | 目标玩法与模式解析 | P0 | 把 Lucky Dice 结果解析成目标玩法模式 | F5 | +| F7 | 最小表现与目标玩法占位入口 | P1 | 展示 Lucky Dice 标题、倍率、结果槽和跳转反馈 | F5, F6 | +| F8 | 追踪与调试信息 | P0 | 用 RollSessionId 串起关键决策 | F1-F6 | +| F9 | 测试套件 | P0 | 覆盖事实、匹配、筛选、随机和链路 | F1-F8 | +| F10 | 后续扩展预留 | P2 | 为更多组合、模式、动画和远端配置留口 | F0-F9 | + +P0 是第一版闭环必须完成的功能包。P1 是第一版玩家可见体验的最小表现。P2 是明确预留但不要求第一版完整实现的扩展。 + +## F0 本地配置与核心常量 + +### 目标 + +建立第一版默认配置和统一 key 入口,让后续模块不在业务逻辑里散落裸字符串和硬编码数量。 + +### 功能需求 + +- 提供普通骰子集合 `normal_main`。 +- 普通骰子面为 `2 / 3 / 4 / 5 / 6 / clover`。 +- 普通骰子数量来自 DiceSet 配置,第一版配置值为 2。 +- 提供 Lucky Dice 特殊结果集合 `lucky_dice`。 +- Lucky Dice 第一版结果至少包含 `rocket`、`thief`。 +- Lucky Dice 默认结果槽数量来自配置,第一版配置值为 3。 +- 提供普通组合规则配置。 +- 提供 Lucky Dice 候选结果配置。 +- 提供目标玩法模式映射配置。 + +### 非功能需求 + +- 配置结构不能绑定具体 Unity 场景对象。 +- key 应集中定义或集中加载,避免业务代码到处裸写。 +- 配置加载失败时需要返回结构化错误。 + +### 验收标准 + +- 能读取普通骰子集合、Lucky Dice 集合、组合规则、候选结果和模式映射。 +- 修改普通骰子数量配置后,核心流程读取到新数量。 +- 修改 Lucky Dice 结果槽数量配置后,展示槽构建读取到新数量。 +- 普通骰子集合和 Lucky Dice 特殊结果集合有明确类型区分。 + +### 不在范围内 + +- 远端配置热更新。 +- 策划可视化配置编辑器。 +- 完整配置校验工具链。 + +## F1 普通 Roll 结果生成 + +### 目标 + +根据普通 DiceSet 配置生成一次普通 Roll 结果,并携带同一个 `RollSessionId`。 + +### 功能需求 + +- 接收玩家 Roll 请求。 +- 从配置获取普通骰子集合和骰子数量。 +- 按配置生成 `DiceRollResult`。 +- `DiceRollResult` 包含 DiceSetId、DiceSetType、DiceCount、Faces、Multiplier、TriggerSource、RollSessionId。 +- 不决定奖励、组合规则或 Lucky Dice 入口。 + +### 非功能需求 + +- 支持注入随机源或可控 Roll 输入,方便测试。 +- 不写死两颗骰子位置。 +- 生成结果数量必须与配置 DiceCount 一致。 + +### 用户故事 + +作为玩家,我希望普通掷骰能产出清晰结果,从而知道每次 Roll 都有意义。 + +### 验收标准 + +- 给定 `DiceCount = 2`,Roll 结果包含 2 个普通骰子面。 +- 给定 `DiceCount = 3`,Roll 结果包含 3 个普通骰子面。 +- Roll 结果携带 `RollSessionId`。 +- Roll 层不调用奖励、规则匹配或 Lucky Dice 服务。 + +## F2 组合事实构建 + +### 目标 + +把普通 Roll 结果转换成结构化 `ComboFacts`,供规则匹配使用。 + +### 功能需求 + +- 从 `DiceRollResult` 构建 `ComboFacts`。 +- 统计原始面列表、面数量、数字列表、点数和、Clover 数量。 +- 生成无序 `NormalizedKey`。 +- 生成有序 `OrderedKey`。 +- `2 + clover` 与 `clover + 2` 归一为相同无序 key。 +- 拒绝或报错处理 Lucky Dice 特殊结果进入普通组合事实构建。 + +### 非功能需求 + +- 必须支持 N 颗骰子。 +- 不依赖固定左骰、右骰字段。 +- 输出字段应可用于日志和测试断言。 + +### 用户故事 + +作为开发者,我希望 Roll 结果先转成组合事实,从而规则可以基于数据匹配,而不是依赖固定位置判断。 + +### 验收标准 + +- `2 + 3` 生成 `NumberSum = 5`、`CloverCount = 0`、`NormalizedKey = 2_3`。 +- `2 + clover` 与 `clover + 2` 生成相同 `NormalizedKey`。 +- `clover + clover` 生成 `CloverCount = 2`。 +- `2 + 2 + clover` 能生成 3 骰事实。 +- Lucky Dice 特殊结果不会被当成普通组合事实处理。 + +## F3 组合规则匹配 + +### 目标 + +按规则优先级和通用 matcher 匹配普通组合,输出命中规则和 action 列表。 + +### 功能需求 + +- 支持规则优先级。 +- 第一版默认命中高优先级规则后停止继续匹配。 +- 支持首批 matcher: + - `ExactCombo` + - `AllNumbers` + - `ContainsFace` + - `FaceCount` + - `FaceCountRange` + - `DiceCount` + - `NumberPair` + - `NumberOfAKind` + - `NumberSumRange` +- 第一版默认规则: + - 双 Clover 触发 Lucky Dice。 + - 单 Clover 加数字发普通奖励和 Clover bonus。 + - 纯数字发普通奖励。 + +### 非功能需求 + +- matcher 只按 matcher type 注册,不按组合 key 注册。 +- 规则匹配不执行业务副作用。 +- 匹配结果需要包含命中规则 id、优先级、matcher type。 + +### 用户故事 + +作为策划,我希望组合规则支持优先级,从而让双 Clover 这类稀有触发优先于普通奖励规则。 + +### 验收标准 + +- `clover_clover` 命中 Lucky Dice 规则,而不是泛用 Clover 规则。 +- `2_clover` 命中 Clover bonus 规则。 +- `2_6` 命中纯数字普通奖励规则。 +- 高优先级规则优先于低优先级规则。 +- 3 骰事实可以通过统计型 matcher 命中规则。 + +## F4 行为执行与分发 + +### 目标 + +把命中规则中的 action list 按 `ActionType` 分发给通用行为执行器。 + +### 功能需求 + +- 支持 `grant_reward`。 +- 支持 `add_multiplier`。 +- 支持 `trigger_lucky_dice`。 +- 支持 `enter_mode` 扩展口。 +- 支持 `show_popup` 扩展口。 +- 多个 action 按规则配置顺序执行。 +- 未注册 action 返回结构化错误并写入追踪。 + +### 非功能需求 + +- dispatcher 只维护 `ActionType -> Executor` 注册表。 +- 不允许维护 `combo key -> method` 映射。 +- executor 不重新做组合匹配判断。 + +### 用户故事 + +作为开发者,我希望行为分发按行为类型注册,从而不需要维护巨大的“组合 key 到方法”映射表。 + +### 验收标准 + +- 纯数字规则执行普通奖励 action。 +- 单 Clover 规则执行普通奖励和倍率或 bonus action。 +- 双 Clover 规则执行 Lucky Dice 触发 action。 +- 新增组合但复用现有 action 时,不需要新增 executor。 +- 未注册 action 能被追踪到。 + +## F5 Lucky Dice 候选筛选与权重随机 + +### 目标 + +Lucky Dice 触发后,从独立特殊结果池中构建候选、筛选候选、处理兜底,并按权重随机选中一个结果。 + +### 功能需求 + +- 构建 Lucky Dice 候选池。 +- 候选包含 ResultKey、TargetKey、Weight、Enabled、MinLevel、SourceFilter、CooldownRule、IsDefault、ResultSlotCount。 +- 支持首批筛选器: + - EnabledFilter + - ProgressFilter + - SourceFilter + - CooldownFilter + - TutorialFilter +- 筛选后按权重随机。 +- 权重随机使用可注入随机源。 +- 候选池为空时优先使用 default result。 +- default result 不可用时降级为普通奖励兜底。 + +### 非功能需求 + +- Lucky Dice 特殊结果不与普通骰子集合混用。 +- 随机结果可通过固定随机源复现。 +- 筛选过程需要记录每个 filter 前后的候选数量。 + +### 用户故事 + +作为策划,我希望 Lucky Dice 候选结果可筛选,从而未解锁、未开启或冷却中的目标玩法不会被抽中。 + +### 验收标准 + +- 未开启候选会被过滤。 +- 玩家进度不足候选会被过滤。 +- 来源不匹配候选会被过滤。 +- 固定随机源下可以稳定选中预期结果。 +- 候选池为空时进入可观测兜底。 +- `rocket` 和 `thief` 可以作为第一版有效候选。 + +## F6 目标玩法与模式解析 + +### 目标 + +把 Lucky Dice 选中的结果解析为目标玩法和具体模式。 + +### 功能需求 + +- 支持 `ResultKey -> TargetKey -> ModeKey` 两级解析。 +- `rocket` 解析到 `slap_down_normal`。 +- `thief` 解析到 `treasure_heist_normal`。 +- 保留教程模式和 bonus 模式扩展规则。 +- 输出 `TargetModeEntry`,包含 TargetKey、ModeKey、ResultKey、Multiplier、TriggerSource、RollSessionId。 + +### 非功能需求 + +- 模式解析不参与 Lucky Dice 随机。 +- 目标玩法启动不反向影响候选筛选。 +- 模式解析失败必须可兜底、可追踪。 + +### 用户故事 + +作为玩家,我希望 Rocket 结果进入 Slap Down,从而让结果和后续玩法有明确绑定。 + +### 验收标准 + +- `rocket` 解析为 `TargetKey = slap_down`、`ModeKey = slap_down_normal`。 +- `thief` 解析为 `TargetKey = treasure_heist`、`ModeKey = treasure_heist_normal`。 +- 模式解析结果携带原始 `RollSessionId`。 +- 模式映射失败时进入可观测兜底。 + +## F7 最小表现与目标玩法占位入口 + +### 目标 + +提供第一版玩家可见反馈:Lucky Dice 标题、倍率、结果槽图标和目标玩法跳转反馈。 + +### 功能需求 + +- 展示 Lucky Dice 标题。 +- 展示当前倍率。 +- 按结果槽数量展示特殊图标。 +- 第一版结果槽可全部展示同一个 ResultKey。 +- 触发目标玩法入口反馈。 +- 目标玩法结束后能回到主循环。 +- Slap Down 和 Treasure Heist 第一版可以是占位入口,不要求完整玩法内容。 + +### 非功能需求 + +- 表现层不参与规则匹配、候选筛选和权重随机。 +- 动画失败不能改变核心选择结果。 +- 结果槽数量来自配置或候选,不写死在 UI 中。 + +### 用户故事 + +作为玩家,我希望 Lucky Dice 结果能清楚显示特殊图标,从而知道即将进入哪个短玩法。 + +### 验收标准 + +- Lucky Dice 触发后能看到标题、倍率和结果槽。 +- 选中 `rocket` 时展示 Rocket 结果并进入 Slap Down 占位入口。 +- 选中 `thief` 时展示 Thief 结果并进入 Treasure Heist 占位入口。 +- 结果槽数量修改为 2 或 4 时,表现层能按配置展示。 + +### 不在范围内 + +- 托盘动画。 +- 骰子飞入。 +- 粒子爆发。 +- 白烟转场。 +- 完整 Slap Down 和 Treasure Heist 玩法。 + +## F8 追踪与调试信息 + +### 目标 + +让一次 Roll 从普通结果到最终玩法模式的关键决策可解释、可回放、可排查。 + +### 功能需求 + +- 每次 Roll 创建或携带同一个 `RollSessionId`。 +- 记录普通骰子结果和骰子数量。 +- 记录组合事实和 `NormalizedKey`。 +- 记录命中规则、优先级和执行 action。 +- 记录 Lucky Dice 初始候选和筛选结果。 +- 记录兜底类型。 +- 记录权重随机输入和选中结果。 +- 记录最终 TargetKey、ModeKey 和 FinalOutcome。 + +### 非功能需求 + +- 追踪层不改变玩法决策。 +- 第一版可以使用本地结构化日志或调试数据结构。 +- 字段命名应稳定,方便后续接入埋点。 + +### 用户故事 + +作为开发者,我希望每次 Roll 都携带同一个会话 id,从而普通 Roll、规则匹配、Lucky Dice 结果和目标玩法启动可以串起来排查。 + +### 验收标准 + +- 普通奖励链路可以从 trace 解释为什么发奖。 +- 双 Clover 链路可以从 trace 解释为什么进入 Lucky Dice。 +- Lucky Dice 链路可以从 trace 看到哪些候选被过滤。 +- 模式解析失败或候选池为空时,trace 中有兜底原因。 + +## F9 测试套件 + +### 目标 + +用测试锁住第一版核心闭环,避免后续扩展组合或结果时破坏基础行为。 + +### 功能需求 + +- 组合事实测试。 +- 规则匹配测试。 +- 行为分发测试。 +- Lucky Dice 筛选测试。 +- 权重随机测试。 +- 目标模式解析测试。 +- 完整链路测试。 +- 兜底场景测试。 + +### 非功能需求 + +- 测试外部行为,不测试私有实现细节。 +- 使用固定随机源。 +- 关键链路测试应覆盖 `RollSessionId`。 + +### 验收标准 + +- `2 + 3` 最终发普通奖励。 +- `2 + clover` 最终发普通奖励并提供 Clover bonus。 +- `clover + clover` 最终进入 Lucky Dice。 +- Lucky Dice 选中 `rocket` 后进入 `slap_down_normal`。 +- Lucky Dice 选中 `thief` 后进入 `treasure_heist_normal`。 +- 候选池为空时走兜底。 +- 普通骰子从 2 改为 3 后,组合事实和 matcher 仍工作。 + +## F10 后续扩展预留 + +### 目标 + +明确第一版不做但架构需要保留的扩展方向,防止第一版把未来道路堵死。 + +### 扩展方向 + +- 更多普通组合,例如对子、三连、点数和区间、指定数字组合、有序组合。 +- 更多 Lucky Dice 特殊结果,例如 Chest、Bomb、Key。 +- 教程模式和 bonus 模式。 +- 按活动或关卡调整权重。 +- 远端配置热更新。 +- 完整表现动画。 +- 完整 Slap Down 和 Treasure Heist 玩法内容。 +- 埋点管线集成。 + +### 约束 + +- 扩展普通组合优先新增配置,不新增组合专用方法。 +- 扩展特殊结果优先新增候选和模式映射。 +- 扩展动画不能改变核心随机和模式解析结果。 +- 接入埋点时复用 `RollSessionId` 和现有 trace 字段。 + +## 依赖顺序 + +```mermaid +flowchart TD + F0["F0 本地配置与核心常量"] --> F1["F1 普通 Roll 结果生成"] + F1 --> F2["F2 组合事实构建"] + F2 --> F3["F3 组合规则匹配"] + F3 --> F4["F4 行为执行与分发"] + F4 --> F5["F5 Lucky Dice 筛选与随机"] + F5 --> F6["F6 目标玩法与模式解析"] + F5 --> F7["F7 最小表现"] + F6 --> F7 + F1 --> F8["F8 追踪"] + F3 --> F8 + F5 --> F8 + F6 --> F8 + F1 --> F9["F9 测试"] + F2 --> F9 + F3 --> F9 + F4 --> F9 + F5 --> F9 + F6 --> F9 +``` + +## 建议迭代切片 + +### Slice 1:普通 Roll 到普通奖励 + +包含 F0、F1、F2、F3、F4 的普通奖励部分、F8 基础字段、F9 基础测试。 + +验收: + +- 纯数字结果发普通奖励。 +- 单 Clover 加数字发普通奖励和 bonus。 +- 组合事实和规则命中可追踪。 + +### Slice 2:双 Clover 触发 Lucky Dice + +包含 F4 的 `trigger_lucky_dice`、F5、F6、F8 Lucky Dice 字段、F9 Lucky Dice 测试。 + +验收: + +- 双 Clover 不发普通奖励,而是进入 Lucky Dice。 +- `rocket` 能解析到 Slap Down 普通模式。 +- `thief` 能解析到 Treasure Heist 普通模式。 +- 候选池为空有兜底。 + +### Slice 3:最小玩家可见闭环 + +包含 F7 和链路测试补齐。 + +验收: + +- Lucky Dice 标题、倍率、结果槽可见。 +- 目标玩法占位入口可达。 +- 目标玩法结束后能回到主循环。 + +### Slice 4:扩展性验证 + +包含 3 骰配置测试、结果槽数量变化测试、一个统计型 matcher 示例。 + +验收: + +- 普通骰子数量改为 3 后不改核心流程。 +- 结果槽数量改为 2 或 4 后表现层按配置展示。 +- 新增一个组合规则时复用已有 matcher 和 action。 + +## 总体验收标准 + +- 普通骰子结果能转换为结构化组合事实。 +- 普通骰子数量和 Lucky Dice 结果槽数量来自配置。 +- 规则系统通过少量通用 matcher 命中普通组合。 +- 双 Clover 优先触发 Lucky Dice。 +- 行为执行层只按 action type 分发。 +- Lucky Dice 先筛选候选,再按权重随机。 +- `rocket` 进入 `slap_down_normal`。 +- `thief` 进入 `treasure_heist_normal`。 +- 候选池为空、模式解析失败、未注册 action 都有可观测兜底。 +- `RollSessionId` 能串起完整链路。 + +## 第一版不做 + +- 完整复刻参考 Lucky Dice 动画。 +- 托盘、飞入、粒子爆发、白烟转场。 +- 完整 Slap Down 玩法。 +- 完整 Treasure Heist 玩法。 +- 所有 Lucky Dice 特殊结果类型。 +- 远端配置热更新。 +- 复杂运营活动概率策略。 +- 正式埋点管线集成。 +- 为每个组合 key 编写独立业务方法。 +- 假设普通骰子永远只有 2 颗。 +- 假设 Lucky Dice 永远只有 3 个结果槽。 diff --git a/FishDice/Docs/Requirements/lucky-dice-core-flow-requirements.md b/FishDice/Docs/Requirements/lucky-dice-core-flow-requirements.md new file mode 100644 index 0000000..3722daf --- /dev/null +++ b/FishDice/Docs/Requirements/lucky-dice-core-flow-requirements.md @@ -0,0 +1,354 @@ +# Lucky Dice 核心流程需求 + +## 目标 + +本文档定义 FishDice 中普通骰子流程与 Lucky Dice 特殊分支的最小核心逻辑。当前阶段忽略复杂动画表现,只保留玩法闭环、规则扩展口、随机筛选口和目标玩法模式解析口。 + +核心目标: + +- 普通骰子流程可以通过不同组合触发不同业务行为。 +- 双四叶草组合可以触发 Lucky Dice 特殊流程。 +- Lucky Dice 内部使用独立的特殊骰子结果池,不与普通数字骰混用。 +- 骰子数量和结果槽数量需要可配置,第一版可以使用 2 颗普通骰和 3 个 Lucky Dice 结果槽,但规则系统不能写死这个数量。 +- Lucky Dice 结果进入目标玩法前,需要保留筛选、随机和模式解析扩展点。 +- 执行层避免维护巨大的“组合 key 到方法”的硬编码映射。 + +## 概念边界 + +### 普通骰子 + +普通流程使用一套主循环骰子面: + +```text +2 / 3 / 4 / 5 / 6 / Clover +``` + +其中 `1` 被 `Clover` 替代。普通骰子负责主循环奖励、倍率、特殊触发等常规行为。 + +普通骰子的数量需要由骰子集合配置决定。第一版默认使用 2 颗普通骰,但系统需要支持后续扩展到 3 颗、4 颗或特殊关卡自定义数量。 + +最小组合语义: + +- 数字 + 数字:普通奖励。 +- 数字 + Clover:普通奖励加小额 bonus 或倍率反馈。 +- Clover + Clover:触发 Lucky Dice。 + +后续可扩展更多组合含义,例如对子、三连、指定数量命中、点数和区间、指定数字组合、顺序组合等。 + +### Lucky Dice 特殊骰子 + +Lucky Dice 进入后使用另一套特殊结果骰子,不再使用普通数字骰。 + +示例特殊结果: + +```text +Rocket / Thief / Chest / Bomb / Key +``` + +这套结果的职责是决定后续玩法入口,而不是做普通点数结算。 + +Lucky Dice 的结果槽数量也需要可配置。竞品拆解中表现为 3 格相同特殊结果,第一版可以固定为 3;但数据结构和播放流程需要允许后续扩展为 2 格、4 格或按玩法目标定义槽数量。 + +最小语义: + +- Rocket:进入 Slap Down。 +- Thief:进入 Treasure Heist。 + +## 总体流程 + +```text +玩家触发普通 Roll +→ 产出普通骰子结果 +→ 提取组合事实 ComboFacts +→ 按优先级匹配普通组合规则 +→ 执行规则配置的行为列表 + → 普通奖励:发放奖励并结束 + → Lucky Dice:进入 Lucky Dice 流程 + +Lucky Dice 流程 +→ 构建特殊结果候选池 +→ 按上下文筛选候选结果 +→ 按权重随机一个结果 +→ 按结果槽数量显示相同或配置指定的特殊结果 +→ 解析目标玩法与进入模式 +→ 启动目标玩法 +→ 目标玩法结束后回到主循环 +``` + +## 普通组合规则需求 + +### 组合 key + +组合规则允许使用字符串 key 作为数据 ID,例如: + +```text +clover_clover +number_number +number_clover +``` + +使用字符串 key 的目的: + +- 便于配置表、JSON、远端配置和日志使用。 +- 新增组合时不一定需要新增枚举或改代码。 +- 规则识别与执行逻辑可以解耦。 + +约束: + +- 不允许在业务代码中到处裸写字符串。 +- 组合 key 只作为规则索引或配置 ID。 +- 原始骰子结果、数量统计、是否有序等信息必须保留在结构化数据中。 + +### 组合事实 + +普通 Roll 产出后,需要生成组合事实,供规则匹配器使用。 + +至少包含: + +```text +DiceSetType 骰子集合类型,普通骰子或 Lucky Dice +DiceCount 本次 Roll 实际骰子数量 +Faces 原始骰子面列表 +FaceCounts 每个骰子面的数量 +NumberSum 数字骰点数和 +NumberValues 本次出现的数字点数列表 +CloverCount 四叶草数量 +HasClover 是否包含四叶草 +NormalizedKey 无序归一化 key +OrderedKey 有序 key +``` + +组合事实不能假设只有 2 颗骰子。`Faces` 和 `FaceCounts` 必须支持任意数量,规则匹配器应基于数量统计和条件表达,而不是只读取左骰、右骰两个固定位置。 + +普通组合默认按无序处理,例如: + +```text +clover + 2 +2 + clover +``` + +都归一为: + +```text +2_clover +``` + +如果后续需要区分左骰、右骰或先后顺序,可以在规则中显式声明使用有序 key,例如: + +```text +clover_then_rocket +rocket_then_clover +``` + +### 规则匹配 + +组合系统不应为每个组合写独立方法,而应使用少量通用匹配器。 + +首批匹配器需求: + +- ExactCombo:精确匹配组合 key,例如 `clover_clover`。 +- AllNumbers:全部为数字骰。 +- ContainsFace:包含指定面,例如包含 1 个 Clover。 +- FaceCount:指定面数量达到要求。 +- FaceCountRange:指定面数量落在区间内,例如至少 2 个 Clover。 +- DiceCount:本次骰子数量满足要求,例如只匹配 2 骰或 3 骰规则。 +- NumberPair:数字对子。 +- NumberOfAKind:N 个相同数字,例如三连、四连。 +- NumberSumRange:点数和落在区间内。 + +规则需要支持优先级,优先级高的规则先匹配。 + +最小规则配置: + +```text +优先级 100:clover_clover → trigger_lucky_dice +优先级 50:contains Clover count 1 → grant_reward + clover_bonus +优先级 10:all_numbers → grant_reward +``` + +当后续加入更多骰子数量时,应优先使用统计型规则,例如: + +```text +Clover count >= 2 → trigger_lucky_dice +NumberOfAKind count 3 → grant_reward + combo_bonus +DiceCount 4 + NumberSumRange 18-24 → grant_reward + high_sum_bonus +``` + +## 行为执行需求 + +执行层只注册少量通用行为执行器,不维护“所有组合 key 到方法”的巨大 map。 + +规则配置只描述要执行哪些行为,行为执行器按行为类型处理。 + +首批行为类型: + +- grant_reward:发放普通奖励。 +- add_multiplier:调整倍率或临时倍率。 +- trigger_lucky_dice:进入 Lucky Dice。 +- enter_mode:进入指定玩法模式。 +- show_popup:显示提示或轻量弹窗。 + +执行器注册表只随行为类型增长,不随组合数量增长。 + +示例: + +```text +grant_reward → GrantRewardExecutor +trigger_lucky_dice → TriggerLuckyDiceExecutor +enter_mode → EnterModeExecutor +``` + +组合扩展优先通过新增规则配置完成;只有出现新的通用行为类型时,才新增执行器。 + +## Lucky Dice 筛选与随机需求 + +Lucky Dice 结果不能直接从全部特殊结果中随机,必须经过候选池构建、筛选和权重随机。 + +### 候选结果 + +候选结果至少包含: + +```text +ResultKey 特殊结果 ID,例如 rocket / thief +TargetKey 目标玩法 ID,例如 slap_down / treasure_heist +Weight 随机权重 +Enabled 是否启用 +MinLevel 最低等级或进度要求 +SourceFilter 允许的触发来源 +CooldownRule 冷却或次数限制 +``` + +### 筛选器 + +首批筛选器需求: + +- EnabledFilter:过滤未开启结果。 +- ProgressFilter:过滤玩家进度不满足的结果。 +- SourceFilter:过滤当前触发来源不允许的结果。 +- CooldownFilter:过滤处于冷却或次数已满的玩法。 +- TutorialFilter:新手期可强制或限制候选结果。 + +筛选器应可组合,筛选后如果候选池为空,需要有兜底策略。 + +兜底策略: + +- 优先使用配置的 default result。 +- 如果 default result 不可用,走普通奖励兜底。 +- 兜底发生时需要打日志,方便排查配置问题。 + +### 随机选择 + +筛选后的候选结果按权重随机。 + +随机需求: + +- 支持普通权重随机。 +- 支持按上下文调整权重,例如活动期间提高某结果权重。 +- 支持新手期固定结果或半随机结果。 +- 随机结果需要可记录,方便回放、埋点和问题排查。 + +## 目标玩法与模式解析 + +Lucky Dice 的 `ResultKey` 不直接等于最终进入模式。需要分为两层: + +```text +ResultKey → TargetKey → ModeKey +``` + +含义: + +- ResultKey:Lucky Dice 抽中的特殊结果,例如 `rocket`。 +- TargetKey:目标玩法,例如 `slap_down`。 +- ModeKey:目标玩法的具体进入模式,例如 `slap_down_normal`。 + +示例: + +```text +rocket → slap_down +slap_down + 新手期 → slap_down_tutorial +slap_down + 普通状态 → slap_down_normal +slap_down + 高倍率 → slap_down_bonus +``` + +首批模式解析规则: + +- TutorialModeRule:新手期进入教程模式。 +- BonusModeRule:高倍率或特殊上下文进入 bonus 模式。 +- DefaultModeRule:默认进入普通模式。 + +目标玩法启动时,需要携带: + +```text +TargetKey +ModeKey +ResultKey +Multiplier +TriggerSource +RollSessionId +``` + +## 最小可交付范围 + +第一版只需要实现以下玩法闭环: + +### 普通骰子 + +```text +骰子面:2 / 3 / 4 / 5 / 6 / Clover +默认骰子数量:2 +骰子数量来源:DiceSet 配置 +``` + +组合行为: + +```text +数字 + 数字 → 普通奖励 +数字 + Clover → 普通奖励 + Clover bonus +Clover + Clover → 触发 Lucky Dice +``` + +### Lucky Dice + +特殊结果: + +```text +Rocket → SlapDownNormal +Thief → TreasureHeistNormal +默认结果槽数量:3 +结果槽数量来源:Lucky Dice 结果或玩法配置 +``` + +表现最小化: + +```text +显示 Lucky Dice 标题 +→ 显示倍率 +→ 按结果槽数量显示特殊图标 +→ 进入目标玩法 +``` + +复杂动画如托盘、骰子飞入、粒子爆发、白烟转场等不进入第一版核心逻辑要求。 + +## 非目标 + +第一版不要求: + +- 完整复刻竞品动画时长和粒子效果。 +- 实现所有特殊结果类型。 +- 实现复杂运营活动权重策略。 +- 实现完整短玩法内容。 +- 实现远端配置热更新。 + +## 验收标准 + +- 普通骰子结果能被转换为结构化组合事实。 +- 普通骰子数量和 Lucky Dice 结果槽数量来自配置,核心规则不写死为 2 骰或 3 格。 +- 规则系统能通过少量通用匹配器命中普通组合。 +- 双四叶草能触发 Lucky Dice,而不是直接发普通奖励。 +- 行为执行层只依赖少量通用执行器,不存在按组合数量增长的大型方法 map。 +- Lucky Dice 能先筛选候选结果,再按权重随机。 +- Lucky Dice 结果能解析到目标玩法和具体模式。 +- Rocket 能进入 Slap Down 普通模式。 +- Thief 能进入 Treasure Heist 普通模式。 +- 候选池为空时存在可观测兜底。 +- 日志或调试信息能追踪一次 Roll 从普通结果到最终行为的关键决策。 diff --git a/FishDice/Docs/Specs/lucky-dice-core-flow-spec.md b/FishDice/Docs/Specs/lucky-dice-core-flow-spec.md new file mode 100644 index 0000000..4cbb988 --- /dev/null +++ b/FishDice/Docs/Specs/lucky-dice-core-flow-spec.md @@ -0,0 +1,765 @@ +# Lucky Dice 核心流程规格 + +## 文档信息 + +- 来源需求:[lucky-dice-core-flow-requirements.md](../Requirements/lucky-dice-core-flow-requirements.md) +- 适用范围:FishDice 第一版普通骰子主循环与 Lucky Dice 特殊分支 +- 文档目标:把玩法需求拆成可实现、可测试、可扩展的系统规格 + +## 设计目标 + +Lucky Dice 核心流程需要支持两层玩法: + +1. 普通骰子主循环:玩家按骰子集合配置 Roll 普通骰子,根据组合事实匹配规则并执行奖励或特殊触发行为。 +2. Lucky Dice 特殊流程:当普通骰子出现双四叶草或配置声明的触发条件时,进入独立的特殊结果池,经过筛选、权重随机和模式解析后启动目标玩法。 + +第一版只做核心闭环,不实现复杂动画、远端热更、完整短玩法内容和复杂运营权重策略。系统边界必须保留扩展点,避免后续新增组合或特殊结果时不断堆硬编码分支。 + +第一版默认值: + +- 普通骰子数量默认 2 颗,但必须来自 DiceSet 配置。 +- Lucky Dice 结果槽数量默认 3 格,但必须来自 Lucky Dice 结果或目标玩法配置。 +- 规则系统、组合事实和展示流程不得把 2 颗骰子或 3 格结果槽写死为不可扩展假设。 + +## 总体架构 + +核心模块按职责拆分为以下几层: + +```text +Roll 输入层 +→ 骰子结果生成层 +→ 组合事实提取层 +→ 组合规则匹配层 +→ 行为执行层 +→ Lucky Dice 流程层 +→ 目标玩法模式解析层 +→ 目标玩法启动层 +``` + +各层只依赖前一层产出的结构化数据,不直接读取 UI 状态或动画状态。 + +### 模块职责 + +| 模块 | 职责 | 不负责 | +| --- | --- | --- | +| DiceSetConfigProvider | 提供骰子集合、骰子数量、结果槽数量等配置 | 执行 Roll 或业务行为 | +| DiceRollService | 按骰子集合配置生成普通骰子或 Lucky Dice 结果 | 决定奖励、玩法入口 | +| ComboFactBuilder | 把普通 Roll 结果转换为结构化组合事实 | 执行业务行为 | +| ComboRuleMatcher | 按优先级匹配组合规则 | 直接发奖或进玩法 | +| RollActionDispatcher | 按行为类型分发到通用执行器 | 按组合 key 维护巨大方法 map | +| LuckyDiceFlowService | 构建候选池、筛选、随机、生成可展示结果槽 | 直接决定目标模式细节 | +| TargetModeResolver | 把 ResultKey/TargetKey 解析为 ModeKey | 执行目标玩法内容 | +| TargetModeLauncher | 统一启动目标玩法 | 参与 Lucky Dice 随机 | +| RollTraceLogger | 记录一次 Roll 的关键决策链路 | 影响玩法决策 | + +## 核心流程 + +### 普通 Roll 流程 + +```text +PlayerRollRequest +→ DiceSetConfigProvider.GetNormalDiceSet(request) +→ DiceRollService.Roll(NormalDiceSet) +→ ComboFactBuilder.Build(result) +→ ComboRuleMatcher.Match(facts, ruleSet) +→ RollActionDispatcher.Execute(matchedRule.Actions) + → grant_reward:发放普通奖励并结束 + → grant_reward + add_multiplier:发奖并应用 Clover bonus + → trigger_lucky_dice:进入 LuckyDiceFlowService +``` + +普通 Roll 第一版默认两颗骰子,骰子面为: + +```text +2 / 3 / 4 / 5 / 6 / Clover +``` + +骰子数量必须从 `DiceSetConfig.DiceCount` 读取。第一版配置值可以是 2,但 `DiceRollService`、`ComboFactBuilder` 和 matcher 都必须按 `Faces.Count` 或 `DiceCount` 处理,不读取固定的左骰、右骰两个位置。 + +### Lucky Dice 流程 + +```text +TriggerLuckyDiceAction +→ LuckyDiceFlowService.BuildCandidatePool(context) +→ LuckyDiceFlowService.ApplyFilters(pool, context) +→ LuckyDiceFlowService.ResolveEmptyPoolFallback(filteredPool, context) +→ LuckyDiceFlowService.WeightedPick(filteredPool, context) +→ LuckyDiceFlowService.BuildResultSlots(result, slotConfig) +→ LuckyDicePresentation.ShowResultSlots(resultSlots) +→ TargetModeResolver.Resolve(result, context) +→ TargetModeLauncher.Launch(targetEntry) +→ TargetModeCompletion 回到主循环 +``` + +第一版 Lucky Dice 默认展示 3 格相同特殊图标,不要求实现托盘、飞入、粒子爆发、白烟转场等表现。结果槽数量必须从 Lucky Dice 结果或目标玩法配置读取,后续可扩展为 2 格、4 格或按玩法定义不同槽数。 + +## 数据模型 + +### DiceSetConfig + +骰子集合配置定义一次 Roll 使用哪套骰子、投几颗骰子,以及该集合是否拥有展示槽概念。 + +```csharp +public sealed class DiceSetConfig +{ + public string DiceSetId { get; init; } + public DiceSetType SetType { get; init; } + public int DiceCount { get; init; } + public IReadOnlyList Faces { get; init; } + public int? DefaultResultSlotCount { get; init; } +} +``` + +约束: + +- `DiceCount` 必须大于 0。 +- 第一版普通骰子 `DiceCount = 2`,但核心逻辑不得依赖该常量。 +- Lucky Dice 如果不真实 Roll 多颗特殊骰,可以将 `DiceCount` 理解为结果选择次数;第一版只随机一次,再由展示槽配置生成多个相同槽。 +- `DefaultResultSlotCount` 用于展示层默认槽数,第一版 Lucky Dice 为 3。 + +### DiceFace + +普通骰子面与 Lucky Dice 特殊结果必须区分集合类型,不能混用同一个裸字符串列表。 + +```csharp +public enum DiceSetType +{ + Normal, + Lucky +} + +public readonly struct DiceFace +{ + public DiceSetType SetType { get; } + public string Key { get; } + public int? NumberValue { get; } +} +``` + +约束: + +- 普通数字面使用 `Key = "2" ... "6"`,`NumberValue` 为对应数字。 +- 普通四叶草使用 `Key = "clover"`,`NumberValue = null`。 +- Lucky Dice 特殊结果使用 `DiceSetType.Lucky`,例如 `rocket`、`thief`。 +- 业务代码不得到处裸写这些 key,必须集中在定义表或常量入口。 + +### DiceRollResult + +```csharp +public sealed class DiceRollResult +{ + public string RollSessionId { get; init; } + public string DiceSetId { get; init; } + public DiceSetType SetType { get; init; } + public int DiceCount { get; init; } + public IReadOnlyList Faces { get; init; } + public int Multiplier { get; init; } + public string TriggerSource { get; init; } +} +``` + +约束: + +- `DiceCount` 必须等于本次配置要求的投掷数量。 +- `Faces.Count` 必须等于本次实际产出的骰子面数量。 +- 普通 Roll 中 `DiceCount` 和 `Faces.Count` 不一致时,应拒绝进入规则匹配并记录错误。 + +### ComboFacts + +`ComboFacts` 是普通组合匹配的唯一输入。 + +```csharp +public sealed class ComboFacts +{ + public DiceSetType DiceSetType { get; init; } + public int DiceCount { get; init; } + public IReadOnlyList Faces { get; init; } + public IReadOnlyDictionary FaceCounts { get; init; } + public int NumberSum { get; init; } + public IReadOnlyList NumberValues { get; init; } + public int CloverCount { get; init; } + public bool HasClover { get; init; } + public string NormalizedKey { get; init; } + public string OrderedKey { get; init; } +} +``` + +归一化规则: + +- 默认按无序组合处理。 +- `2 + clover` 与 `clover + 2` 都归一为 `2_clover`。 +- `clover + clover` 归一为 `clover_clover`。 +- N 颗骰子的 `NormalizedKey` 由所有面 key 排序后拼接,例如 `2_2_clover`、`3_3_3`。 +- 后续若需要有序语义,由规则显式声明使用 `OrderedKey`。 + +约束: + +- `ComboFacts` 不能假设只有 2 颗骰子。 +- matcher 必须基于 `DiceCount`、`Faces`、`FaceCounts`、`NumberValues` 等统计信息判断。 +- 只有有序规则才能读取 `OrderedKey`,普通规则默认使用无序事实。 + +### ComboRule + +```csharp +public sealed class ComboRule +{ + public string RuleId { get; init; } + public int Priority { get; init; } + public ComboMatcherSpec Matcher { get; init; } + public IReadOnlyList Actions { get; init; } + public bool StopAfterMatched { get; init; } = true; +} +``` + +第一版规则: + +| Priority | Matcher | Actions | +| --- | --- | --- | +| 100 | ExactCombo `clover_clover` | `trigger_lucky_dice` | +| 50 | ContainsFace `clover` count 1 | `grant_reward`, `add_multiplier` | +| 10 | AllNumbers | `grant_reward` | + +匹配策略: + +- 按 `Priority` 从高到低匹配。 +- 同优先级按配置顺序匹配。 +- 第一版默认命中第一条 `StopAfterMatched = true` 的规则后停止。 +- 未命中任何规则时走可观测兜底,默认发普通奖励或返回明确错误,由产品配置决定。 + +### ComboMatcherSpec + +```csharp +public sealed class ComboMatcherSpec +{ + public string MatcherType { get; init; } + public string ComboKey { get; init; } + public string FaceKey { get; init; } + public int? RequiredCount { get; init; } + public int? MinCount { get; init; } + public int? MaxCount { get; init; } + public int? DiceCount { get; init; } + public int? OfAKindCount { get; init; } + public int? MinSum { get; init; } + public int? MaxSum { get; init; } + public bool UseOrderedKey { get; init; } +} +``` + +首批 matcher: + +- `ExactCombo` +- `AllNumbers` +- `ContainsFace` +- `FaceCount` +- `FaceCountRange` +- `DiceCount` +- `NumberPair` +- `NumberOfAKind` +- `NumberSumRange` + +新增组合语义优先新增规则配置;只有出现新的通用匹配能力时才新增 matcher。 + +N 颗骰子扩展示例: + +| Matcher | 例子 | 含义 | +| --- | --- | --- | +| `FaceCountRange` | `faceKey = clover`, `minCount = 2` | 至少 2 个 Clover | +| `DiceCount` | `diceCount = 3` | 只匹配 3 骰规则 | +| `NumberOfAKind` | `ofAKindCount = 3` | 任意数字三连 | +| `NumberSumRange` | `minSum = 18`, `maxSum = 24` | 点数和落在 18 到 24 | + +### RollActionSpec + +```csharp +public sealed class RollActionSpec +{ + public string ActionType { get; init; } + public IReadOnlyDictionary Params { get; init; } +} +``` + +首批 action: + +- `grant_reward` +- `add_multiplier` +- `trigger_lucky_dice` +- `enter_mode` +- `show_popup` + +执行层只按 `ActionType` 找执行器,不按组合 key 找方法。 + +### LuckyDiceCandidate + +```csharp +public sealed class LuckyDiceCandidate +{ + public string ResultKey { get; init; } + public string TargetKey { get; init; } + public int? ResultSlotCount { get; init; } + public int Weight { get; init; } + public bool Enabled { get; init; } + public int MinLevel { get; init; } + public IReadOnlySet SourceFilter { get; init; } + public CooldownRule CooldownRule { get; init; } + public bool IsDefault { get; init; } +} +``` + +第一版候选配置: + +| ResultKey | TargetKey | ResultSlotCount | Weight | Enabled | Mode 默认结果 | +| --- | --- | --- | --- | --- | --- | +| `rocket` | `slap_down` | 3 | 1 | true | `slap_down_normal` | +| `thief` | `treasure_heist` | 3 | 1 | true | `treasure_heist_normal` | + +`ResultSlotCount` 允许为空。为空时使用 Lucky Dice 集合配置或目标玩法配置中的默认槽数。 + +### LuckyDiceResultSlots + +Lucky Dice 随机只选中一个特殊结果;展示层按槽配置生成实际槽位。 + +```csharp +public sealed class LuckyDiceResultSlots +{ + public string ResultKey { get; init; } + public string TargetKey { get; init; } + public int SlotCount { get; init; } + public IReadOnlyList SlotResultKeys { get; init; } +} +``` + +第一版要求: + +- `SlotCount` 默认 3。 +- `SlotResultKeys` 第一版全部填入同一个 `ResultKey`。 +- 后续如某玩法需要不同槽位结果,可通过配置扩展 `SlotResultKeys` 的生成策略。 + +### LuckyDiceContext + +```csharp +public sealed class LuckyDiceContext +{ + public string RollSessionId { get; init; } + public string TriggerSource { get; init; } + public int NormalDiceCount { get; init; } + public int LuckyResultSlotCount { get; init; } + public int PlayerLevel { get; init; } + public int Multiplier { get; init; } + public bool IsTutorial { get; init; } + public IReadOnlyDictionary RuntimeTags { get; init; } +} +``` + +### TargetModeEntry + +```csharp +public sealed class TargetModeEntry +{ + public string TargetKey { get; init; } + public string ModeKey { get; init; } + public string ResultKey { get; init; } + public int Multiplier { get; init; } + public string TriggerSource { get; init; } + public string RollSessionId { get; init; } +} +``` + +## 接口边界 + +### 组合事实构建 + +```csharp +public interface IComboFactBuilder +{ + ComboFacts Build(DiceRollResult result); +} +``` + +要求: + +- 只接受普通骰子结果构建普通组合事实。 +- 如果传入 Lucky Dice 结果,应返回错误或拒绝处理,避免两套骰子语义混用。 +- 输出必须保留 DiceCount、原始 Faces、FaceCounts、NumberValues、NormalizedKey、OrderedKey。 +- 不允许只读取固定两个骰子位置;必须遍历 `Faces`。 + +### 组合规则匹配 + +```csharp +public interface IComboRuleMatcher +{ + ComboRuleMatchResult Match(ComboFacts facts, IReadOnlyList rules); +} +``` + +要求: + +- matcher 由 `MatcherType` 注册,不由组合 key 注册。 +- 匹配结果需要包含命中的 `RuleId`、`Priority`、`MatcherType` 和关键输入 facts,供日志追踪。 + +### 行为执行 + +```csharp +public interface IRollActionExecutor +{ + string ActionType { get; } + RollActionResult Execute(RollActionContext context, RollActionSpec action); +} +``` + +要求: + +- `RollActionDispatcher` 只维护 `ActionType -> IRollActionExecutor` 注册表。 +- 不允许维护 `clover_clover -> SomeMethod` 这类随组合数量增长的 map。 +- 多个 action 按规则配置顺序执行。 +- action 失败时需要返回结构化失败结果,并进入统一日志。 + +### Lucky Dice 筛选 + +```csharp +public interface ILuckyDiceCandidateFilter +{ + string FilterType { get; } + IReadOnlyList Apply( + IReadOnlyList candidates, + LuckyDiceContext context); +} +``` + +首批 filter: + +- `EnabledFilter` +- `ProgressFilter` +- `SourceFilter` +- `CooldownFilter` +- `TutorialFilter` + +筛选器按固定顺序执行,顺序建议为: + +```text +Enabled → Progress → Source → Cooldown → Tutorial +``` + +### 权重随机 + +```csharp +public interface IWeightedPicker +{ + LuckyDiceCandidate Pick( + IReadOnlyList candidates, + LuckyDiceContext context, + IRandomSource random); +} +``` + +要求: + +- 权重小于等于 0 的候选不参与普通随机。 +- 随机源可注入,便于测试和回放。 +- 选中结果必须记录候选池快照、权重、随机值和最终 ResultKey。 + +### Lucky Dice 结果槽构建 + +```csharp +public interface ILuckyDiceResultSlotBuilder +{ + LuckyDiceResultSlots BuildSlots( + LuckyDiceCandidate selected, + LuckyDiceContext context, + DiceSetConfig luckyDiceSetConfig); +} +``` + +要求: + +- 槽数优先级:候选结果配置 `ResultSlotCount` → 目标玩法配置 → Lucky Dice 集合配置 `DefaultResultSlotCount`。 +- 第一版每个槽都展示同一个 `ResultKey`。 +- 槽数小于等于 0 时返回结构化错误并进入 Lucky Dice 兜底。 + +### 模式解析 + +```csharp +public interface ITargetModeResolver +{ + TargetModeEntry Resolve(LuckyDiceCandidate selected, LuckyDiceContext context); +} +``` + +解析优先级: + +1. `TutorialModeRule` +2. `BonusModeRule` +3. `DefaultModeRule` + +第一版映射: + +| ResultKey | TargetKey | Default ModeKey | +| --- | --- | --- | +| `rocket` | `slap_down` | `slap_down_normal` | +| `thief` | `treasure_heist` | `treasure_heist_normal` | + +## 兜底与错误处理 + +### 普通组合未命中 + +处理策略: + +1. 记录 `RollSessionId`、原始 Faces、NormalizedKey。 +2. 使用配置的普通奖励兜底,或返回可见错误给调试面板。 +3. 不进入 Lucky Dice。 + +第一版建议默认发普通奖励兜底,避免玩家流程中断。 + +### Lucky Dice 候选池为空 + +处理策略: + +1. 尝试使用配置的 default result。 +2. 如果 default result 不可用,执行普通奖励兜底。 +3. 记录空池原因:初始候选数量、每个 filter 过滤前后数量、最终兜底类型。 + +### 目标模式解析失败 + +处理策略: + +1. 记录 `ResultKey`、`TargetKey`、上下文标签。 +2. 如果存在 TargetKey 默认模式,降级到默认模式。 +3. 如果不存在默认模式,返回 Lucky Dice 失败并执行普通奖励兜底。 + +## 可观测性 + +每次 Roll 必须拥有同一个 `RollSessionId`,贯穿普通 Roll、规则匹配、Lucky Dice、模式解析和目标玩法启动。 + +最小日志字段: + +```text +RollSessionId +TriggerSource +NormalFaces +NormalDiceCount +ComboNormalizedKey +MatchedRuleId +MatchedRulePriority +ExecutedActions +LuckyDiceInitialCandidates +LuckyDiceFilteredCandidates +LuckyDiceFallbackType +LuckyDiceSelectedResultKey +LuckyDiceSelectedTargetKey +LuckyDiceResultSlotCount +LuckyDiceSlotResultKeys +ResolvedModeKey +FinalOutcome +``` + +日志目标: + +- 复盘一次 Roll 为什么发奖、为什么进 Lucky Dice、为什么进某个玩法。 +- 发现配置问题时能看出是规则未命中、候选池为空、权重异常还是模式解析失败。 + +## 配置示例 + +第一版可以使用本地静态配置或 ScriptableObject;远端配置热更新不在范围内。 + +### 骰子集合配置示例 + +```json +{ + "diceSetId": "normal_main", + "setType": "Normal", + "diceCount": 2, + "faces": ["2", "3", "4", "5", "6", "clover"] +} +``` + +```json +{ + "diceSetId": "lucky_dice", + "setType": "Lucky", + "diceCount": 1, + "defaultResultSlotCount": 3, + "faces": ["rocket", "thief", "chest", "bomb", "key"] +} +``` + +### 普通组合规则示例 + +```json +[ + { + "ruleId": "normal_clover_clover_lucky_dice", + "priority": 100, + "matcher": { + "matcherType": "ExactCombo", + "comboKey": "clover_clover" + }, + "actions": [ + { "actionType": "trigger_lucky_dice", "params": {} } + ], + "stopAfterMatched": true + }, + { + "ruleId": "normal_number_clover_bonus", + "priority": 50, + "matcher": { + "matcherType": "ContainsFace", + "faceKey": "clover", + "requiredCount": 1 + }, + "actions": [ + { "actionType": "grant_reward", "params": {} }, + { "actionType": "add_multiplier", "params": { "reason": "clover_bonus" } } + ], + "stopAfterMatched": true + }, + { + "ruleId": "normal_all_numbers_reward", + "priority": 10, + "matcher": { + "matcherType": "AllNumbers" + }, + "actions": [ + { "actionType": "grant_reward", "params": {} } + ], + "stopAfterMatched": true + } +] +``` + +后续多骰规则示例: + +```json +[ + { + "ruleId": "normal_at_least_two_clovers_lucky_dice", + "priority": 100, + "matcher": { + "matcherType": "FaceCountRange", + "faceKey": "clover", + "minCount": 2 + }, + "actions": [ + { "actionType": "trigger_lucky_dice", "params": {} } + ], + "stopAfterMatched": true + }, + { + "ruleId": "normal_three_of_a_kind_bonus", + "priority": 60, + "matcher": { + "matcherType": "NumberOfAKind", + "ofAKindCount": 3 + }, + "actions": [ + { "actionType": "grant_reward", "params": {} }, + { "actionType": "add_multiplier", "params": { "reason": "three_of_a_kind" } } + ], + "stopAfterMatched": true + } +] +``` + +### Lucky Dice 候选示例 + +```json +[ + { + "resultKey": "rocket", + "targetKey": "slap_down", + "resultSlotCount": 3, + "weight": 1, + "enabled": true, + "minLevel": 0, + "sourceFilter": ["normal_roll"], + "isDefault": true + }, + { + "resultKey": "thief", + "targetKey": "treasure_heist", + "resultSlotCount": 3, + "weight": 1, + "enabled": true, + "minLevel": 0, + "sourceFilter": ["normal_roll"], + "isDefault": false + } +] +``` + +## 测试规格 + +### 组合事实测试 + +- `2 + 3` 生成 `NumberSum = 5`、`CloverCount = 0`、`NormalizedKey = "2_3"`。 +- `2 + clover` 与 `clover + 2` 都生成 `NormalizedKey = "2_clover"`。 +- `clover + clover` 生成 `CloverCount = 2`、`NormalizedKey = "clover_clover"`。 +- `2 + 2 + clover` 生成 `DiceCount = 3`、`NumberValues = [2, 2]`、`NormalizedKey = "2_2_clover"`。 +- Lucky Dice 特殊结果不能被普通 `ComboFactBuilder` 当成普通组合处理。 + +### 规则匹配测试 + +- `clover_clover` 命中优先级 100 的 Lucky Dice 规则。 +- `2_clover` 命中优先级 50 的 Clover bonus 规则。 +- `2_6` 命中优先级 10 的 AllNumbers 规则。 +- 当多条规则同时可命中时,高优先级先执行。 +- 3 骰配置下,`FaceCountRange clover min 2` 能命中任意至少 2 个 Clover 的组合。 +- `NumberOfAKind count 3` 能命中三个相同数字,不依赖具体数字 key。 + +### 行为执行测试 + +- `grant_reward` 调用奖励执行器。 +- `trigger_lucky_dice` 调用 Lucky Dice 流程入口。 +- dispatcher 只按 `ActionType` 分发,不依赖组合 key。 +- 未注册的 action 返回结构化错误并记录日志。 + +### Lucky Dice 测试 + +- Enabled=false 的候选被过滤。 +- 玩家等级不足时候选被 `ProgressFilter` 过滤。 +- 候选池为空时使用 default result。 +- default result 不可用时走普通奖励兜底并记录日志。 +- 权重随机可以通过固定随机源得到可预测结果。 +- Lucky Dice 结果槽数量来自候选结果、目标玩法或 DiceSet 默认配置。 +- 第一版 `rocket` 生成 3 个 `rocket` 展示槽。 +- `rocket` 解析到 `slap_down_normal`。 +- `thief` 解析到 `treasure_heist_normal`。 + +### 链路测试 + +- 普通 Roll 出 `clover + clover` 后,最终进入 Lucky Dice,而不是发普通奖励。 +- Lucky Dice 选中 `rocket` 后,启动 Slap Down 普通模式。 +- Lucky Dice 选中 `thief` 后,启动 Treasure Heist 普通模式。 +- 同一个 `RollSessionId` 能串起普通结果、命中规则、Lucky Dice 候选、随机结果和最终 ModeKey。 +- 普通骰子数量从 2 改为 3 时,组合事实与 matcher 仍按统计结果工作,不出现固定左右骰逻辑。 + +## 验收映射 + +| 需求验收点 | 规格落点 | +| --- | --- | +| 普通骰子结果能转换为结构化组合事实 | `ComboFacts`、组合事实测试 | +| 普通骰子数量和 Lucky Dice 结果槽数量来自配置 | `DiceSetConfig`、`LuckyDiceResultSlots`、槽构建测试 | +| 少量通用匹配器命中普通组合 | `ComboMatcherSpec`、规则匹配测试 | +| 双四叶草触发 Lucky Dice | 普通规则优先级 100、链路测试 | +| 执行层不维护大型组合 map | `IRollActionExecutor`、行为执行测试 | +| Lucky Dice 先筛选再权重随机 | Lucky Dice 筛选、权重随机接口 | +| Lucky Dice 结果解析到目标玩法和模式 | `TargetModeResolver`、模式映射 | +| Rocket 进入 Slap Down 普通模式 | 第一版模式映射、Lucky Dice 测试 | +| Thief 进入 Treasure Heist 普通模式 | 第一版模式映射、Lucky Dice 测试 | +| 候选池为空时有可观测兜底 | 兜底与错误处理、日志字段 | +| 日志能追踪一次 Roll 的关键决策 | 可观测性字段、链路测试 | + +## 第一版不做 + +- 复杂动画完整复刻。 +- 所有 Lucky Dice 特殊结果类型。 +- 复杂运营活动权重策略。 +- 完整短玩法内容实现。 +- 远端配置热更新。 +- 按组合 key 直接绑定业务方法。 + +## 后续扩展建议 + +- 当组合规则增多后,把本地配置迁移到 ScriptableObject 或 JSON 配置表。 +- 当 Lucky Dice 特殊结果超过第一版范围后,补充结果图标、目标玩法、默认模式和冷却规则的配置校验。 +- 当接入埋点时,直接复用 `RollSessionId` 和可观测性字段,避免另起一套追踪链路。 +- 当新增有序组合时,只新增规则声明与 matcher 参数,不修改 `ComboFacts` 的原始结果保存方式。 +- 当普通骰子扩展到 3 颗或 4 颗时,优先新增统计型规则,不新增固定位置分支。 diff --git a/FishDice/Packages/manifest.json b/FishDice/Packages/manifest.json new file mode 100644 index 0000000..9e4493a --- /dev/null +++ b/FishDice/Packages/manifest.json @@ -0,0 +1,41 @@ +{ + "dependencies": { + "com.unity.collab-proxy": "2.12.4", + "com.unity.feature.development": "1.0.1", + "com.unity.textmeshpro": "3.0.7", + "com.unity.timeline": "1.7.7", + "com.unity.ugui": "1.0.0", + "com.unity.visualscripting": "1.9.4", + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.cloth": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.screencapture": "1.0.0", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.terrainphysics": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.umbra": "1.0.0", + "com.unity.modules.unityanalytics": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vehicles": "1.0.0", + "com.unity.modules.video": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.wind": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } +} diff --git a/FishDice/Packages/packages-lock.json b/FishDice/Packages/packages-lock.json new file mode 100644 index 0000000..8e272d6 --- /dev/null +++ b/FishDice/Packages/packages-lock.json @@ -0,0 +1,382 @@ +{ + "dependencies": { + "com.unity.collab-proxy": { + "version": "2.12.4", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.cn" + }, + "com.unity.editorcoroutines": { + "version": "1.0.0", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.cn" + }, + "com.unity.ext.nunit": { + "version": "1.0.6", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.cn" + }, + "com.unity.feature.development": { + "version": "1.0.1", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.ide.visualstudio": "2.0.22", + "com.unity.ide.rider": "3.0.36", + "com.unity.ide.vscode": "1.2.5", + "com.unity.editorcoroutines": "1.0.0", + "com.unity.performance.profile-analyzer": "1.2.3", + "com.unity.test-framework": "1.1.33", + "com.unity.testtools.codecoverage": "1.2.6" + } + }, + "com.unity.ide.rider": { + "version": "3.0.36", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6" + }, + "url": "https://packages.unity.cn" + }, + "com.unity.ide.visualstudio": { + "version": "2.0.22", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.9" + }, + "url": "https://packages.unity.cn" + }, + "com.unity.ide.vscode": { + "version": "1.2.5", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.cn" + }, + "com.unity.performance.profile-analyzer": { + "version": "1.2.3", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.cn" + }, + "com.unity.settings-manager": { + "version": "2.1.0", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.cn" + }, + "com.unity.test-framework": { + "version": "1.1.33", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.cn" + }, + "com.unity.testtools.codecoverage": { + "version": "1.2.6", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.0.16", + "com.unity.settings-manager": "1.0.1" + }, + "url": "https://packages.unity.cn" + }, + "com.unity.textmeshpro": { + "version": "3.0.7", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0" + }, + "url": "https://packages.unity.cn" + }, + "com.unity.timeline": { + "version": "1.7.7", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0" + }, + "url": "https://packages.unity.cn" + }, + "com.unity.ugui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, + "com.unity.visualscripting": { + "version": "1.9.4", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.cn" + }, + "com.unity.modules.ai": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.androidjni": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.animation": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.assetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.audio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.cloth": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.director": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.imageconversion": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imgui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.jsonserialize": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.screencapture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.subsystems": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.terrainphysics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.terrain": "1.0.0" + } + }, + "com.unity.modules.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics2d": "1.0.0" + } + }, + "com.unity.modules.ui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.uielements": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.umbra": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unityanalytics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unitywebrequestassetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestaudio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.audio": "1.0.0" + } + }, + "com.unity.modules.unitywebrequesttexture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestwww": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.vehicles": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.video": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.vr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } + }, + "com.unity.modules.wind": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.xr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.subsystems": "1.0.0" + } + } + } +} diff --git a/FishDice/ProjectSettings/AudioManager.asset b/FishDice/ProjectSettings/AudioManager.asset new file mode 100644 index 0000000..07ebfb0 --- /dev/null +++ b/FishDice/ProjectSettings/AudioManager.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!11 &1 +AudioManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Volume: 1 + Rolloff Scale: 1 + Doppler Factor: 1 + Default Speaker Mode: 2 + m_SampleRate: 0 + m_DSPBufferSize: 1024 + m_VirtualVoiceCount: 512 + m_RealVoiceCount: 32 + m_SpatializerPlugin: + m_AmbisonicDecoderPlugin: + m_DisableAudio: 0 + m_VirtualizeEffects: 1 + m_RequestedDSPBufferSize: 1024 diff --git a/FishDice/ProjectSettings/ClusterInputManager.asset b/FishDice/ProjectSettings/ClusterInputManager.asset new file mode 100644 index 0000000..e7886b2 --- /dev/null +++ b/FishDice/ProjectSettings/ClusterInputManager.asset @@ -0,0 +1,6 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!236 &1 +ClusterInputManager: + m_ObjectHideFlags: 0 + m_Inputs: [] diff --git a/FishDice/ProjectSettings/DynamicsManager.asset b/FishDice/ProjectSettings/DynamicsManager.asset new file mode 100644 index 0000000..cdc1f3e --- /dev/null +++ b/FishDice/ProjectSettings/DynamicsManager.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!55 &1 +PhysicsManager: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_Gravity: {x: 0, y: -9.81, z: 0} + m_DefaultMaterial: {fileID: 0} + m_BounceThreshold: 2 + m_SleepThreshold: 0.005 + m_DefaultContactOffset: 0.01 + m_DefaultSolverIterations: 6 + m_DefaultSolverVelocityIterations: 1 + m_QueriesHitBackfaces: 0 + m_QueriesHitTriggers: 1 + m_EnableAdaptiveForce: 0 + m_ClothInterCollisionDistance: 0 + m_ClothInterCollisionStiffness: 0 + m_ContactsGeneration: 1 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 0 + m_ReuseCollisionCallbacks: 1 + m_ClothInterCollisionSettingsToggle: 0 + m_ContactPairsMode: 0 + m_BroadphaseType: 0 + m_WorldBounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 250, y: 250, z: 250} + m_WorldSubdivisions: 8 + m_FrictionType: 0 + m_EnableEnhancedDeterminism: 0 + m_EnableUnifiedHeightmaps: 1 + m_DefaultMaxAngluarSpeed: 7 diff --git a/FishDice/ProjectSettings/EditorBuildSettings.asset b/FishDice/ProjectSettings/EditorBuildSettings.asset new file mode 100644 index 0000000..f2d323c --- /dev/null +++ b/FishDice/ProjectSettings/EditorBuildSettings.asset @@ -0,0 +1,11 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1045 &1 +EditorBuildSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Scenes: + - enabled: 1 + path: Assets/Scenes/LuckyDiceDemo.unity + guid: b98f810db9dc423aa31423fa5d89d155 + m_configObjects: {} diff --git a/FishDice/ProjectSettings/EditorSettings.asset b/FishDice/ProjectSettings/EditorSettings.asset new file mode 100644 index 0000000..1e44a0a --- /dev/null +++ b/FishDice/ProjectSettings/EditorSettings.asset @@ -0,0 +1,30 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!159 &1 +EditorSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_ExternalVersionControlSupport: Visible Meta Files + m_SerializationMode: 2 + m_LineEndingsForNewScripts: 0 + m_DefaultBehaviorMode: 0 + m_PrefabRegularEnvironment: {fileID: 0} + m_PrefabUIEnvironment: {fileID: 0} + m_SpritePackerMode: 0 + m_SpritePackerPaddingPower: 1 + m_EtcTextureCompressorBehavior: 1 + m_EtcTextureFastCompressor: 1 + m_EtcTextureNormalCompressor: 2 + m_EtcTextureBestCompressor: 4 + m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;rsp;asmref + m_ProjectGenerationRootNamespace: + m_CollabEditorSettings: + inProgressEnabled: 1 + m_EnableTextureStreamingInEditMode: 1 + m_EnableTextureStreamingInPlayMode: 1 + m_AsyncShaderCompilation: 1 + m_EnterPlayModeOptionsEnabled: 0 + m_EnterPlayModeOptions: 3 + m_ShowLightmapResolutionOverlay: 1 + m_UseLegacyProbeSampleCount: 0 + m_SerializeInlineMappingsOnOneLine: 1 diff --git a/FishDice/ProjectSettings/GraphicsSettings.asset b/FishDice/ProjectSettings/GraphicsSettings.asset new file mode 100644 index 0000000..43369e3 --- /dev/null +++ b/FishDice/ProjectSettings/GraphicsSettings.asset @@ -0,0 +1,63 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!30 &1 +GraphicsSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_Deferred: + m_Mode: 1 + m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} + m_DeferredReflections: + m_Mode: 1 + m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} + m_ScreenSpaceShadows: + m_Mode: 1 + m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} + m_LegacyDeferred: + m_Mode: 1 + m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0} + m_DepthNormals: + m_Mode: 1 + m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} + m_MotionVectors: + m_Mode: 1 + m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} + m_LightHalo: + m_Mode: 1 + m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} + m_LensFlare: + m_Mode: 1 + m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} + m_AlwaysIncludedShaders: + - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} + m_PreloadedShaders: [] + m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, + type: 0} + m_CustomRenderPipeline: {fileID: 0} + m_TransparencySortMode: 0 + m_TransparencySortAxis: {x: 0, y: 0, z: 1} + m_DefaultRenderingPath: 1 + m_DefaultMobileRenderingPath: 1 + m_TierSettings: [] + m_LightmapStripping: 0 + m_FogStripping: 0 + m_InstancingStripping: 0 + m_LightmapKeepPlain: 1 + m_LightmapKeepDirCombined: 1 + m_LightmapKeepDynamicPlain: 1 + m_LightmapKeepDynamicDirCombined: 1 + m_LightmapKeepShadowMask: 1 + m_LightmapKeepSubtractive: 1 + m_FogKeepLinear: 1 + m_FogKeepExp: 1 + m_FogKeepExp2: 1 + m_AlbedoSwatchInfos: [] + m_LightsUseLinearIntensity: 0 + m_LightsUseColorTemperature: 0 + m_LogWhenShaderIsCompiled: 0 + m_AllowEnlightenSupportForUpgradedProject: 0 diff --git a/FishDice/ProjectSettings/InputManager.asset b/FishDice/ProjectSettings/InputManager.asset new file mode 100644 index 0000000..17c8f53 --- /dev/null +++ b/FishDice/ProjectSettings/InputManager.asset @@ -0,0 +1,295 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!13 &1 +InputManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Axes: + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: a + altPositiveButton: d + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: s + altPositiveButton: w + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: mouse 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: mouse 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: mouse 2 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: space + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse X + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse Y + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse ScrollWheel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 2 + joyNum: 0 + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 0 + type: 2 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 1 + type: 2 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 0 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 1 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 2 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 3 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: enter + altNegativeButton: + altPositiveButton: space + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Cancel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: escape + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 diff --git a/FishDice/ProjectSettings/MemorySettings.asset b/FishDice/ProjectSettings/MemorySettings.asset new file mode 100644 index 0000000..5b5face --- /dev/null +++ b/FishDice/ProjectSettings/MemorySettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!387306366 &1 +MemorySettings: + m_ObjectHideFlags: 0 + m_EditorMemorySettings: + m_MainAllocatorBlockSize: -1 + m_ThreadAllocatorBlockSize: -1 + m_MainGfxBlockSize: -1 + m_ThreadGfxBlockSize: -1 + m_CacheBlockSize: -1 + m_TypetreeBlockSize: -1 + m_ProfilerBlockSize: -1 + m_ProfilerEditorBlockSize: -1 + m_BucketAllocatorGranularity: -1 + m_BucketAllocatorBucketsCount: -1 + m_BucketAllocatorBlockSize: -1 + m_BucketAllocatorBlockCount: -1 + m_ProfilerBucketAllocatorGranularity: -1 + m_ProfilerBucketAllocatorBucketsCount: -1 + m_ProfilerBucketAllocatorBlockSize: -1 + m_ProfilerBucketAllocatorBlockCount: -1 + m_TempAllocatorSizeMain: -1 + m_JobTempAllocatorBlockSize: -1 + m_BackgroundJobTempAllocatorBlockSize: -1 + m_JobTempAllocatorReducedBlockSize: -1 + m_TempAllocatorSizeGIBakingWorker: -1 + m_TempAllocatorSizeNavMeshWorker: -1 + m_TempAllocatorSizeAudioWorker: -1 + m_TempAllocatorSizeCloudWorker: -1 + m_TempAllocatorSizeGfx: -1 + m_TempAllocatorSizeJobWorker: -1 + m_TempAllocatorSizeBackgroundWorker: -1 + m_TempAllocatorSizePreloadManager: -1 + m_PlatformMemorySettings: {} diff --git a/FishDice/ProjectSettings/NavMeshAreas.asset b/FishDice/ProjectSettings/NavMeshAreas.asset new file mode 100644 index 0000000..3b0b7c3 --- /dev/null +++ b/FishDice/ProjectSettings/NavMeshAreas.asset @@ -0,0 +1,91 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!126 &1 +NavMeshProjectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + areas: + - name: Walkable + cost: 1 + - name: Not Walkable + cost: 1 + - name: Jump + cost: 2 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + m_LastAgentTypeID: -887442657 + m_Settings: + - serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.75 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + debug: + m_Flags: 0 + m_SettingNames: + - Humanoid diff --git a/FishDice/ProjectSettings/PackageManagerSettings.asset b/FishDice/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 0000000..5f48647 --- /dev/null +++ b/FishDice/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,36 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_EnablePreReleasePackages: 0 + m_AdvancedSettingsExpanded: 1 + m_ScopedRegistriesSettingsExpanded: 1 + m_SeeAllPackageVersions: 0 + m_DismissPreviewPackagesInUse: 0 + oneTimeWarningShown: 0 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.cn + m_Scopes: [] + m_IsDefault: 1 + m_Capabilities: 7 + m_ConfigSource: 0 + m_UserSelectedRegistryName: + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_Modified: 0 + m_ErrorMessage: + m_UserModificationsInstanceId: -836 + m_OriginalInstanceId: -838 + m_LoadAssets: 0 diff --git a/FishDice/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json b/FishDice/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json new file mode 100644 index 0000000..3c7b4c1 --- /dev/null +++ b/FishDice/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json @@ -0,0 +1,5 @@ +{ + "m_Dictionary": { + "m_DictionaryValues": [] + } +} \ No newline at end of file diff --git a/FishDice/ProjectSettings/Physics2DSettings.asset b/FishDice/ProjectSettings/Physics2DSettings.asset new file mode 100644 index 0000000..47880b1 --- /dev/null +++ b/FishDice/ProjectSettings/Physics2DSettings.asset @@ -0,0 +1,56 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!19 &1 +Physics2DSettings: + m_ObjectHideFlags: 0 + serializedVersion: 4 + m_Gravity: {x: 0, y: -9.81} + m_DefaultMaterial: {fileID: 0} + m_VelocityIterations: 8 + m_PositionIterations: 3 + m_VelocityThreshold: 1 + m_MaxLinearCorrection: 0.2 + m_MaxAngularCorrection: 8 + m_MaxTranslationSpeed: 100 + m_MaxRotationSpeed: 360 + m_BaumgarteScale: 0.2 + m_BaumgarteTimeOfImpactScale: 0.75 + m_TimeToSleep: 0.5 + m_LinearSleepTolerance: 0.01 + m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_JobOptions: + serializedVersion: 2 + useMultithreading: 0 + useConsistencySorting: 0 + m_InterpolationPosesPerJob: 100 + m_NewContactsPerJob: 30 + m_CollideContactsPerJob: 100 + m_ClearFlagsPerJob: 200 + m_ClearBodyForcesPerJob: 200 + m_SyncDiscreteFixturesPerJob: 50 + m_SyncContinuousFixturesPerJob: 50 + m_FindNearestContactsPerJob: 100 + m_UpdateTriggerContactsPerJob: 100 + m_IslandSolverCostThreshold: 100 + m_IslandSolverBodyCostScale: 1 + m_IslandSolverContactCostScale: 10 + m_IslandSolverJointCostScale: 10 + m_IslandSolverBodiesPerJob: 50 + m_IslandSolverContactsPerJob: 50 + m_AutoSimulation: 1 + m_QueriesHitTriggers: 1 + m_QueriesStartInColliders: 1 + m_CallbacksOnDisable: 1 + m_ReuseCollisionCallbacks: 1 + m_AutoSyncTransforms: 0 + m_AlwaysShowColliders: 0 + m_ShowColliderSleep: 1 + m_ShowColliderContacts: 0 + m_ShowColliderAABB: 0 + m_ContactArrowScale: 0.2 + m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} + m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} + m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} + m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/FishDice/ProjectSettings/PresetManager.asset b/FishDice/ProjectSettings/PresetManager.asset new file mode 100644 index 0000000..67a94da --- /dev/null +++ b/FishDice/ProjectSettings/PresetManager.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_DefaultPresets: {} diff --git a/FishDice/ProjectSettings/ProjectSettings.asset b/FishDice/ProjectSettings/ProjectSettings.asset new file mode 100644 index 0000000..d9ee673 --- /dev/null +++ b/FishDice/ProjectSettings/ProjectSettings.asset @@ -0,0 +1,961 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!129 &1 +PlayerSettings: + m_ObjectHideFlags: 0 + serializedVersion: 26 + productGUID: c3f808813b7bb114cb618f820bee9406 + AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 + AndroidEnableSustainedPerformanceMode: 0 + defaultScreenOrientation: 4 + targetDevice: 2 + useOnDemandResources: 0 + accelerometerFrequency: 60 + companyName: DefaultCompany + productName: FishDice + defaultCursor: {fileID: 0} + cursorHotspot: {x: 0, y: 0} + m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} + m_ShowUnitySplashScreen: 1 + m_ShowUnitySplashLogo: 1 + m_SplashScreenOverlayOpacity: 1 + m_SplashScreenAnimation: 1 + m_SplashScreenLogoStyle: 1 + m_SplashScreenDrawMode: 0 + m_SplashScreenBackgroundAnimationZoom: 1 + m_SplashScreenLogoAnimationZoom: 1 + m_SplashScreenBackgroundLandscapeAspect: 1 + m_SplashScreenBackgroundPortraitAspect: 1 + m_SplashScreenBackgroundLandscapeUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenBackgroundPortraitUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenLogos: [] + m_VirtualRealitySplashScreen: {fileID: 0} + m_HolographicTrackingLossScreen: {fileID: 0} + defaultScreenWidth: 1920 + defaultScreenHeight: 1080 + defaultScreenWidthWeb: 960 + defaultScreenHeightWeb: 600 + m_StereoRenderingPath: 0 + m_ActiveColorSpace: 1 + unsupportedMSAAFallback: 0 + m_SpriteBatchVertexThreshold: 300 + m_MTRendering: 1 + mipStripping: 0 + numberOfMipsStripped: 0 + numberOfMipsStrippedPerMipmapLimitGroup: {} + m_StackTraceTypes: 010000000100000001000000010000000100000001000000 + iosShowActivityIndicatorOnLoading: -1 + androidShowActivityIndicatorOnLoading: -1 + iosUseCustomAppBackgroundBehavior: 0 + allowedAutorotateToPortrait: 1 + allowedAutorotateToPortraitUpsideDown: 1 + allowedAutorotateToLandscapeRight: 1 + allowedAutorotateToLandscapeLeft: 1 + useOSAutorotation: 1 + use32BitDisplayBuffer: 1 + preserveFramebufferAlpha: 0 + disableDepthAndStencilBuffers: 0 + androidStartInFullscreen: 1 + androidRenderOutsideSafeArea: 1 + androidUseSwappy: 1 + androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 + androidAutoRotationBehavior: 1 + androidPredictiveBackSupport: 1 + defaultIsNativeResolution: 1 + macRetinaSupport: 1 + runInBackground: 1 + captureSingleScreen: 0 + muteOtherAudioSources: 0 + Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 + audioSpatialExperience: 0 + deferSystemGesturesMode: 0 + hideHomeButton: 0 + submitAnalytics: 1 + usePlayerLog: 1 + dedicatedServerOptimizations: 0 + bakeCollisionMeshes: 0 + forceSingleInstance: 0 + useFlipModelSwapchain: 1 + resizableWindow: 0 + useMacAppStoreValidation: 0 + macAppStoreCategory: public.app-category.games + gpuSkinning: 1 + xboxPIXTextureCapture: 0 + xboxEnableAvatar: 0 + xboxEnableKinect: 0 + xboxEnableKinectAutoTracking: 0 + xboxEnableFitness: 0 + visibleInBackground: 1 + allowFullscreenSwitch: 1 + fullscreenMode: 1 + xboxSpeechDB: 0 + xboxEnableHeadOrientation: 0 + xboxEnableGuest: 0 + xboxEnablePIXSampling: 0 + metalFramebufferOnly: 0 + xboxOneResolution: 0 + xboxOneSResolution: 0 + xboxOneXResolution: 3 + xboxOneMonoLoggingLevel: 0 + xboxOneLoggingLevel: 1 + xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 + xboxOnePresentImmediateThreshold: 0 + switchQueueCommandMemory: 0 + switchQueueControlMemory: 16384 + switchQueueComputeMemory: 262144 + switchNVNShaderPoolsGranularity: 33554432 + switchNVNDefaultPoolsGranularity: 16777216 + switchNVNOtherPoolsGranularity: 16777216 + switchGpuScratchPoolGranularity: 2097152 + switchAllowGpuScratchShrinking: 0 + switchNVNMaxPublicTextureIDCount: 0 + switchNVNMaxPublicSamplerIDCount: 0 + switchNVNGraphicsFirmwareMemory: 32 + switchMaxWorkerMultiple: 8 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 + vulkanNumSwapchainBuffers: 3 + vulkanEnableSetSRGBWrite: 0 + vulkanEnablePreTransform: 1 + vulkanEnableLateAcquireNextImage: 0 + vulkanEnableCommandBufferRecycling: 1 + loadStoreDebugModeEnabled: 0 + visionOSBundleVersion: 1.0 + tvOSBundleVersion: 1.0 + bundleVersion: 0.1 + preloadedAssets: [] + metroInputSource: 0 + wsaTransparentSwapchain: 0 + m_HolographicPauseOnTrackingLoss: 1 + xboxOneDisableKinectGpuReservation: 1 + xboxOneEnable7thCore: 1 + vrSettings: + enable360StereoCapture: 0 + isWsaHolographicRemotingEnabled: 0 + enableFrameTimingStats: 0 + enableOpenGLProfilerGPURecorders: 1 + allowHDRDisplaySupport: 0 + useHDRDisplay: 0 + hdrBitDepth: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 30 + resolutionScalingMode: 0 + resetResolutionOnWindowResize: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 + applicationIdentifier: {} + buildNumber: + Standalone: 0 + VisionOS: 0 + iPhone: 0 + tvOS: 0 + overrideDefaultApplicationIdentifier: 0 + AndroidBundleVersionCode: 1 + AndroidMinSdkVersion: 22 + AndroidTargetSdkVersion: 0 + AndroidPreferredInstallLocation: 1 + aotOptions: + stripEngineCode: 1 + iPhoneStrippingLevel: 0 + iPhoneScriptCallOptimization: 0 + ForceInternetPermission: 0 + ForceSDCardPermission: 0 + CreateWallpaper: 0 + APKExpansionFiles: 0 + keepLoadedShadersAlive: 0 + StripUnusedMeshComponents: 1 + strictShaderVariantMatching: 0 + VertexChannelCompressionMask: 4054 + iPhoneSdkVersion: 988 + iOSSimulatorArchitecture: 0 + iOSTargetOSVersionString: 12.0 + tvOSSdkVersion: 0 + tvOSSimulatorArchitecture: 0 + tvOSRequireExtendedGameController: 0 + tvOSTargetOSVersionString: 12.0 + VisionOSSdkVersion: 0 + VisionOSTargetOSVersionString: 1.0 + uIPrerenderedIcon: 0 + uIRequiresPersistentWiFi: 0 + uIRequiresFullScreen: 1 + uIStatusBarHidden: 1 + uIExitOnSuspend: 0 + uIStatusBarStyle: 0 + appleTVSplashScreen: {fileID: 0} + appleTVSplashScreen2x: {fileID: 0} + tvOSSmallIconLayers: [] + tvOSSmallIconLayers2x: [] + tvOSLargeIconLayers: [] + tvOSLargeIconLayers2x: [] + tvOSTopShelfImageLayers: [] + tvOSTopShelfImageLayers2x: [] + tvOSTopShelfImageWideLayers: [] + tvOSTopShelfImageWideLayers2x: [] + iOSLaunchScreenType: 0 + iOSLaunchScreenPortrait: {fileID: 0} + iOSLaunchScreenLandscape: {fileID: 0} + iOSLaunchScreenBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreenFillPct: 100 + iOSLaunchScreenSize: 100 + iOSLaunchScreenCustomXibPath: + iOSLaunchScreeniPadType: 0 + iOSLaunchScreeniPadImage: {fileID: 0} + iOSLaunchScreeniPadBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreeniPadFillPct: 100 + iOSLaunchScreeniPadSize: 100 + iOSLaunchScreeniPadCustomXibPath: + iOSLaunchScreenCustomStoryboardPath: + iOSLaunchScreeniPadCustomStoryboardPath: + iOSDeviceRequirements: [] + iOSURLSchemes: [] + macOSURLSchemes: [] + iOSBackgroundModes: 0 + iOSMetalForceHardShadows: 0 + metalEditorSupport: 1 + metalAPIValidation: 1 + metalCompileShaderBinary: 0 + iOSRenderExtraFrameOnPause: 0 + iosCopyPluginsCodeInsteadOfSymlink: 0 + appleDeveloperTeamID: + iOSManualSigningProvisioningProfileID: + tvOSManualSigningProvisioningProfileID: + VisionOSManualSigningProvisioningProfileID: + iOSManualSigningProvisioningProfileType: 0 + tvOSManualSigningProvisioningProfileType: 0 + VisionOSManualSigningProvisioningProfileType: 0 + appleEnableAutomaticSigning: 0 + iOSRequireARKit: 0 + iOSAutomaticallyDetectAndAddCapabilities: 1 + appleEnableProMotion: 0 + shaderPrecisionModel: 0 + clonedFromGUID: c0afd0d1d80e3634a9dac47e8a0426ea + templatePackageId: com.unity.template.3d@8.1.3 + templateDefaultScene: Assets/Scenes/SampleScene.unity + useCustomMainManifest: 0 + useCustomLauncherManifest: 0 + useCustomMainGradleTemplate: 0 + useCustomLauncherGradleManifest: 0 + useCustomBaseGradleTemplate: 0 + useCustomGradlePropertiesTemplate: 0 + useCustomGradleSettingsTemplate: 0 + useCustomProguardFile: 0 + AndroidTargetArchitectures: 1 + AndroidTargetDevices: 0 + AndroidSplashScreenScale: 0 + androidSplashScreen: {fileID: 0} + AndroidKeystoreName: + AndroidKeyaliasName: + AndroidEnableArmv9SecurityFeatures: 0 + AndroidBuildApkPerCpuArchitecture: 0 + AndroidTVCompatibility: 0 + AndroidIsGame: 1 + AndroidEnableTango: 0 + androidEnableBanner: 1 + androidUseLowAccuracyLocation: 0 + androidUseCustomKeystore: 0 + m_AndroidBanners: + - width: 320 + height: 180 + banner: {fileID: 0} + androidGamepadSupportLevel: 0 + chromeosInputEmulation: 1 + AndroidMinifyRelease: 0 + AndroidMinifyDebug: 0 + AndroidValidateAppBundleSize: 1 + AndroidAppBundleSizeToValidate: 150 + m_BuildTargetIcons: [] + m_BuildTargetPlatformIcons: + - m_BuildTarget: iPhone + m_Icons: + - m_Textures: [] + m_Width: 180 + m_Height: 180 + m_Kind: 0 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 120 + m_Height: 120 + m_Kind: 0 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 167 + m_Height: 167 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 152 + m_Height: 152 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 76 + m_Height: 76 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 120 + m_Height: 120 + m_Kind: 3 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 80 + m_Height: 80 + m_Kind: 3 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 80 + m_Height: 80 + m_Kind: 3 + m_SubKind: iPad + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 3 + m_SubKind: iPad + - m_Textures: [] + m_Width: 87 + m_Height: 87 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 58 + m_Height: 58 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 29 + m_Height: 29 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 58 + m_Height: 58 + m_Kind: 1 + m_SubKind: iPad + - m_Textures: [] + m_Width: 29 + m_Height: 29 + m_Kind: 1 + m_SubKind: iPad + - m_Textures: [] + m_Width: 60 + m_Height: 60 + m_Kind: 2 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 2 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 2 + m_SubKind: iPad + - m_Textures: [] + m_Width: 20 + m_Height: 20 + m_Kind: 2 + m_SubKind: iPad + - m_Textures: [] + m_Width: 1024 + m_Height: 1024 + m_Kind: 4 + m_SubKind: App Store + - m_BuildTarget: Android + m_Icons: + - m_Textures: [] + m_Width: 432 + m_Height: 432 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 324 + m_Height: 324 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 216 + m_Height: 216 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 162 + m_Height: 162 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 108 + m_Height: 108 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 81 + m_Height: 81 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 0 + m_SubKind: + m_BuildTargetBatching: + - m_BuildTarget: Standalone + m_StaticBatching: 1 + m_DynamicBatching: 0 + - m_BuildTarget: tvOS + m_StaticBatching: 1 + m_DynamicBatching: 0 + - m_BuildTarget: Android + m_StaticBatching: 1 + m_DynamicBatching: 0 + - m_BuildTarget: iPhone + m_StaticBatching: 1 + m_DynamicBatching: 0 + - m_BuildTarget: WebGL + m_StaticBatching: 0 + m_DynamicBatching: 0 + m_BuildTargetShaderSettings: [] + m_BuildTargetGraphicsJobs: + - m_BuildTarget: MacStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: Switch + m_GraphicsJobs: 1 + - m_BuildTarget: MetroSupport + m_GraphicsJobs: 1 + - m_BuildTarget: AppleTVSupport + m_GraphicsJobs: 0 + - m_BuildTarget: BJMSupport + m_GraphicsJobs: 1 + - m_BuildTarget: LinuxStandaloneSupport + m_GraphicsJobs: 1 + - m_BuildTarget: PS4Player + m_GraphicsJobs: 1 + - m_BuildTarget: iOSSupport + m_GraphicsJobs: 0 + - m_BuildTarget: WindowsStandaloneSupport + m_GraphicsJobs: 1 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobs: 1 + - m_BuildTarget: LuminSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AndroidPlayer + m_GraphicsJobs: 0 + - m_BuildTarget: WebGLSupport + m_GraphicsJobs: 0 + m_BuildTargetGraphicsJobMode: + - m_BuildTarget: PS4Player + m_GraphicsJobMode: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobMode: 0 + m_BuildTargetGraphicsAPIs: + - m_BuildTarget: AndroidPlayer + m_APIs: 150000000b000000 + m_Automatic: 1 + - m_BuildTarget: iOSSupport + m_APIs: 10000000 + m_Automatic: 1 + - m_BuildTarget: AppleTVSupport + m_APIs: 10000000 + m_Automatic: 1 + - m_BuildTarget: WebGLSupport + m_APIs: 0b000000 + m_Automatic: 1 + m_BuildTargetVRSettings: + - m_BuildTarget: Standalone + m_Enabled: 0 + m_Devices: + - Oculus + - OpenVR + m_DefaultShaderChunkSizeInMB: 16 + m_DefaultShaderChunkCount: 0 + openGLRequireES31: 0 + openGLRequireES31AEP: 0 + openGLRequireES32: 0 + m_TemplateCustomTags: {} + mobileMTRendering: + Android: 1 + iPhone: 1 + tvOS: 1 + m_BuildTargetGroupLightmapEncodingQuality: + - m_BuildTarget: Android + m_EncodingQuality: 1 + - m_BuildTarget: iPhone + m_EncodingQuality: 1 + - m_BuildTarget: tvOS + m_EncodingQuality: 1 + m_BuildTargetGroupHDRCubemapEncodingQuality: + - m_BuildTarget: Android + m_EncodingQuality: 1 + - m_BuildTarget: iPhone + m_EncodingQuality: 1 + - m_BuildTarget: tvOS + m_EncodingQuality: 1 + m_BuildTargetGroupLightmapSettings: [] + m_BuildTargetGroupLoadStoreDebugModeSettings: [] + m_BuildTargetNormalMapEncoding: + - m_BuildTarget: Android + m_Encoding: 1 + - m_BuildTarget: iPhone + m_Encoding: 1 + - m_BuildTarget: tvOS + m_Encoding: 1 + m_BuildTargetDefaultTextureCompressionFormat: + - m_BuildTarget: Android + m_Format: 3 + playModeTestRunnerEnabled: 0 + runPlayModeTestAsEditModeTest: 0 + actionOnDotNetUnhandledException: 1 + enableInternalProfiler: 0 + logObjCUncaughtExceptions: 1 + enableCrashReportAPI: 0 + cameraUsageDescription: + locationUsageDescription: + microphoneUsageDescription: + bluetoothUsageDescription: + macOSTargetOSVersion: 10.13.0 + switchNMETAOverride: + switchNetLibKey: + switchSocketMemoryPoolSize: 6144 + switchSocketAllocatorPoolSize: 128 + switchSocketConcurrencyLimit: 14 + switchScreenResolutionBehavior: 2 + switchUseCPUProfiler: 0 + switchEnableFileSystemTrace: 0 + switchLTOSetting: 0 + switchApplicationID: 0x01004b9000490000 + switchNSODependencies: + switchCompilerFlags: + switchTitleNames_0: + switchTitleNames_1: + switchTitleNames_2: + switchTitleNames_3: + switchTitleNames_4: + switchTitleNames_5: + switchTitleNames_6: + switchTitleNames_7: + switchTitleNames_8: + switchTitleNames_9: + switchTitleNames_10: + switchTitleNames_11: + switchTitleNames_12: + switchTitleNames_13: + switchTitleNames_14: + switchTitleNames_15: + switchPublisherNames_0: + switchPublisherNames_1: + switchPublisherNames_2: + switchPublisherNames_3: + switchPublisherNames_4: + switchPublisherNames_5: + switchPublisherNames_6: + switchPublisherNames_7: + switchPublisherNames_8: + switchPublisherNames_9: + switchPublisherNames_10: + switchPublisherNames_11: + switchPublisherNames_12: + switchPublisherNames_13: + switchPublisherNames_14: + switchPublisherNames_15: + switchIcons_0: {fileID: 0} + switchIcons_1: {fileID: 0} + switchIcons_2: {fileID: 0} + switchIcons_3: {fileID: 0} + switchIcons_4: {fileID: 0} + switchIcons_5: {fileID: 0} + switchIcons_6: {fileID: 0} + switchIcons_7: {fileID: 0} + switchIcons_8: {fileID: 0} + switchIcons_9: {fileID: 0} + switchIcons_10: {fileID: 0} + switchIcons_11: {fileID: 0} + switchIcons_12: {fileID: 0} + switchIcons_13: {fileID: 0} + switchIcons_14: {fileID: 0} + switchIcons_15: {fileID: 0} + switchSmallIcons_0: {fileID: 0} + switchSmallIcons_1: {fileID: 0} + switchSmallIcons_2: {fileID: 0} + switchSmallIcons_3: {fileID: 0} + switchSmallIcons_4: {fileID: 0} + switchSmallIcons_5: {fileID: 0} + switchSmallIcons_6: {fileID: 0} + switchSmallIcons_7: {fileID: 0} + switchSmallIcons_8: {fileID: 0} + switchSmallIcons_9: {fileID: 0} + switchSmallIcons_10: {fileID: 0} + switchSmallIcons_11: {fileID: 0} + switchSmallIcons_12: {fileID: 0} + switchSmallIcons_13: {fileID: 0} + switchSmallIcons_14: {fileID: 0} + switchSmallIcons_15: {fileID: 0} + switchManualHTML: + switchAccessibleURLs: + switchLegalInformation: + switchMainThreadStackSize: 1048576 + switchPresenceGroupId: + switchLogoHandling: 0 + switchReleaseVersion: 0 + switchDisplayVersion: 1.0.0 + switchStartupUserAccount: 0 + switchSupportedLanguagesMask: 0 + switchLogoType: 0 + switchApplicationErrorCodeCategory: + switchUserAccountSaveDataSize: 0 + switchUserAccountSaveDataJournalSize: 0 + switchApplicationAttribute: 0 + switchCardSpecSize: -1 + switchCardSpecClock: -1 + switchRatingsMask: 0 + switchRatingsInt_0: 0 + switchRatingsInt_1: 0 + switchRatingsInt_2: 0 + switchRatingsInt_3: 0 + switchRatingsInt_4: 0 + switchRatingsInt_5: 0 + switchRatingsInt_6: 0 + switchRatingsInt_7: 0 + switchRatingsInt_8: 0 + switchRatingsInt_9: 0 + switchRatingsInt_10: 0 + switchRatingsInt_11: 0 + switchRatingsInt_12: 0 + switchLocalCommunicationIds_0: + switchLocalCommunicationIds_1: + switchLocalCommunicationIds_2: + switchLocalCommunicationIds_3: + switchLocalCommunicationIds_4: + switchLocalCommunicationIds_5: + switchLocalCommunicationIds_6: + switchLocalCommunicationIds_7: + switchParentalControl: 0 + switchAllowsScreenshot: 1 + switchAllowsVideoCapturing: 1 + switchAllowsRuntimeAddOnContentInstall: 0 + switchDataLossConfirmation: 0 + switchUserAccountLockEnabled: 0 + switchSystemResourceMemory: 16777216 + switchSupportedNpadStyles: 22 + switchNativeFsCacheSize: 32 + switchIsHoldTypeHorizontal: 0 + switchSupportedNpadCount: 8 + switchEnableTouchScreen: 1 + switchSocketConfigEnabled: 0 + switchTcpInitialSendBufferSize: 32 + switchTcpInitialReceiveBufferSize: 64 + switchTcpAutoSendBufferSizeMax: 256 + switchTcpAutoReceiveBufferSizeMax: 256 + switchUdpSendBufferSize: 9 + switchUdpReceiveBufferSize: 42 + switchSocketBufferEfficiency: 4 + switchSocketInitializeEnabled: 1 + switchNetworkInterfaceManagerInitializeEnabled: 1 + switchDisableHTCSPlayerConnection: 0 + switchUseNewStyleFilepaths: 1 + switchUseLegacyFmodPriorities: 0 + switchUseMicroSleepForYield: 1 + switchEnableRamDiskSupport: 0 + switchMicroSleepForYieldTime: 25 + switchRamDiskSpaceSize: 12 + ps4NPAgeRating: 12 + ps4NPTitleSecret: + ps4NPTrophyPackPath: + ps4ParentalLevel: 11 + ps4ContentID: ED1633-NPXX51362_00-0000000000000000 + ps4Category: 0 + ps4MasterVersion: 01.00 + ps4AppVersion: 01.00 + ps4AppType: 0 + ps4ParamSfxPath: + ps4VideoOutPixelFormat: 0 + ps4VideoOutInitialWidth: 1920 + ps4VideoOutBaseModeInitialWidth: 1920 + ps4VideoOutReprojectionRate: 60 + ps4PronunciationXMLPath: + ps4PronunciationSIGPath: + ps4BackgroundImagePath: + ps4StartupImagePath: + ps4StartupImagesFolder: + ps4IconImagesFolder: + ps4SaveDataImagePath: + ps4SdkOverride: + ps4BGMPath: + ps4ShareFilePath: + ps4ShareOverlayImagePath: + ps4PrivacyGuardImagePath: + ps4ExtraSceSysFile: + ps4NPtitleDatPath: + ps4RemotePlayKeyAssignment: -1 + ps4RemotePlayKeyMappingDir: + ps4PlayTogetherPlayerCount: 0 + ps4EnterButtonAssignment: 1 + ps4ApplicationParam1: 0 + ps4ApplicationParam2: 0 + ps4ApplicationParam3: 0 + ps4ApplicationParam4: 0 + ps4DownloadDataSize: 0 + ps4GarlicHeapSize: 2048 + ps4ProGarlicHeapSize: 2560 + playerPrefsMaxSize: 32768 + ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ + ps4pnSessions: 1 + ps4pnPresence: 1 + ps4pnFriends: 1 + ps4pnGameCustomData: 1 + playerPrefsSupport: 0 + enableApplicationExit: 0 + resetTempFolder: 1 + restrictedAudioUsageRights: 0 + ps4UseResolutionFallback: 0 + ps4ReprojectionSupport: 0 + ps4UseAudio3dBackend: 0 + ps4UseLowGarlicFragmentationMode: 1 + ps4SocialScreenEnabled: 0 + ps4ScriptOptimizationLevel: 0 + ps4Audio3dVirtualSpeakerCount: 14 + ps4attribCpuUsage: 0 + ps4PatchPkgPath: + ps4PatchLatestPkgPath: + ps4PatchChangeinfoPath: + ps4PatchDayOne: 0 + ps4attribUserManagement: 0 + ps4attribMoveSupport: 0 + ps4attrib3DSupport: 0 + ps4attribShareSupport: 0 + ps4attribExclusiveVR: 0 + ps4disableAutoHideSplash: 0 + ps4videoRecordingFeaturesUsed: 0 + ps4contentSearchFeaturesUsed: 0 + ps4CompatibilityPS5: 0 + ps4AllowPS5Detection: 0 + ps4GPU800MHz: 1 + ps4attribEyeToEyeDistanceSettingVR: 0 + ps4IncludedModules: [] + ps4attribVROutputEnabled: 0 + monoEnv: + splashScreenBackgroundSourceLandscape: {fileID: 0} + splashScreenBackgroundSourcePortrait: {fileID: 0} + blurSplashScreenBackground: 1 + spritePackerPolicy: + webGLMemorySize: 16 + webGLExceptionSupport: 1 + webGLNameFilesAsHashes: 0 + webGLShowDiagnostics: 0 + webGLDataCaching: 1 + webGLDebugSymbols: 0 + webGLEmscriptenArgs: + webGLModulesDirectory: + webGLTemplate: APPLICATION:Default + webGLAnalyzeBuildSize: 0 + webGLUseEmbeddedResources: 0 + webGLCompressionFormat: 1 + webGLWasmArithmeticExceptions: 0 + webGLLinkerTarget: 1 + webGLThreadsSupport: 0 + webGLDecompressionFallback: 0 + webGLInitialMemorySize: 32 + webGLMaximumMemorySize: 2048 + webGLMemoryGrowthMode: 2 + webGLMemoryLinearGrowthStep: 16 + webGLMemoryGeometricGrowthStep: 0.2 + webGLMemoryGeometricGrowthCap: 96 + webGLPowerPreference: 2 + scriptingDefineSymbols: {} + additionalCompilerArguments: {} + platformArchitecture: {} + scriptingBackend: {} + il2cppCompilerConfiguration: {} + il2cppCodeGeneration: {} + managedStrippingLevel: + EmbeddedLinux: 1 + GameCoreScarlett: 1 + GameCoreXboxOne: 1 + Nintendo Switch: 1 + PS4: 1 + PS5: 1 + QNX: 1 + Stadia: 1 + VisionOS: 1 + WebGL: 1 + Windows Store Apps: 1 + XboxOne: 1 + iPhone: 1 + tvOS: 1 + incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 + allowUnsafeCode: 0 + useDeterministicCompilation: 1 + additionalIl2CppArgs: + scriptingRuntimeVersion: 1 + gcIncremental: 1 + gcWBarrierValidation: 0 + apiCompatibilityLevelPerPlatform: {} + m_RenderingPath: 1 + m_MobileRenderingPath: 1 + metroPackageName: FishDice + metroPackageVersion: + metroCertificatePath: + metroCertificatePassword: + metroCertificateSubject: + metroCertificateIssuer: + metroCertificateNotAfter: 0000000000000000 + metroApplicationDescription: FishDice + wsaImages: {} + metroTileShortName: + metroTileShowName: 0 + metroMediumTileShowName: 0 + metroLargeTileShowName: 0 + metroWideTileShowName: 0 + metroSupportStreamingInstall: 0 + metroLastRequiredScene: 0 + metroDefaultTileSize: 1 + metroTileForegroundText: 2 + metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0} + metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, a: 1} + metroSplashScreenUseBackgroundColor: 0 + syncCapabilities: 0 + platformCapabilities: {} + metroTargetDeviceFamilies: {} + metroFTAName: + metroFTAFileTypes: [] + metroProtocolName: + vcxProjDefaultLanguage: + XboxOneProductId: + XboxOneUpdateKey: + XboxOneSandboxId: + XboxOneContentId: + XboxOneTitleId: + XboxOneSCId: + XboxOneGameOsOverridePath: + XboxOnePackagingOverridePath: + XboxOneAppManifestOverridePath: + XboxOneVersion: 1.0.0.0 + XboxOnePackageEncryption: 0 + XboxOnePackageUpdateGranularity: 2 + XboxOneDescription: + XboxOneLanguage: + - enus + XboxOneCapability: [] + XboxOneGameRating: {} + XboxOneIsContentPackage: 0 + XboxOneEnhancedXboxCompatibilityMode: 0 + XboxOneEnableGPUVariability: 1 + XboxOneSockets: {} + XboxOneSplashScreen: {fileID: 0} + XboxOneAllowedProductIds: [] + XboxOnePersistentLocalStorageSize: 0 + XboxOneXTitleMemory: 8 + XboxOneOverrideIdentityName: + XboxOneOverrideIdentityPublisher: + vrEditorSettings: {} + cloudServicesEnabled: + UNet: 1 + luminIcon: + m_Name: + m_ModelFolderPath: + m_PortalFolderPath: + luminCert: + m_CertPath: + m_SignPackage: 1 + luminIsChannelApp: 0 + luminVersion: + m_VersionCode: 1 + m_VersionName: + hmiPlayerDataPath: + hmiForceSRGBBlit: 1 + embeddedLinuxEnableGamepadInput: 1 + hmiLogStartupTiming: 0 + hmiCpuConfiguration: + apiCompatibilityLevel: 6 + activeInputHandler: 0 + windowsGamepadBackendHint: 0 + cloudProjectId: 7acf0de3-b123-48ee-87a5-2813ed79a71b + framebufferDepthMemorylessMode: 0 + qualitySettingsNames: [] + projectName: FishDice + organizationId: jisd8018 + cloudEnabled: 0 + legacyClampBlendShapeWeights: 0 + hmiLoadingImage: {fileID: 0} + platformRequiresReadableAssets: 0 + virtualTexturingSupportEnabled: 0 + insecureHttpOption: 0 diff --git a/FishDice/ProjectSettings/ProjectVersion.txt b/FishDice/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..d87ee45 --- /dev/null +++ b/FishDice/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2022.3.62f2c1 +m_EditorVersionWithRevision: 2022.3.62f2c1 (92e6e6be66dc) diff --git a/FishDice/ProjectSettings/QualitySettings.asset b/FishDice/ProjectSettings/QualitySettings.asset new file mode 100644 index 0000000..36c0dad --- /dev/null +++ b/FishDice/ProjectSettings/QualitySettings.asset @@ -0,0 +1,234 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!47 &1 +QualitySettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_CurrentQuality: 5 + m_QualitySettings: + - serializedVersion: 2 + name: Very Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 15 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 1 + textureQuality: 1 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.3 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.4 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 16 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Medium + pixelLightCount: 1 + shadows: 1 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 0.7 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 64 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: High + pixelLightCount: 2 + shadows: 2 + shadowResolution: 1 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 40 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 256 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Very High + pixelLightCount: 3 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 70 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 4 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1.5 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 1024 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Ultra + pixelLightCount: 4 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 4 + shadowDistance: 150 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 4 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 2 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4096 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + m_PerPlatformDefaultQuality: + Android: 2 + Lumin: 5 + GameCoreScarlett: 5 + GameCoreXboxOne: 5 + Nintendo 3DS: 5 + Nintendo Switch: 5 + PS4: 5 + PS5: 5 + Stadia: 5 + Standalone: 5 + WebGL: 3 + Windows Store Apps: 5 + XboxOne: 5 + iPhone: 2 + tvOS: 2 diff --git a/FishDice/ProjectSettings/TagManager.asset b/FishDice/ProjectSettings/TagManager.asset new file mode 100644 index 0000000..1c92a78 --- /dev/null +++ b/FishDice/ProjectSettings/TagManager.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!78 &1 +TagManager: + serializedVersion: 2 + tags: [] + layers: + - Default + - TransparentFX + - Ignore Raycast + - + - Water + - UI + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + m_SortingLayers: + - name: Default + uniqueID: 0 + locked: 0 diff --git a/FishDice/ProjectSettings/TimeManager.asset b/FishDice/ProjectSettings/TimeManager.asset new file mode 100644 index 0000000..558a017 --- /dev/null +++ b/FishDice/ProjectSettings/TimeManager.asset @@ -0,0 +1,9 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!5 &1 +TimeManager: + m_ObjectHideFlags: 0 + Fixed Timestep: 0.02 + Maximum Allowed Timestep: 0.33333334 + m_TimeScale: 1 + Maximum Particle Timestep: 0.03 diff --git a/FishDice/ProjectSettings/UnityConnectSettings.asset b/FishDice/ProjectSettings/UnityConnectSettings.asset new file mode 100644 index 0000000..a27ab5f --- /dev/null +++ b/FishDice/ProjectSettings/UnityConnectSettings.asset @@ -0,0 +1,38 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!310 &1 +UnityConnectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 1 + m_Enabled: 0 + m_TestMode: 0 + m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events + m_EventUrl: https://cdp.cloud.unity3d.com/v1/events + m_ConfigUrl: https://config.uca.cloud.unity3d.com + m_DashboardUrl: https://dashboard.unity3d.com + m_CNEventUrl: https://cdp.cloud.unity.cn/v1/events + m_CNConfigUrl: https://cdp.cloud.unity.cn/config + m_TestInitMode: 0 + CrashReportingSettings: + m_EventUrl: https://perf-events.cloud.unity.cn + m_Enabled: 0 + m_LogBufferSize: 10 + m_CaptureEditorExceptions: 1 + UnityPurchasingSettings: + m_Enabled: 0 + m_TestMode: 0 + UnityAnalyticsSettings: + m_Enabled: 0 + m_TestMode: 0 + m_InitializeOnStartup: 1 + m_PackageRequiringCoreStatsPresent: 0 + UnityAdsSettings: + m_Enabled: 0 + m_InitializeOnStartup: 1 + m_TestMode: 0 + m_IosGameId: + m_AndroidGameId: + m_GameIds: {} + m_GameId: + PerformanceReportingSettings: + m_Enabled: 0 diff --git a/FishDice/ProjectSettings/VFXManager.asset b/FishDice/ProjectSettings/VFXManager.asset new file mode 100644 index 0000000..3a95c98 --- /dev/null +++ b/FishDice/ProjectSettings/VFXManager.asset @@ -0,0 +1,12 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!937362698 &1 +VFXManager: + m_ObjectHideFlags: 0 + m_IndirectShader: {fileID: 0} + m_CopyBufferShader: {fileID: 0} + m_SortShader: {fileID: 0} + m_StripUpdateShader: {fileID: 0} + m_RenderPipeSettingsPath: + m_FixedTimeStep: 0.016666668 + m_MaxDeltaTime: 0.05 diff --git a/FishDice/ProjectSettings/VersionControlSettings.asset b/FishDice/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 0000000..dca2881 --- /dev/null +++ b/FishDice/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!890905787 &1 +VersionControlSettings: + m_ObjectHideFlags: 0 + m_Mode: Visible Meta Files + m_CollabEditorSettings: + inProgressEnabled: 1 diff --git a/FishDice/ProjectSettings/XRSettings.asset b/FishDice/ProjectSettings/XRSettings.asset new file mode 100644 index 0000000..482590c --- /dev/null +++ b/FishDice/ProjectSettings/XRSettings.asset @@ -0,0 +1,10 @@ +{ + "m_SettingKeys": [ + "VR Device Disabled", + "VR Device User Alert" + ], + "m_SettingValues": [ + "False", + "False" + ] +} \ No newline at end of file