implemented skill system according to designdocuments

This commit is contained in:
2026-08-15 08:09:29 +02:00
parent f8def361fa
commit 063e94f6eb
95 changed files with 2924 additions and 4 deletions
@@ -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;
}
}
}