implemented skill system according to designdocuments
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Passive skill commentary outside the Yarn node tree.
|
||||
/// Fires at most one speaker per RequestCommentary call.
|
||||
/// </summary>
|
||||
public sealed class CommentarySystem : MonoBehaviour
|
||||
{
|
||||
[SerializeField] DialogueRunner dialogueRunner;
|
||||
[SerializeField] SkillDatabase database;
|
||||
[SerializeField] float minSecondsBetween = 20f;
|
||||
[SerializeField] int minRankToSpeak = 1;
|
||||
[SerializeField] int recencyWindow = 3;
|
||||
|
||||
readonly List<string> _recent = new List<string>();
|
||||
readonly HashSet<string> _firedContexts = new HashSet<string>(StringComparer.Ordinal);
|
||||
float _lastFiredAt = float.NegativeInfinity;
|
||||
|
||||
public void RequestCommentary(string contextId)
|
||||
{
|
||||
RequestCommentary(contextId, repeatable: false);
|
||||
}
|
||||
|
||||
public void RequestCommentary(string contextId, bool repeatable)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contextId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogueRunner == null || database == null)
|
||||
{
|
||||
Debug.LogWarning($"[{nameof(CommentarySystem)}] Missing DialogueRunner or SkillDatabase.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogueRunner.IsDialogueRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time.time - _lastFiredAt < minSecondsBetween)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repeatable && _firedContexts.Contains(contextId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var runtime = SkillRuntime.Instance;
|
||||
if (runtime == null || runtime.Skills == null || runtime.Rolls == null)
|
||||
{
|
||||
Debug.LogWarning($"[{nameof(CommentarySystem)}] SkillRuntime is not ready.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
var candidates = new List<SkillDefinition>();
|
||||
foreach (var skill in database.All)
|
||||
{
|
||||
if (skill == null || string.IsNullOrEmpty(skill.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (runtime.Skills.GetRank(skill.Id) < minRankToSpeak)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nodeName = $"Commentary_{contextId}_{skill.Id}";
|
||||
if (!NodeExists(nodeName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add(skill);
|
||||
}
|
||||
|
||||
var picked = Pick(candidates, runtime.Skills, _recent, recencyWindow, runtime.Rolls);
|
||||
if (picked == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var startNode = $"Commentary_{contextId}_{picked.Id}";
|
||||
_ = dialogueRunner.StartDialogue(startNode);
|
||||
_lastFiredAt = Time.time;
|
||||
_firedContexts.Add(contextId);
|
||||
_recent.Add(picked.Id);
|
||||
while (_recent.Count > Math.Max(1, recencyWindow))
|
||||
{
|
||||
_recent.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
bool NodeExists(string nodeName)
|
||||
{
|
||||
if (dialogueRunner.Dialogue != null && dialogueRunner.Dialogue.NodeExists(nodeName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var project = dialogueRunner.YarnProject;
|
||||
if (project != null)
|
||||
{
|
||||
foreach (var name in project.NodeNames)
|
||||
{
|
||||
if (string.Equals(name, nodeName, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Weighted pick: weight = max(1, rank); recent speakers (within recencyWindow) have weight halved.
|
||||
/// </summary>
|
||||
public static SkillDefinition Pick(
|
||||
IReadOnlyList<SkillDefinition> candidates,
|
||||
PlayerSkills skills,
|
||||
IReadOnlyList<string> recent,
|
||||
int recencyWindowSize,
|
||||
IRollSource rolls)
|
||||
{
|
||||
if (candidates == null || candidates.Count == 0 || skills == null || rolls == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var weights = new int[candidates.Count];
|
||||
var total = 0;
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
var skill = candidates[i];
|
||||
var weight = Math.Max(1, skills.GetRank(skill.Id));
|
||||
if (WasRecent(skill.Id, recent, recencyWindowSize))
|
||||
{
|
||||
weight = Math.Max(1, weight / 2);
|
||||
}
|
||||
|
||||
weights[i] = weight;
|
||||
total += weight;
|
||||
}
|
||||
|
||||
if (total <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// IRollSource returns 1..sides; map onto [0, total).
|
||||
var pick = rolls.Roll(total) - 1;
|
||||
var cumulative = 0;
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
cumulative += weights[i];
|
||||
if (pick < cumulative)
|
||||
{
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
|
||||
return candidates[candidates.Count - 1];
|
||||
}
|
||||
|
||||
static bool WasRecent(string skillId, IReadOnlyList<string> recent, int window)
|
||||
{
|
||||
if (recent == null || recent.Count == 0 || window <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var start = Math.Max(0, recent.Count - window);
|
||||
for (var i = start; i < recent.Count; i++)
|
||||
{
|
||||
if (string.Equals(recent[i], skillId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user