feat(lucky-dice): 提交 demo 前核心流程

This commit is contained in:
JSD\13999
2026-06-23 14:13:08 +08:00
commit 855659bf78
140 changed files with 9267 additions and 0 deletions

101
FishDice/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 5f266ba0b7dd4949b3e14abe82b5fc32
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 678f1a9001a34e9eb1a79bcc58b8f347
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"name": "FishDice.Runtime",
"rootNamespace": "FishDice",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": true
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ea1dba60786d4669be927ea3ec48f6f3
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 62d522d99c944966acf214919da3046a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 64a35bdcdd7447d49db30885a065cc3d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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() });
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: d2973688c08e47babf53588cd7d3e72c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
namespace FishDice.LuckyDice
{
public interface IRollActionExecutor
{
string ActionType { get; }
RollActionResult Execute(RollActionContext context, RollActionSpec action);
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a96e9c7c4cd442fa9e4f74d201c9cbdf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FishDice.LuckyDice
{
public sealed class RollActionDispatcher
{
private readonly Dictionary<string, IRollActionExecutor> _executors;
public RollActionDispatcher(IEnumerable<IRollActionExecutor> executors)
{
if (executors == null)
{
throw new ArgumentNullException(nameof(executors));
}
_executors = executors.ToDictionary(executor => executor.ActionType, executor => executor);
}
public IReadOnlyList<RollActionResult> Execute(
RollActionContext context,
IReadOnlyList<RollActionSpec> actions)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (actions == null)
{
return Array.Empty<RollActionResult>();
}
var results = new List<RollActionResult>(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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 771e68c81ca14d2fb5998ac049f9ca5a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace FishDice.LuckyDice
{
public static class RollActionExecutorRegistry
{
public static IReadOnlyList<IRollActionExecutor> CreateDefaultExecutors()
{
return new IRollActionExecutor[]
{
new GrantRewardExecutor(),
new AddMultiplierExecutor(),
new TriggerLuckyDiceExecutor()
};
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6bc162f33bbf4660b1599dce107dc223
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: d1eeebb9f2ae47058ce0524e8539b8cc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f1f14033988649cebb5429c594a53de9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FishDice.LuckyDice
{
public static class ComboRuleDefinitionLoader
{
public static IReadOnlyList<ComboRule> Load(IReadOnlyList<ComboRuleDefinition> definitions)
{
if (definitions == null)
{
return Array.Empty<ComboRule>();
}
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<RollActionSpec> ToRuntimeActions(IReadOnlyList<RollActionDefinition> definitions)
{
if (definitions == null)
{
return Array.Empty<RollActionSpec>();
}
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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: d8b93de4100240c792be3c214c06218c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6f07727f4e84a88a31eb2d4caec3dc6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 56d5d29378624c30b6510846b4fd45e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<string> sourceFilter,
bool isDefault)
{
ResultKey = resultKey;
TargetKey = targetKey;
ResultSlotCount = resultSlotCount;
Weight = weight;
Enabled = enabled;
MinLevel = minLevel;
SourceFilter = sourceFilter == null
? Array.Empty<string>()
: new List<string>(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<string> 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; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d22247243d584a8fa28b45e708517c1d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 98f58d378176481380ec6413753c5717
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FishDice.LuckyDice
{
public sealed class LuckyDiceConfigValidationContext
{
public LuckyDiceConfigValidationContext(
IReadOnlyList<IComboMatcher> matchers,
IReadOnlyList<IRollActionExecutor> actionExecutors)
{
Matchers = matchers == null ? Array.Empty<IComboMatcher>() : matchers.ToArray();
ActionExecutors = actionExecutors == null ? Array.Empty<IRollActionExecutor>() : actionExecutors.ToArray();
}
public IReadOnlyList<IComboMatcher> Matchers { get; }
public IReadOnlyList<IRollActionExecutor> ActionExecutors { get; }
public static LuckyDiceConfigValidationContext CreateDefault()
{
return new LuckyDiceConfigValidationContext(
ComboMatcherRegistry.CreateDefaultMatchers(),
RollActionExecutorRegistry.CreateDefaultExecutors());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2bd13dcfd4a54d9fa90b11b60df1a7e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<ComboRuleDefinition> rules,
IReadOnlyList<LuckyDiceCandidateDefinition> candidates,
IReadOnlyList<TargetModeDefinition> targetModes,
IReadOnlyList<IComboMatcher> matchers,
IReadOnlyList<IRollActionExecutor> actionExecutors)
{
var errors = new List<ConfigValidationError>();
var matcherTypes = ValidateMatchers(errors, matchers);
var actionTypes = ValidateActionExecutors(errors, actionExecutors);
var ruleIds = new HashSet<string>();
foreach (var rule in rules ?? Array.Empty<ComboRuleDefinition>())
{
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<RollActionDefinition>())
{
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<LuckyDiceCandidateDefinition>()).All(candidate => candidate == null || !candidate.IsDefault))
{
errors.Add(new ConfigValidationError("MissingDefaultCandidate", "Lucky Dice needs one default candidate."));
}
var targetModeKeys = new HashSet<string>();
foreach (var targetMode in targetModes ?? Array.Empty<TargetModeDefinition>())
{
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<string>();
foreach (var candidate in candidates ?? Array.Empty<LuckyDiceCandidateDefinition>())
{
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<TargetModeDefinition>())
.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<string> ValidateMatchers(
List<ConfigValidationError> errors,
IReadOnlyList<IComboMatcher> matchers)
{
var matcherTypes = new HashSet<string>();
foreach (var matcher in matchers ?? Array.Empty<IComboMatcher>())
{
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<string> ValidateActionExecutors(
List<ConfigValidationError> errors,
IReadOnlyList<IRollActionExecutor> actionExecutors)
{
var actionTypes = new HashSet<string>();
foreach (var executor in actionExecutors ?? Array.Empty<IRollActionExecutor>())
{
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<ConfigValidationError> errors,
IReadOnlyList<ActionParamDefinition> actionParams)
{
var paramKeys = new HashSet<string>();
foreach (var param in actionParams ?? Array.Empty<ActionParamDefinition>())
{
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<ConfigValidationError> errors)
{
Errors = errors == null ? Array.Empty<ConfigValidationError>() : errors.ToArray();
}
public IReadOnlyList<ConfigValidationError> 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; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 31bec0fb5e6c45ddadf3a0039a287423
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,238 @@
using System.Collections.Generic;
namespace FishDice.LuckyDice
{
public sealed class LuckyDiceDefaultConfig
{
private readonly Dictionary<string, DiceSetConfig> _diceSets;
private LuckyDiceDefaultConfig(
Dictionary<string, DiceSetConfig> diceSets,
IReadOnlyList<ComboRule> comboRules,
IReadOnlyList<LuckyDiceCandidateConfig> luckyDiceCandidates,
IReadOnlyList<TargetModeConfig> targetModeDefinitions)
{
_diceSets = diceSets;
ComboRules = comboRules;
LuckyDiceCandidates = luckyDiceCandidates;
TargetModeDefinitions = targetModeDefinitions;
}
public IReadOnlyList<ComboRule> ComboRules { get; }
public IReadOnlyList<LuckyDiceCandidateConfig> LuckyDiceCandidates { get; }
public IReadOnlyList<TargetModeConfig> 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<ComboRuleDefinition> ruleDefinitions,
IReadOnlyList<LuckyDiceCandidateDefinition> candidateDefinitions,
IReadOnlyList<TargetModeDefinition> targetModeDefinitions)
{
return CreateValidated(
ruleDefinitions,
candidateDefinitions,
targetModeDefinitions,
LuckyDiceConfigValidationContext.CreateDefault());
}
public static LuckyDiceDefaultConfig CreateValidated(
IReadOnlyList<ComboRuleDefinition> ruleDefinitions,
IReadOnlyList<LuckyDiceCandidateDefinition> candidateDefinitions,
IReadOnlyList<TargetModeDefinition> 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<string, DiceSetConfig>
{
{ normalSet.DiceSetId, normalSet },
{ luckySet.DiceSetId, luckySet }
},
ComboRuleDefinitionLoader.Load(ruleDefinitions),
ToCandidateConfigs(candidateDefinitions),
ToTargetModeConfigs(targetModeDefinitions));
}
private static LuckyDiceCandidateConfig[] ToCandidateConfigs(IReadOnlyList<LuckyDiceCandidateDefinition> 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<TargetModeDefinition> 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"
}
};
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2872fbab847848f9bf968e717a4acd70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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";
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 3a0439dddf734a14a1f49eeae48e349b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 2867533dcede4ee9b8222a2b4d63deab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<LuckyDiceRequestedEffect>().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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 1934c9ee9d2f40d897afde268a169778
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<ComboRuleDefinition> ruleDefinitions,
IReadOnlyList<LuckyDiceCandidateDefinition> candidateDefinitions,
IReadOnlyList<TargetModeDefinition> 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());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19d0cc9a1e284be7a23c2ee1c6b7bd21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 1ff1f71d729f48438f1a81077076ca5a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<DiceFace> faces,
IReadOnlyDictionary<string, int> faceCounts,
int numberSum,
IReadOnlyList<int> 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<DiceFace> Faces { get; }
public IReadOnlyDictionary<string, int> FaceCounts { get; }
public int NumberSum { get; }
public IReadOnlyList<int> NumberValues { get; }
public int CloverCount { get; }
public bool HasClover { get; }
public string NormalizedKey { get; }
public string OrderedKey { get; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 74faceaa58a144008abe8f08f3b3db27
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 11a380073489461c997f9ce85c45d607
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<RollActionSpec> actions,
bool stopAfterMatched)
{
RuleId = ruleId ?? throw new ArgumentNullException(nameof(ruleId));
Priority = priority;
Matcher = matcher ?? throw new ArgumentNullException(nameof(matcher));
Actions = actions == null ? Array.Empty<RollActionSpec>() : actions.ToArray();
StopAfterMatched = stopAfterMatched;
}
public string RuleId { get; }
public int Priority { get; }
public ComboMatcherSpec Matcher { get; }
public IReadOnlyList<RollActionSpec> Actions { get; }
public bool StopAfterMatched { get; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a94f5fa117fb4bada6467d3d7d0c31d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<RollActionSpec> Actions { get; }
}
public sealed class ComboRuleMatchResult
{
public ComboRuleMatchResult(IReadOnlyList<ComboRuleMatch> matches)
{
Matches = matches == null ? Array.Empty<ComboRuleMatch>() : matches.ToArray();
}
public IReadOnlyList<ComboRuleMatch> Matches { get; }
public bool HasMatch => Matches.Count > 0;
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: d9df28fce97b4d4fa5cf29a5b6f38d23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9175dd08e215408faa01a3ee1cc86a13
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<DiceFace> 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<DiceFace> Faces { get; }
public int Multiplier { get; }
public string TriggerSource { get; }
public static DiceRollResult Create(
string rollSessionId,
string diceSetId,
DiceSetType setType,
IReadOnlyList<DiceFace> 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<DiceFace> faces,
int multiplier,
string triggerSource)
{
return new DiceRollResult(
rollSessionId,
config.DiceSetId,
config.SetType,
config.DiceCount,
faces,
multiplier,
triggerSource);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 839ecc30eed6498d93ce34d9ffe847eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<DiceFace> 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<DiceFace> Faces { get; }
public int? DefaultResultSlotCount { get; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6c890293bb854609a2b916d1d35e6063
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
namespace FishDice.LuckyDice
{
public enum DiceSetType
{
Normal,
Lucky
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 0147bf70503045fbb65baec35ba74936
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2e21edd33e2d469d9736fa7ce87409f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8b28add99b6540b19611b2e1ad429bae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<RollEffect> effects,
string outcome,
string error)
{
ActionType = actionType;
Success = success;
Effects = effects == null ? Array.Empty<RollEffect>() : effects.ToArray();
Outcome = outcome;
Error = error;
}
public string ActionType { get; }
public bool Success { get; }
public IReadOnlyList<RollEffect> 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<RollEffect>(), outcome, null);
}
public static RollActionResult Succeeded(string actionType, IReadOnlyList<RollEffect> 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<RollEffect>(), null, error);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 218cb0b26b39475fb0b5eff1b7f939fd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<string, string> parameters)
{
ActionType = actionType ?? throw new ArgumentNullException(nameof(actionType));
Params = parameters == null
? new Dictionary<string, string>()
: parameters.ToDictionary(pair => pair.Key, pair => pair.Value);
}
public string ActionType { get; }
public IReadOnlyDictionary<string, string> Params { get; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6f8ccfed08184b1b9f553c455e747c10
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 5524489d53e74a4c88a00bba1de976b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2cb236f5ac214d77aae6c15c3c8429d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<ComboRuleMatch> matchedRules,
IReadOnlyList<RollActionResult> actionResults,
IReadOnlyList<RollEffect> effects,
LuckyDiceSelectionResult luckyDiceResult,
TargetModeEntry targetMode,
RollTrace trace)
{
RollSessionId = rollSessionId;
RollResult = rollResult;
ComboFacts = comboFacts;
MatchedRules = matchedRules == null ? Array.Empty<ComboRuleMatch>() : matchedRules.ToArray();
ActionResults = actionResults == null ? Array.Empty<RollActionResult>() : actionResults.ToArray();
Effects = effects == null ? Array.Empty<RollEffect>() : effects.ToArray();
LuckyDiceResult = luckyDiceResult;
TargetMode = targetMode;
Trace = trace;
}
public string RollSessionId { get; }
public DiceRollResult RollResult { get; }
public ComboFacts ComboFacts { get; }
public IReadOnlyList<ComboRuleMatch> MatchedRules { get; }
public IReadOnlyList<RollActionResult> ActionResults { get; }
public IReadOnlyList<RollEffect> Effects { get; }
public LuckyDiceSelectionResult LuckyDiceResult { get; }
public TargetModeEntry TargetMode { get; }
public RollTrace Trace { get; }
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: ebb54a7e748c4b838f46a5d51e00e89f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 92ce07ad741540bbbaee1c4c8b349ad7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<LuckyDiceCandidateConfig> candidates,
RollFlowRequest request,
DiceSetConfig luckyDiceSetConfig)
{
var source = candidates ?? Array.Empty<LuckyDiceCandidateConfig>();
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<LuckyDiceCandidateConfig> 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];
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: e6956f55ed064f2d872f52a9c9eba554
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: ac0d6f0f00124234809917e89e30eba3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FishDice.LuckyDice
{
public sealed class ComboRuleMatcher
{
private readonly IReadOnlyDictionary<string, IComboMatcher> _matchers;
public ComboRuleMatcher()
: this(ComboMatcherRegistry.CreateDefaultMatchers())
{
}
public ComboRuleMatcher(IEnumerable<IComboMatcher> matchers)
{
if (matchers == null)
{
throw new ArgumentNullException(nameof(matchers));
}
_matchers = matchers.ToDictionary(matcher => matcher.MatcherType, matcher => matcher);
}
public ComboRuleMatchResult Match(ComboFacts facts, IReadOnlyList<ComboRule> rules)
{
if (facts == null)
{
throw new ArgumentNullException(nameof(facts));
}
if (rules == null)
{
throw new ArgumentNullException(nameof(rules));
}
var matches = new List<ComboRuleMatch>();
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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 5ffdaec353584cbf8006d846816af810
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: f02fda27051d4ff8b44d459bd69c176b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace FishDice.LuckyDice
{
public static class ComboMatcherRegistry
{
public static IReadOnlyList<IComboMatcher> CreateDefaultMatchers()
{
return new IComboMatcher[]
{
new ExactComboMatcher(),
new AllNumbersMatcher(),
new ContainsFaceMatcher(),
new FaceCountMatcher(),
new FaceCountRangeMatcher(),
new DiceCountMatcher(),
new NumberPairMatcher(),
new NumberOfAKindMatcher(),
new NumberSumRangeMatcher()
};
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 024047cd3f684a5a813d1abe30fc350a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 670344b8b7c74696baef9fe7a61ffc34
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
namespace FishDice.LuckyDice
{
public interface IComboMatcher
{
string MatcherType { get; }
bool IsMatch(ComboFacts facts, ComboMatcherSpec spec);
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 5252627bba5a463bb7006295c672a164
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 32a9a61586304026ad6aabe05b1316c2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: fb0a92fa21434dc6b216eaf039c8eb86
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<DiceFace>(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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8a9d95f67cf84b62b960aae1b964e9db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 48e76359cf424f43b578f3098179d64f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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<TargetModeConfig> definitions)
{
if (selected == null)
{
return TargetModeResolveResult.Failed("MissingLuckyDiceCandidate");
}
var definition = (definitions ?? Array.Empty<TargetModeConfig>())
.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));
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: c8560c502d0b42cd856b23761cf7ea4b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: d407eb4e046b45ff99f112e6646ab1d0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,121 @@
using System.Collections.Generic;
using System.Linq;
namespace FishDice.LuckyDice
{
public sealed class RollTrace
{
private readonly List<string> _executedActions = new List<string>();
private readonly List<ComboRuleMatch> _matchedRules = new List<ComboRuleMatch>();
private readonly List<RollActionResult> _actionResults = new List<RollActionResult>();
public RollTrace(string rollSessionId)
{
RollSessionId = rollSessionId;
}
public string RollSessionId { get; }
public IReadOnlyList<string> 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<ComboRuleMatch> MatchedRules => _matchedRules;
public IReadOnlyList<string> ExecutedActions => _executedActions;
public IReadOnlyList<RollActionResult> 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<string> 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";
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: d2c57c4ac2f54a638326e104f5afac01
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 243feaaf2c05440ebb1fff35768a653c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 020efc0ae0324888bb93be8b772cd5fd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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"
]
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c807bf8363384e5a8ffb7ccd72e79729
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More