implemented skill system according to designdocuments
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
public interface IRollSource
|
||||
{
|
||||
/// <summary>Returns a value in [1, sides] inclusive.</summary>
|
||||
int Roll(int sides);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43de4384f09d4bd7b142d105f2c04007
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure rank state. No rolling, DC comparison, modifier math, UI, or Yarn.
|
||||
/// </summary>
|
||||
public sealed class PlayerSkills
|
||||
{
|
||||
readonly Dictionary<string, int> _ranks =
|
||||
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public PlayerSkills(SkillDatabase db)
|
||||
{
|
||||
if (db == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(db));
|
||||
}
|
||||
|
||||
foreach (var skill in db.All)
|
||||
{
|
||||
if (skill == null || string.IsNullOrEmpty(skill.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ranks[skill.Id] = skill.StartingRank;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, int> AllRanks => _ranks;
|
||||
|
||||
public int GetRank(string skillId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(skillId))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _ranks.TryGetValue(skillId, out var rank) ? rank : 0;
|
||||
}
|
||||
|
||||
public void SetRank(string skillId, int rank)
|
||||
{
|
||||
if (string.IsNullOrEmpty(skillId))
|
||||
{
|
||||
throw new ArgumentException("Skill id is required.", nameof(skillId));
|
||||
}
|
||||
|
||||
_ranks[skillId] = rank;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ace1f9c8cfc64ff9aa040d60495b6115
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
public sealed class SeededRollSource : IRollSource
|
||||
{
|
||||
readonly Random _random;
|
||||
|
||||
public SeededRollSource(int seed)
|
||||
{
|
||||
_random = new Random(seed);
|
||||
}
|
||||
|
||||
public int Roll(int sides)
|
||||
{
|
||||
if (sides < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(sides));
|
||||
}
|
||||
|
||||
return _random.Next(1, sides + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e89e99b02ed453db3efbd72ec8d038a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure check math. Exactly three modifier sources: rank, situational, state.
|
||||
/// No UnityEngine types. No side effects on PlayerSkills / VariableStorage / UI.
|
||||
/// </summary>
|
||||
public sealed class SkillCheckSystem
|
||||
{
|
||||
readonly SkillDatabase _database;
|
||||
readonly PlayerSkills _skills;
|
||||
readonly SkillStateModifiers _state;
|
||||
readonly IRollSource _rolls;
|
||||
|
||||
public SkillCheckSystem(
|
||||
SkillDatabase database,
|
||||
PlayerSkills skills,
|
||||
SkillStateModifiers state,
|
||||
IRollSource rolls)
|
||||
{
|
||||
_database = database ?? throw new ArgumentNullException(nameof(database));
|
||||
_skills = skills ?? throw new ArgumentNullException(nameof(skills));
|
||||
_state = state ?? throw new ArgumentNullException(nameof(state));
|
||||
_rolls = rolls ?? throw new ArgumentNullException(nameof(rolls));
|
||||
}
|
||||
|
||||
public SkillCheckResult Resolve(string skillId, int dc, int situationalMod)
|
||||
{
|
||||
skillId ??= string.Empty;
|
||||
|
||||
string displayName;
|
||||
int rankMod;
|
||||
|
||||
if (_database.TryGet(skillId, out var def) && def != null)
|
||||
{
|
||||
displayName = string.IsNullOrEmpty(def.DisplayName)
|
||||
? def.Id.ToUpperInvariant()
|
||||
: def.DisplayName;
|
||||
rankMod = _skills.GetRank(skillId);
|
||||
}
|
||||
else
|
||||
{
|
||||
displayName = skillId.ToUpperInvariant();
|
||||
rankMod = 0;
|
||||
}
|
||||
|
||||
var roll = _rolls.Roll(20);
|
||||
var stateMod = _state.TotalFor(skillId);
|
||||
var total = roll + rankMod + situationalMod + stateMod;
|
||||
|
||||
bool success;
|
||||
SkillCheckDegree degree;
|
||||
|
||||
if (roll == 1)
|
||||
{
|
||||
success = false;
|
||||
degree = SkillCheckDegree.CriticalFailure;
|
||||
}
|
||||
else if (roll == 20)
|
||||
{
|
||||
success = true;
|
||||
degree = SkillCheckDegree.CriticalSuccess;
|
||||
}
|
||||
else
|
||||
{
|
||||
success = total >= dc;
|
||||
degree = success ? SkillCheckDegree.Success : SkillCheckDegree.Failure;
|
||||
}
|
||||
|
||||
return new SkillCheckResult(
|
||||
skillId,
|
||||
displayName,
|
||||
roll,
|
||||
rankMod,
|
||||
situationalMod,
|
||||
stateMod,
|
||||
total,
|
||||
dc,
|
||||
success,
|
||||
degree);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83a2adbf493a4d6eb4f5911582cc15ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,75 @@
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Composition root and singleton that static Yarn commands route through.
|
||||
/// </summary>
|
||||
public sealed class SkillRuntime : MonoBehaviour
|
||||
{
|
||||
public static SkillRuntime Instance { get; private set; }
|
||||
|
||||
[SerializeField] SkillDatabase database;
|
||||
[SerializeField] DialogueRunner dialogueRunner;
|
||||
[SerializeField] SkillCheckResultView resultView;
|
||||
[SerializeField] CommentarySystem commentary;
|
||||
[SerializeField] int rollSeed;
|
||||
[SerializeField] string debugContextId = "Entrance_Queue";
|
||||
|
||||
public SkillDatabase Database => database;
|
||||
public DialogueRunner DialogueRunner => dialogueRunner;
|
||||
public SkillCheckResultView ResultView => resultView;
|
||||
public CommentarySystem Commentary => commentary;
|
||||
public PlayerSkills Skills { get; private set; }
|
||||
public SkillStateModifiers States { get; private set; }
|
||||
public SkillCheckSystem Checks { get; private set; }
|
||||
public IRollSource Rolls { get; private set; }
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError($"[{nameof(SkillRuntime)}] Duplicate instance on '{name}'. Destroying this component.", this);
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
|
||||
if (database == null)
|
||||
{
|
||||
Debug.LogError($"[{nameof(SkillRuntime)}] SkillDatabase is not assigned.", this);
|
||||
}
|
||||
|
||||
Skills = database != null ? new PlayerSkills(database) : null;
|
||||
States = new SkillStateModifiers();
|
||||
Rolls = rollSeed != 0 ? (IRollSource)new SeededRollSource(rollSeed) : new UnityRollSource();
|
||||
|
||||
if (Skills != null)
|
||||
{
|
||||
Checks = new SkillCheckSystem(database, Skills, States, Rolls);
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Fire Commentary (debug)")]
|
||||
void FireCommentaryDebug()
|
||||
{
|
||||
if (commentary == null)
|
||||
{
|
||||
Debug.LogWarning($"[{nameof(SkillRuntime)}] No CommentarySystem assigned.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
commentary.RequestCommentary(debugContextId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb04a8b1bce645fba398b22bbb4db58a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Transient state modifiers (third of three modifier sources). Pure state — no check math.
|
||||
/// Use skillId "*" to apply to every skill.
|
||||
/// </summary>
|
||||
public sealed class SkillStateModifiers
|
||||
{
|
||||
readonly Dictionary<string, Dictionary<string, int>> _bySource =
|
||||
new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal);
|
||||
|
||||
public void Set(string sourceKey, string skillId, int delta)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
throw new ArgumentException("Source key is required.", nameof(sourceKey));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(skillId))
|
||||
{
|
||||
throw new ArgumentException("Skill id is required.", nameof(skillId));
|
||||
}
|
||||
|
||||
if (!_bySource.TryGetValue(sourceKey, out var map))
|
||||
{
|
||||
map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
_bySource[sourceKey] = map;
|
||||
}
|
||||
|
||||
map[skillId] = delta;
|
||||
}
|
||||
|
||||
public void Clear(string sourceKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bySource.Remove(sourceKey);
|
||||
}
|
||||
|
||||
public int TotalFor(string skillId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(skillId))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var total = 0;
|
||||
foreach (var map in _bySource.Values)
|
||||
{
|
||||
if (map.TryGetValue(skillId, out var specific))
|
||||
{
|
||||
total += specific;
|
||||
}
|
||||
|
||||
if (map.TryGetValue("*", out var wildcard))
|
||||
{
|
||||
total += wildcard;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02f6b5bf30ad4f8db0711ed25b083247
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
public sealed class UnityRollSource : IRollSource
|
||||
{
|
||||
public int Roll(int sides)
|
||||
{
|
||||
if (sides < 1)
|
||||
{
|
||||
throw new System.ArgumentOutOfRangeException(nameof(sides));
|
||||
}
|
||||
|
||||
return Random.Range(1, sides + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54552e499883406b9118fd2ba28e08ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user