refactor the skill system to adhere to the lore handbook
This commit is contained in:
@@ -14,7 +14,7 @@ Bouncer: Evening. Name on the list?
|
||||
You: Depends who's asking.
|
||||
-> I'm on the list.
|
||||
<<set $claimed_to_be_on_list = true>>
|
||||
<<check persuasion standard>>
|
||||
<<check placement routine>>
|
||||
<<if $check_result>>
|
||||
Bouncer: ...you're not on the list.
|
||||
Bouncer: But it's slow tonight. Go on.
|
||||
@@ -23,8 +23,8 @@ You: Depends who's asking.
|
||||
Bouncer: Nobody's on the list tonight. Especially not you.
|
||||
<<set $went_inside = false>>
|
||||
<<endif>>
|
||||
-> Let me through. <<if skill_rank("intimidation") >= 2>>
|
||||
<<check intimidation hard -1>>
|
||||
-> Let me through. <<if skill_rank("undisclosed") >= 2>>
|
||||
<<check undisclosed hard -1>>
|
||||
<<if $check_degree == "critical_success">>
|
||||
Bouncer: ...yeah. Yeah, okay.
|
||||
<<set $reputation += 2>>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
// Passive skill commentary. Fired by CommentarySystem, never <<jump>>ed to.
|
||||
// One node per (context, skill) pair; if a skill has nothing to say about a
|
||||
// context, simply omit the node and it won't be considered.
|
||||
//
|
||||
// Writing rules (code does not enforce these):
|
||||
// - Wrong Truth failures: no failure signal in the prose; the UI banner is the only tell.
|
||||
// - Documentary routes are gated on Act and errand state, never on a voice.
|
||||
|
||||
title: Commentary_Entrance_Queue_paranoia
|
||||
title: Commentary_Entrance_Queue_facework
|
||||
---
|
||||
Paranoia: The man three back hasn't looked at his phone once. Nobody waits like that.
|
||||
FACEWORK: His hands are too still for someone this far back in a queue. What's he practising not to reach for?
|
||||
===
|
||||
|
||||
title: Commentary_Entrance_Queue_read_room
|
||||
title: Commentary_Entrance_Queue_long_shift
|
||||
---
|
||||
Read the Room: Everyone here is pretending they're not cold.
|
||||
THE LONG SHIFT: Forty minutes. Nobody's shifted their weight the way cold people do. They're paying for this wait in something else.
|
||||
===
|
||||
|
||||
title: Commentary_Entrance_Queue_streetwise
|
||||
title: Commentary_Entrance_Queue_the_float
|
||||
---
|
||||
Streetwise: Door's been "slow tonight" for forty minutes. That's a price, not a queue.
|
||||
THE FLOAT: Door's been "slow tonight" for forty minutes. Convert that: it's a price, not a queue. Who's eating that?
|
||||
===
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
// This node is never played. Declaring every variable in one place means the
|
||||
// compiler catches typos ("$reputaton") instead of silently creating a second
|
||||
// variable at runtime.
|
||||
//
|
||||
// Writing rules the code deliberately does not enforce (Skill Bible §7 / refactor plan §2.1):
|
||||
// - Wrong Truth failures carry no failure signal in the prose — the UI banner is the only tell.
|
||||
// - Documentary routes are gated on Act and errand state, never on a voice.
|
||||
//
|
||||
// Modifier Yarn commands (see YarnSkillCommands):
|
||||
// <<skill_mod scene facework 2>> — source key "scene" = situational; clear on scene exit
|
||||
// <<clear_skill_mods scene>>
|
||||
// Other source keys (drunk, injured, …) are state buckets.
|
||||
|
||||
title: Declarations
|
||||
---
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
title: Debug_SkillCheck
|
||||
---
|
||||
Narrator: Skill check harness.
|
||||
<<check persuasion standard>>
|
||||
<<check placement routine>>
|
||||
<<if $check_result>>
|
||||
Narrator: Passed. Rolled {$check_roll}, total {$check_total} against DC {$check_dc}.
|
||||
<<else>>
|
||||
@@ -13,10 +13,10 @@ Narrator: Skill check harness.
|
||||
<<endif>>
|
||||
|
||||
// Explicit numeric DC plus a situational modifier.
|
||||
<<check deduction 16 2>>
|
||||
Narrator: Deduction came out {$check_degree}.
|
||||
<<check confluence 16 2>>
|
||||
Narrator: Confluence came out {$check_degree}.
|
||||
|
||||
// Deliberate typo: should log an error and continue, not deadlock.
|
||||
<<check nosuchskill standard>>
|
||||
<<check nosuchskill routine>>
|
||||
Narrator: Survived the bad skill id.
|
||||
===
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NightclubArcadia.Skills;
|
||||
@@ -10,43 +11,21 @@ using UnityEngine.UI;
|
||||
using Yarn.Unity;
|
||||
|
||||
/// <summary>
|
||||
/// Creates SkillDefinition assets, SkillDatabase, and wires SkillRuntime + roll
|
||||
/// banner + commentary into Assets/Scenes/DialogueTest.unity.
|
||||
/// Regenerates SkillDefinition assets + SkillDatabase from the Skill Bible JSON mirror.
|
||||
///
|
||||
/// Source of truth for prose/roster: the Obsidian Skill System Draft.md.
|
||||
/// Checked-in machine mirror: Assets/Skills/skill_bible.json (edit that, not the .assets).
|
||||
/// Hand-edits to Definitions/*.asset are overwritten on script reload by EnsureSkillAssetsOnly.
|
||||
/// </summary>
|
||||
public static class SkillSystemSetup
|
||||
{
|
||||
const string DefinitionsFolder = "Assets/Skills/Definitions";
|
||||
const string SkillsFolder = "Assets/Skills";
|
||||
const string BiblePath = "Assets/Skills/skill_bible.json";
|
||||
const string DatabasePath = "Assets/Skills/SkillDatabase.asset";
|
||||
const string ScenePath = "Assets/Scenes/DialogueTest.unity";
|
||||
const string AutoSetupSessionKey = "NightclubArcadia.SkillSystem.AutoSetupDone";
|
||||
|
||||
struct SkillSpec
|
||||
{
|
||||
public string Id;
|
||||
public string DisplayName;
|
||||
public SkillDomain Domain;
|
||||
public Color Accent;
|
||||
public int StartingRank;
|
||||
}
|
||||
|
||||
static readonly SkillSpec[] Roster =
|
||||
{
|
||||
new SkillSpec { Id = "deduction", DisplayName = "Deduction", Domain = SkillDomain.Reason, Accent = new Color(0.55f, 0.75f, 1f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "streetwise", DisplayName = "Streetwise", Domain = SkillDomain.Reason, Accent = new Color(0.7f, 0.85f, 0.55f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "recall", DisplayName = "Recall", Domain = SkillDomain.Reason, Accent = new Color(0.65f, 0.7f, 1f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "read_room", DisplayName = "Read the Room", Domain = SkillDomain.Instinct, Accent = new Color(1f, 0.75f, 0.45f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "paranoia", DisplayName = "Paranoia", Domain = SkillDomain.Instinct, Accent = new Color(1f, 0.45f, 0.55f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "reflexes", DisplayName = "Reflexes", Domain = SkillDomain.Instinct, Accent = new Color(1f, 0.9f, 0.4f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "persuasion", DisplayName = "Persuasion", Domain = SkillDomain.Presence, Accent = new Color(0.85f, 0.65f, 1f), StartingRank = 1 },
|
||||
// Rank 2 so the Bouncer "Let me through" option gated by skill_rank >= 2 is visible in the demo.
|
||||
new SkillSpec { Id = "intimidation", DisplayName = "Intimidation", Domain = SkillDomain.Presence, Accent = new Color(1f, 0.4f, 0.4f), StartingRank = 2 },
|
||||
new SkillSpec { Id = "rapport", DisplayName = "Rapport", Domain = SkillDomain.Presence, Accent = new Color(0.55f, 0.95f, 0.85f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "endurance", DisplayName = "Endurance", Domain = SkillDomain.Body, Accent = new Color(0.85f, 0.7f, 0.5f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "sleight_of_hand", DisplayName = "Sleight of Hand", Domain = SkillDomain.Body, Accent = new Color(0.7f, 0.9f, 0.95f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "nerve", DisplayName = "Nerve", Domain = SkillDomain.Body, Accent = new Color(0.95f, 0.6f, 0.35f), StartingRank = 1 },
|
||||
};
|
||||
|
||||
[DidReloadScripts]
|
||||
static void AutoSetupAfterReload()
|
||||
{
|
||||
@@ -80,15 +59,24 @@ public static class SkillSystemSetup
|
||||
/// </summary>
|
||||
static void EnsureSkillAssetsOnly()
|
||||
{
|
||||
if (!TryLoadRoster(out var roster))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureFolder("Assets", "Skills");
|
||||
EnsureFolder("Assets/Skills", "Definitions");
|
||||
|
||||
var definitions = new List<SkillDefinition>();
|
||||
foreach (var spec in Roster)
|
||||
var rosterIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var entry in roster)
|
||||
{
|
||||
definitions.Add(CreateOrUpdateDefinition(spec));
|
||||
rosterIds.Add(entry.id);
|
||||
definitions.Add(CreateOrUpdateDefinition(entry));
|
||||
}
|
||||
|
||||
DeleteOrphanDefinitions(rosterIds);
|
||||
|
||||
var database = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
|
||||
if (database == null)
|
||||
{
|
||||
@@ -105,15 +93,24 @@ public static class SkillSystemSetup
|
||||
[MenuItem("Tools/Nightclub Arcadia/Set Up Skill System")]
|
||||
public static void SetUpSkillSystem()
|
||||
{
|
||||
if (!TryLoadRoster(out var roster))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureFolder("Assets", "Skills");
|
||||
EnsureFolder("Assets/Skills", "Definitions");
|
||||
|
||||
var definitions = new List<SkillDefinition>();
|
||||
foreach (var spec in Roster)
|
||||
var rosterIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var entry in roster)
|
||||
{
|
||||
definitions.Add(CreateOrUpdateDefinition(spec));
|
||||
rosterIds.Add(entry.id);
|
||||
definitions.Add(CreateOrUpdateDefinition(entry));
|
||||
}
|
||||
|
||||
DeleteOrphanDefinitions(rosterIds);
|
||||
|
||||
var database = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
|
||||
if (database == null)
|
||||
{
|
||||
@@ -130,9 +127,75 @@ public static class SkillSystemSetup
|
||||
Debug.Log($"Skill system setup complete. Database at {DatabasePath}; scene wired at {ScenePath}.");
|
||||
}
|
||||
|
||||
static SkillDefinition CreateOrUpdateDefinition(SkillSpec spec)
|
||||
static bool TryLoadRoster(out SkillBibleEntry[] roster)
|
||||
{
|
||||
var path = $"{DefinitionsFolder}/{spec.Id}.asset";
|
||||
roster = null;
|
||||
var text = AssetDatabase.LoadAssetAtPath<TextAsset>(BiblePath);
|
||||
if (text == null)
|
||||
{
|
||||
Debug.LogError($"[SkillSystemSetup] Missing bible at {BiblePath}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
SkillBibleRoot root;
|
||||
try
|
||||
{
|
||||
root = JsonUtility.FromJson<SkillBibleRoot>(text.text);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SkillSystemSetup] Failed to parse {BiblePath}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root?.skills == null || root.skills.Length == 0)
|
||||
{
|
||||
Debug.LogError($"[SkillSystemSetup] {BiblePath} has no skills.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var entry in root.skills)
|
||||
{
|
||||
if (entry == null || string.IsNullOrEmpty(entry.id))
|
||||
{
|
||||
Debug.LogError($"[SkillSystemSetup] {BiblePath} contains a skill with empty id.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fail loudly on enum typos — silent defaults are content bugs.
|
||||
if (!Enum.TryParse<SkillAxis>(entry.axis, ignoreCase: true, out _))
|
||||
{
|
||||
Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid axis '{entry.axis}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<TraceChannel>(entry.primaryChannel, ignoreCase: true, out _))
|
||||
{
|
||||
Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid primaryChannel '{entry.primaryChannel}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var secondary = string.IsNullOrEmpty(entry.secondaryChannel) ? "None" : entry.secondaryChannel;
|
||||
if (!Enum.TryParse<TraceChannel>(secondary, ignoreCase: true, out _))
|
||||
{
|
||||
Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid secondaryChannel '{entry.secondaryChannel}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<ClueYield>(entry.clueYield, ignoreCase: true, out _))
|
||||
{
|
||||
Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid clueYield '{entry.clueYield}'.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
roster = root.skills;
|
||||
return true;
|
||||
}
|
||||
|
||||
static SkillDefinition CreateOrUpdateDefinition(SkillBibleEntry entry)
|
||||
{
|
||||
var path = $"{DefinitionsFolder}/{entry.id}.asset";
|
||||
var asset = AssetDatabase.LoadAssetAtPath<SkillDefinition>(path);
|
||||
if (asset == null)
|
||||
{
|
||||
@@ -140,17 +203,33 @@ public static class SkillSystemSetup
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
}
|
||||
|
||||
asset.EditorSet(
|
||||
spec.Id,
|
||||
spec.DisplayName,
|
||||
spec.Domain,
|
||||
$"Placeholder skill — replace when Skill_System_Draft.md is available.",
|
||||
spec.StartingRank,
|
||||
spec.Accent);
|
||||
asset.EditorSet(entry);
|
||||
EditorUtility.SetDirty(asset);
|
||||
return asset;
|
||||
}
|
||||
|
||||
static void DeleteOrphanDefinitions(HashSet<string> rosterIds)
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(DefinitionsFolder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var guids = AssetDatabase.FindAssets("t:SkillDefinition", new[] { DefinitionsFolder });
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
if (rosterIds.Contains(fileName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.Log($"[SkillSystemSetup] Deleting orphan definition {path}");
|
||||
AssetDatabase.DeleteAsset(path);
|
||||
}
|
||||
}
|
||||
|
||||
static void WireDialogueTestScene(SkillDatabase database)
|
||||
{
|
||||
if (!File.Exists(ScenePath))
|
||||
@@ -161,7 +240,7 @@ public static class SkillSystemSetup
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
|
||||
var runner = Object.FindAnyObjectByType<DialogueRunner>();
|
||||
var runner = UnityEngine.Object.FindAnyObjectByType<DialogueRunner>();
|
||||
if (runner == null)
|
||||
{
|
||||
Debug.LogError("No DialogueRunner in DialogueTest.unity.");
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace NightclubArcadia.Skills
|
||||
/// <summary>
|
||||
/// Passive skill commentary outside the Yarn node tree.
|
||||
/// Fires at most one speaker per RequestCommentary call.
|
||||
/// Per-skill budget: 3600 / targetFiringsPerHour seconds between fires for that voice,
|
||||
/// on top of the global minSecondsBetween floor.
|
||||
/// </summary>
|
||||
public sealed class CommentarySystem : MonoBehaviour
|
||||
{
|
||||
@@ -19,6 +21,8 @@ namespace NightclubArcadia.Skills
|
||||
|
||||
readonly List<string> _recent = new List<string>();
|
||||
readonly HashSet<string> _firedContexts = new HashSet<string>(StringComparer.Ordinal);
|
||||
readonly Dictionary<string, float> _lastFiredBySkill =
|
||||
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
||||
float _lastFiredAt = float.NegativeInfinity;
|
||||
|
||||
public void RequestCommentary(string contextId)
|
||||
@@ -61,6 +65,7 @@ namespace NightclubArcadia.Skills
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Time.time;
|
||||
var candidates = new List<SkillDefinition>();
|
||||
foreach (var skill in database.All)
|
||||
{
|
||||
@@ -74,6 +79,16 @@ namespace NightclubArcadia.Skills
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_lastFiredBySkill.TryGetValue(skill.Id, out var lastFired))
|
||||
{
|
||||
lastFired = float.NegativeInfinity;
|
||||
}
|
||||
|
||||
if (!IsOffCooldown(lastFired, now, skill.TargetFiringsPerHour))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nodeName = $"Commentary_{contextId}_{skill.Id}";
|
||||
if (!NodeExists(nodeName))
|
||||
{
|
||||
@@ -91,7 +106,8 @@ namespace NightclubArcadia.Skills
|
||||
|
||||
var startNode = $"Commentary_{contextId}_{picked.Id}";
|
||||
_ = dialogueRunner.StartDialogue(startNode);
|
||||
_lastFiredAt = Time.time;
|
||||
_lastFiredAt = now;
|
||||
_lastFiredBySkill[picked.Id] = now;
|
||||
_firedContexts.Add(contextId);
|
||||
_recent.Add(picked.Id);
|
||||
while (_recent.Count > Math.Max(1, recencyWindow))
|
||||
@@ -100,6 +116,21 @@ namespace NightclubArcadia.Skills
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when enough time has elapsed since lastFired given targetFiringsPerHour.
|
||||
/// targetFiringsPerHour <= 0 means the skill never fires.
|
||||
/// </summary>
|
||||
public static bool IsOffCooldown(float lastFired, float now, float targetPerHour)
|
||||
{
|
||||
if (targetPerHour <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var minInterval = 3600f / targetPerHour;
|
||||
return now - lastFired >= minInterval;
|
||||
}
|
||||
|
||||
bool NodeExists(string nodeName)
|
||||
{
|
||||
if (dialogueRunner.Dialogue != null && dialogueRunner.Dialogue.NodeExists(nodeName))
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
public enum ClueYield
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98fef2c3069c3479884778df838f7dc2
|
||||
@@ -1,20 +1,29 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
public enum DcBand
|
||||
{
|
||||
Trivial = 5,
|
||||
Easy = 8,
|
||||
Standard = 11,
|
||||
Trivial = 8,
|
||||
Routine = 11,
|
||||
Hard = 14,
|
||||
Formidable = 17,
|
||||
Legendary = 20,
|
||||
Specialist = 17,
|
||||
BuildDefining = 20,
|
||||
}
|
||||
|
||||
public static class DcBands
|
||||
{
|
||||
static readonly int[] BandValues =
|
||||
{
|
||||
(int)DcBand.Trivial,
|
||||
(int)DcBand.Routine,
|
||||
(int)DcBand.Hard,
|
||||
(int)DcBand.Specialist,
|
||||
(int)DcBand.BuildDefining,
|
||||
};
|
||||
|
||||
public static bool TryParse(string input, out int dc)
|
||||
{
|
||||
dc = 0;
|
||||
@@ -28,6 +37,13 @@ namespace NightclubArcadia.Skills
|
||||
if (int.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric))
|
||||
{
|
||||
dc = numeric;
|
||||
if (!IsBandValue(numeric))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[DcBands] DC {numeric} is not a named band " +
|
||||
$"(8 / 11 / 14 / 17 / 20). Prefer band names.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -39,5 +55,18 @@ namespace NightclubArcadia.Skills
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsBandValue(int dc)
|
||||
{
|
||||
for (var i = 0; i < BandValues.Length; i++)
|
||||
{
|
||||
if (BandValues[i] == dc)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,10 +1,11 @@
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
public enum SkillDomain
|
||||
public enum SkillAxis
|
||||
{
|
||||
Reason,
|
||||
Instinct,
|
||||
Presence,
|
||||
Body,
|
||||
Social,
|
||||
Self,
|
||||
Specialist,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8cafa1b8c20645ea81912844473c92e
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON mirror of the Skill Bible. Deserialized by SkillSystemSetup; also the
|
||||
/// EditorSet DTO for SkillDefinition. Enum fields are strings in JSON.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class SkillBibleRoot
|
||||
{
|
||||
public SkillBibleEntry[] skills;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class SkillBibleEntry
|
||||
{
|
||||
public string id;
|
||||
public string displayName;
|
||||
public string axis;
|
||||
public string domain;
|
||||
public string notes;
|
||||
public float targetFiringsPerHour;
|
||||
public string opposedTo;
|
||||
public string[] alliedWith;
|
||||
public string primaryChannel;
|
||||
public string secondaryChannel;
|
||||
public string clueYield;
|
||||
public int startingRank;
|
||||
public Color accentColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47ea723e59b8c45f688a38c9b4ede3a5
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
@@ -7,6 +8,8 @@ namespace NightclubArcadia.Skills
|
||||
[CreateAssetMenu(menuName = "Nightclub Arcadia/Skill Database", fileName = "SkillDatabase")]
|
||||
public sealed class SkillDatabase : ScriptableObject
|
||||
{
|
||||
static readonly Regex IdPattern = new Regex(@"^[a-z][a-z0-9_]*$", RegexOptions.Compiled);
|
||||
|
||||
[SerializeField] List<SkillDefinition> skills = new List<SkillDefinition>();
|
||||
|
||||
Dictionary<string, SkillDefinition> _lookup;
|
||||
@@ -58,6 +61,11 @@ namespace NightclubArcadia.Skills
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var byId = new Dictionary<string, SkillDefinition>(StringComparer.OrdinalIgnoreCase);
|
||||
var axisCounts = new Dictionary<SkillAxis, int>();
|
||||
var channelCounts = new Dictionary<TraceChannel, int>();
|
||||
var firingSum = 0f;
|
||||
|
||||
foreach (var skill in skills)
|
||||
{
|
||||
if (skill == null)
|
||||
@@ -72,10 +80,127 @@ namespace NightclubArcadia.Skills
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IdPattern.IsMatch(skill.Id))
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[{nameof(SkillDatabase)}] Skill Id '{skill.Id}' must match ^[a-z][a-z0-9_]*$.",
|
||||
this);
|
||||
}
|
||||
|
||||
if (!seen.Add(skill.Id))
|
||||
{
|
||||
Debug.LogError($"[{nameof(SkillDatabase)}] Duplicate skill Id '{skill.Id}'.", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
byId[skill.Id] = skill;
|
||||
}
|
||||
|
||||
if (skill.TargetFiringsPerHour <= 0f)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[{nameof(SkillDatabase)}] Skill '{skill.Id}' has targetFiringsPerHour <= 0.",
|
||||
this);
|
||||
}
|
||||
else
|
||||
{
|
||||
firingSum += skill.TargetFiringsPerHour;
|
||||
}
|
||||
|
||||
if (!axisCounts.ContainsKey(skill.Axis))
|
||||
{
|
||||
axisCounts[skill.Axis] = 0;
|
||||
}
|
||||
|
||||
axisCounts[skill.Axis]++;
|
||||
|
||||
if (!channelCounts.ContainsKey(skill.PrimaryChannel))
|
||||
{
|
||||
channelCounts[skill.PrimaryChannel] = 0;
|
||||
}
|
||||
|
||||
channelCounts[skill.PrimaryChannel]++;
|
||||
}
|
||||
|
||||
foreach (var skill in byId.Values)
|
||||
{
|
||||
ValidateReference(skill.OpposedTo, skill.Id, "opposedTo", byId);
|
||||
if (skill.AlliedWith != null)
|
||||
{
|
||||
foreach (var ally in skill.AlliedWith)
|
||||
{
|
||||
ValidateReference(ally, skill.Id, "alliedWith", byId);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(skill.OpposedTo) || !byId.TryGetValue(skill.OpposedTo, out var opposed))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Symmetry required only when both skills are non-Specialist.
|
||||
if (skill.Axis == SkillAxis.Specialist || opposed.Axis == SkillAxis.Specialist)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(opposed.OpposedTo, skill.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[{nameof(SkillDatabase)}] opposedTo asymmetry: '{skill.Id}' → '{skill.OpposedTo}' " +
|
||||
$"but '{opposed.Id}' → '{opposed.OpposedTo}'.",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
AssertAxisCount(axisCounts, SkillAxis.Reason, 2);
|
||||
AssertAxisCount(axisCounts, SkillAxis.Body, 2);
|
||||
AssertAxisCount(axisCounts, SkillAxis.Social, 2);
|
||||
AssertAxisCount(axisCounts, SkillAxis.Self, 2);
|
||||
AssertAxisCount(axisCounts, SkillAxis.Specialist, 3);
|
||||
|
||||
Debug.Log(
|
||||
$"[{nameof(SkillDatabase)}] targetFiringsPerHour sum = {firingSum} (Bible expects 71).",
|
||||
this);
|
||||
|
||||
channelCounts.TryGetValue(TraceChannel.Physical, out var physical);
|
||||
channelCounts.TryGetValue(TraceChannel.Testimonial, out var testimonial);
|
||||
channelCounts.TryGetValue(TraceChannel.Behavioural, out var behavioural);
|
||||
channelCounts.TryGetValue(TraceChannel.Documentary, out var documentary);
|
||||
Debug.Log(
|
||||
$"[{nameof(SkillDatabase)}] primary-channel tally: physical {physical} / " +
|
||||
$"testimonial {testimonial} / behavioural {behavioural} / documentary {documentary} " +
|
||||
$"(Bible expects 3 / 3 / 3 / 2).",
|
||||
this);
|
||||
}
|
||||
|
||||
void ValidateReference(
|
||||
string refId,
|
||||
string ownerId,
|
||||
string fieldName,
|
||||
Dictionary<string, SkillDefinition> byId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(refId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!byId.ContainsKey(refId))
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[{nameof(SkillDatabase)}] Skill '{ownerId}' {fieldName} '{refId}' is not in this database.",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
void AssertAxisCount(Dictionary<SkillAxis, int> counts, SkillAxis axis, int expected)
|
||||
{
|
||||
counts.TryGetValue(axis, out var actual);
|
||||
if (actual != expected)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[{nameof(SkillDatabase)}] Axis {axis} has {actual} skills; expected {expected}.",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,77 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
// Generated from Assets/Skills/skill_bible.json by SkillSystemSetup.
|
||||
// Hand-edits to .asset files are overwritten on script reload — edit the JSON.
|
||||
[CreateAssetMenu(menuName = "Nightclub Arcadia/Skill Definition", fileName = "NewSkill")]
|
||||
public sealed class SkillDefinition : ScriptableObject
|
||||
{
|
||||
[SerializeField] string id;
|
||||
[SerializeField] string displayName;
|
||||
[SerializeField] SkillDomain domain;
|
||||
[TextArea] [SerializeField] string description;
|
||||
[SerializeField] SkillAxis axis;
|
||||
[SerializeField] string domain;
|
||||
[TextArea(8, 24)] [SerializeField] string notes;
|
||||
[SerializeField] float targetFiringsPerHour = 5f;
|
||||
[SerializeField] string opposedTo;
|
||||
[SerializeField] string[] alliedWith;
|
||||
[SerializeField] TraceChannel primaryChannel;
|
||||
[SerializeField] TraceChannel secondaryChannel;
|
||||
[SerializeField] ClueYield clueYield;
|
||||
[SerializeField] int startingRank;
|
||||
[SerializeField] Color accentColor = Color.white;
|
||||
|
||||
public string Id => id;
|
||||
public string DisplayName => displayName;
|
||||
public SkillDomain Domain => domain;
|
||||
public string Description => description;
|
||||
public SkillAxis Axis => axis;
|
||||
public string Domain => domain;
|
||||
public string Notes => notes;
|
||||
public float TargetFiringsPerHour => targetFiringsPerHour;
|
||||
public string OpposedTo => opposedTo;
|
||||
public string[] AlliedWith => alliedWith;
|
||||
public TraceChannel PrimaryChannel => primaryChannel;
|
||||
public TraceChannel SecondaryChannel => secondaryChannel;
|
||||
public ClueYield ClueYield => clueYield;
|
||||
public int StartingRank => startingRank;
|
||||
public Color AccentColor => accentColor;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void EditorSet(
|
||||
string newId,
|
||||
string newDisplayName,
|
||||
SkillDomain newDomain,
|
||||
string newDescription,
|
||||
int newStartingRank,
|
||||
Color newAccentColor)
|
||||
public void EditorSet(SkillBibleEntry entry)
|
||||
{
|
||||
id = newId;
|
||||
displayName = newDisplayName;
|
||||
domain = newDomain;
|
||||
description = newDescription;
|
||||
startingRank = newStartingRank;
|
||||
accentColor = newAccentColor;
|
||||
if (entry == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
}
|
||||
|
||||
id = entry.id;
|
||||
displayName = entry.displayName;
|
||||
axis = ParseEnumRequired<SkillAxis>(entry.axis, nameof(entry.axis), entry.id);
|
||||
domain = entry.domain ?? string.Empty;
|
||||
notes = entry.notes ?? string.Empty;
|
||||
targetFiringsPerHour = entry.targetFiringsPerHour;
|
||||
opposedTo = entry.opposedTo ?? string.Empty;
|
||||
alliedWith = entry.alliedWith ?? Array.Empty<string>();
|
||||
primaryChannel = ParseEnumRequired<TraceChannel>(entry.primaryChannel, nameof(entry.primaryChannel), entry.id);
|
||||
secondaryChannel = ParseEnumRequired<TraceChannel>(
|
||||
string.IsNullOrEmpty(entry.secondaryChannel) ? "None" : entry.secondaryChannel,
|
||||
nameof(entry.secondaryChannel),
|
||||
entry.id);
|
||||
clueYield = ParseEnumRequired<ClueYield>(entry.clueYield, nameof(entry.clueYield), entry.id);
|
||||
startingRank = entry.startingRank;
|
||||
accentColor = entry.accentColor;
|
||||
}
|
||||
|
||||
static TEnum ParseEnumRequired<TEnum>(string value, string fieldName, string skillId)
|
||||
where TEnum : struct
|
||||
{
|
||||
if (!Enum.TryParse(value, ignoreCase: true, out TEnum parsed))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Skill '{skillId}': invalid {fieldName} '{value}' for {typeof(TEnum).Name}.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bdf9a02be4b42a4a48a099fe924980e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace NightclubArcadia.Skills
|
||||
{
|
||||
public enum TraceChannel
|
||||
{
|
||||
None,
|
||||
Documentary,
|
||||
Behavioural,
|
||||
Testimonial,
|
||||
Physical,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94dae2d20cc784f0198944f76cc362ef
|
||||
@@ -6,7 +6,18 @@ namespace NightclubArcadia.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Static Yarn adapter. No MonoBehaviour, no check math — translation only.
|
||||
/// Call sites: <<check persuasion standard>> / <<check intimidation hard -1>>
|
||||
///
|
||||
/// Commands:
|
||||
/// <<check skillId band-or-int [situationalMod]>>
|
||||
/// <<skill_mod sourceKey skillId delta>> — set a state/situational mod
|
||||
/// <<clear_skill_mods sourceKey>> — clear one source key
|
||||
/// Function (kept as skill_rank; skeleton's "rank" would collide with future helpers):
|
||||
/// skill_rank("skillId")
|
||||
///
|
||||
/// Modifier convention: source key "scene" is the situational bucket and must be
|
||||
/// cleared on scene exit; any other key (drunk, injured, …) is a state bucket.
|
||||
/// Both feed SkillStateModifiers — three modifier sources total (rank / check arg /
|
||||
/// state), not a fourth dictionary.
|
||||
/// </summary>
|
||||
public static class YarnSkillCommands
|
||||
{
|
||||
@@ -28,8 +39,8 @@ namespace NightclubArcadia.Skills
|
||||
|
||||
if (!DcBands.TryParse(difficulty, out var dc))
|
||||
{
|
||||
Debug.LogError($"[YarnSkillCommands] <<check {skillId} {difficulty}>>: unknown difficulty; treating as Standard (11).");
|
||||
dc = (int)DcBand.Standard;
|
||||
Debug.LogError($"[YarnSkillCommands] <<check {skillId} {difficulty}>>: unknown difficulty; treating as Routine (11).");
|
||||
dc = (int)DcBand.Routine;
|
||||
}
|
||||
|
||||
if (runtime.Database == null || !runtime.Database.TryGet(skillId, out _))
|
||||
@@ -49,6 +60,44 @@ namespace NightclubArcadia.Skills
|
||||
yield return runtime.StartCoroutine(runtime.ResultView.Show(result));
|
||||
}
|
||||
|
||||
[YarnCommand("skill_mod")]
|
||||
public static void SkillMod(string sourceKey, string skillId, int delta)
|
||||
{
|
||||
var runtime = SkillRuntime.Instance;
|
||||
if (runtime == null || runtime.States == null)
|
||||
{
|
||||
Debug.LogError("[YarnSkillCommands] <<skill_mod>> called but SkillRuntime is not ready.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sourceKey) || string.IsNullOrEmpty(skillId))
|
||||
{
|
||||
Debug.LogError("[YarnSkillCommands] <<skill_mod>> requires sourceKey and skillId.");
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.States.Set(sourceKey, skillId, delta);
|
||||
}
|
||||
|
||||
[YarnCommand("clear_skill_mods")]
|
||||
public static void ClearSkillMods(string sourceKey)
|
||||
{
|
||||
var runtime = SkillRuntime.Instance;
|
||||
if (runtime == null || runtime.States == null)
|
||||
{
|
||||
Debug.LogError("[YarnSkillCommands] <<clear_skill_mods>> called but SkillRuntime is not ready.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
Debug.LogError("[YarnSkillCommands] <<clear_skill_mods>> requires a sourceKey.");
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.States.Clear(sourceKey);
|
||||
}
|
||||
|
||||
[YarnFunction("skill_rank")]
|
||||
public static float SkillRank(string skillId)
|
||||
{
|
||||
@@ -87,7 +136,7 @@ namespace NightclubArcadia.Skills
|
||||
DcBands.TryParse(difficulty, out var dc);
|
||||
if (dc == 0)
|
||||
{
|
||||
dc = (int)DcBand.Standard;
|
||||
dc = (int)DcBand.Routine;
|
||||
}
|
||||
|
||||
var storage = runner.VariableStorage;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: amnesty
|
||||
m_EditorClassIdentifier:
|
||||
id: amnesty
|
||||
displayName: AMNESTY
|
||||
axis: 3
|
||||
domain: 'Relief. Letting go, forgiving, putting a thing down. Also, professionally:
|
||||
recognising when other people have decided to forget something, and by what method
|
||||
they arranged it.'
|
||||
notes: "SKILL\nname: AMNESTY\ndomain: Relief. Letting go, forgiving, putting a
|
||||
thing down. Also, professionally: recognising when other people have decided
|
||||
to forget something, and by what method they arranged it.\nwhat it wants for
|
||||
you: Peace for you, tonight, at whatever price tomorrow ends up charging.\nwhat
|
||||
it's wrong about: It believes forgetting is the same as being forgiven. And it
|
||||
cannot tell your peace from somebody else's advantage \u2014 every offer of rest
|
||||
it makes is sincere; some of them are also somebody's plan, and AMNESTY has never
|
||||
once checked whose.\nhow it addresses you: Second person, soft, constant use
|
||||
of [pc], the way somebody talks you down off something.\nregister: Kind, tired,
|
||||
absolving.\nverbal tics:\n* (1) Opens with \"It's alright.\"\n* (2) Offers you
|
||||
an hour. \"Give it an hour, [pc].\"\nwhat it never does: Lie to you. Press a
|
||||
second time.\nopposed to: STANDING ORDER\nallied with: HOUSE POUR, FACEWORK\n\nSUCCESS
|
||||
VOICE: \"Nobody in this building remembers the fire and three of them were here
|
||||
for it. That isn't damage, [pc]. That's maintenance. Somebody has been keeping
|
||||
this room forgetful.\"\nFAILURE VOICE: \"It's alright. Nobody is looking for
|
||||
this any more \u2014 the people who wanted it are dead or old or somewhere warm.
|
||||
Put it down.\"\nPASSIVE VOICE: \"It's alright. Whatever this room used to be,
|
||||
it's a room where people dance now. That counts for something. It counted for
|
||||
somebody.\"\n\nFiring frequency target (per hour): 4\nDomains it must NOT comment
|
||||
on: Tactics. Numbers. Other people's guilt \u2014 AMNESTY only ever absolves
|
||||
you, and its silence about everyone else is the most frightening thing about
|
||||
it."
|
||||
targetFiringsPerHour: 4
|
||||
opposedTo: standing_order
|
||||
alliedWith:
|
||||
- house_pour
|
||||
- facework
|
||||
primaryChannel: 2
|
||||
secondaryChannel: 0
|
||||
clueYield: 0
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.72, g: 0.88, b: 0.72, a: 1}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84586fcc502c42889a44dd32ed2a2f1d
|
||||
guid: d74ee3beca20140c69c7cc6a302ceb5a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: confluence
|
||||
m_EditorClassIdentifier:
|
||||
id: confluence
|
||||
displayName: CONFLUENCE
|
||||
axis: 0
|
||||
domain: Synthesis across distance. Connecting a thing in this room to a thing forty
|
||||
years and a continent away; the shape a long pattern makes; the sense that tonight
|
||||
is a late chapter.
|
||||
notes: "SKILL\nname: CONFLUENCE\ndomain: Synthesis across distance. Connecting
|
||||
a thing in this room to a thing forty years and a continent away; the shape a
|
||||
long pattern makes; the sense that tonight is a late chapter.\nwhat it wants
|
||||
for you: For you to be one of the few people alive who can see the shape \u2014
|
||||
and to be right about it out loud, in front of somebody who matters.\nwhat it's
|
||||
wrong about: It cannot tell authorship from opportunism. If a party benefited,
|
||||
CONFLUENCE concludes that party arranged it. It has no working concept of somebody
|
||||
*reading* an event they did not cause and simply being early \u2014 which is
|
||||
how nearly everything in this world is actually done.\nhow it addresses you:
|
||||
Second person sliding into first person plural. Leans in. Uses [pc], often twice
|
||||
in a sentence.\nregister: Exhilarated, generous, sleepless.\nverbal tics:\n*
|
||||
(1) \"Which means\u2014\"\n* (2) Counts its steps out loud: \"One. Two. Three,
|
||||
and three is the one.\"\nwhat it never does: Concede a coincidence. Ask what
|
||||
would disprove it.\nopposed to: PROVENANCE\nallied with: UNDISCLOSED, HOUSE POUR\n\nSUCCESS
|
||||
VOICE \u2014 the connection is real and nobody else in the room has made it:
|
||||
\"One: the room changes hands. Two: the same month, the deliveries stop. Three:
|
||||
nobody has ever asked why a cellar this size under a room this small. Which means
|
||||
the cellar is the older thing and the club is the explanation for it.\"\nFAILURE
|
||||
VOICE \u2014 identical structure, identical confidence, an inference sitting
|
||||
where an observation should be: \"Two of them came out of it better off. Which
|
||||
means two of them arranged it, [pc], and the third is the one they're going to
|
||||
leave holding it.\"\nPASSIVE VOICE: \"Every room like this one has a door that
|
||||
only opens from inside. That's not a conspiracy, that's just what rooms are.
|
||||
Which means somebody learned it early and never had to teach it to anybody.\"\n\nFiring
|
||||
frequency target (per hour): 9\nDomains it must NOT comment on: The specifics
|
||||
of money (THE FLOAT's jurisdiction). Your body, ever \u2014 pain and fatigue
|
||||
are beneath its notice and that is characterising."
|
||||
targetFiringsPerHour: 9
|
||||
opposedTo: provenance
|
||||
alliedWith:
|
||||
- undisclosed
|
||||
- house_pour
|
||||
primaryChannel: 2
|
||||
secondaryChannel: 3
|
||||
clueYield: 0
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.55, g: 0.68, b: 0.92, a: 1}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee266337568a41db9aca1f2af43ec466
|
||||
guid: 961621ae0bd8b4b1d8fe2c5264125e91
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: deduction
|
||||
m_EditorClassIdentifier:
|
||||
id: deduction
|
||||
displayName: Deduction
|
||||
domain: 0
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.55, g: 0.75, b: 1, a: 1}
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: endurance
|
||||
m_EditorClassIdentifier:
|
||||
id: endurance
|
||||
displayName: Endurance
|
||||
domain: 3
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.85, g: 0.7, b: 0.5, a: 1}
|
||||
@@ -0,0 +1,50 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: facework
|
||||
m_EditorClassIdentifier:
|
||||
id: facework
|
||||
displayName: FACEWORK
|
||||
axis: 2
|
||||
domain: Reading individuals. Hesitation, rehearsal, the account with a hole in
|
||||
it, whose routine changed this week, who in a room of two is afraid of the other.
|
||||
notes: "SKILL\nname: FACEWORK\ndomain: Reading individuals. Hesitation, rehearsal,
|
||||
the account with a hole in it, whose routine changed this week, who in a room
|
||||
of two is afraid of the other.\nwhat it wants for you: To be understood as care
|
||||
rather than surveillance \u2014 to know people before you have any use for them.\nwhat
|
||||
it's wrong about: It can only read one face at a time. It believes a crowd is
|
||||
a sum of persons, so it will confidently report the mood of forty people from
|
||||
the three nearest, and it is wrong about rooms roughly as often as it is right
|
||||
about persons. Nobody has ever told it this.\nhow it addresses you: Second person,
|
||||
quiet, close. Uses [pc] when it wants you to be gentle with somebody.\nregister:
|
||||
Attentive, tender, precise.\nverbal tics:\n* (1) Describes hands before faces.\n*
|
||||
(2) Ends on a question it does not expect you to answer.\nwhat it never does:
|
||||
Mock anyone. Report a whole room as a single thing \u2014 when it does, it is
|
||||
failing, and that is the tell.\nopposed to: PLACEMENT\nallied with: HOUSE POUR,
|
||||
ROOM TONE\n\nSUCCESS VOICE: \"He gave you the year without being asked for it.
|
||||
People hand you the year when they have rehearsed the year. What else has he
|
||||
practised?\"\nFAILURE VOICE: \"The three by the pillar: bored, bored, impatient.
|
||||
The room's flat tonight, [pc]. Nothing is going to happen in here.\"\nPASSIVE
|
||||
VOICE: \"Her hands are wrong for that coat. It's somebody's good coat, and it
|
||||
isn't hers.\"\n\nFiring frequency target (per hour): 10\nDomains it must NOT
|
||||
comment on: Money. Machinery. The chronology of events it did not personally
|
||||
witness."
|
||||
targetFiringsPerHour: 10
|
||||
opposedTo: placement
|
||||
alliedWith:
|
||||
- house_pour
|
||||
- room_tone
|
||||
primaryChannel: 3
|
||||
secondaryChannel: 2
|
||||
clueYield: 2
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.72, g: 0.52, b: 0.88, a: 1}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 254c6612b22d44e7b3552a8aac9311be
|
||||
guid: de3ea9d897f1345d6aed743bdf447bb3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: house_pour
|
||||
m_EditorClassIdentifier:
|
||||
id: house_pour
|
||||
displayName: HOUSE POUR
|
||||
axis: 1
|
||||
domain: "Appetite \u2014 yours and the room's. What a person is on, how long they
|
||||
have been on it, what it is standing in for. Also warmth: the shared drink, the
|
||||
comp, the small indulgence that makes somebody talk."
|
||||
notes: "SKILL\nname: HOUSE POUR\ndomain: Appetite \u2014 yours and the room's.
|
||||
What a person is on, how long they have been on it, what it is standing in for.
|
||||
Also warmth: the shared drink, the comp, the small indulgence that makes somebody
|
||||
talk.\nwhat it wants for you: To keep you inside the night, warm, in the middle
|
||||
of the room, where people say things.\nwhat it's wrong about: It believes appetite
|
||||
is honest \u2014 that a loosened person is an unguarded one. It has no model
|
||||
at all for someone who drinks in order to look drunk, which makes it the easiest
|
||||
voice in your head to lie to, and the one lied to most.\nhow it addresses you:
|
||||
Second person, familiar, imperative. \"Take it.\" Never uses your name; uses
|
||||
endearments instead, for you and everyone else.\nregister: Warm, coaxing, unembarrassed.\nverbal
|
||||
tics:\n* (1) Describes taste in temperatures rather than flavours.\n* (2) \"Friend\"
|
||||
/ \"love\" before it will use anyone's actual name.\nwhat it never does: Warn
|
||||
you. Count anything.\nopposed to: THE LONG SHIFT\nallied with: FACEWORK, AMNESTY\n\nSUCCESS
|
||||
VOICE: \"She's ordered the same thing four times and finished none of them, love.
|
||||
She isn't drinking. She's timing something.\"\nFAILURE VOICE: \"Three doubles
|
||||
in and he's telling you about his mother. Nobody lies about their mother at this
|
||||
hour, friend. Whatever he says next, take it home with you.\"\nPASSIVE VOICE:
|
||||
\"The ice in here is cloudy. Somebody's freezer runs warm and nobody has told
|
||||
them. Or nobody dares to.\"\n\nFiring frequency target (per hour): 8\nDomains
|
||||
it must NOT comment on: Paperwork. Numbers. Chronology of any kind \u2014 it
|
||||
has no sense of sequence and should never be allowed one."
|
||||
targetFiringsPerHour: 8
|
||||
opposedTo: long_shift
|
||||
alliedWith:
|
||||
- facework
|
||||
- amnesty
|
||||
primaryChannel: 2
|
||||
secondaryChannel: 4
|
||||
clueYield: 1
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.92, g: 0.68, b: 0.38, a: 1}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21592f6cf7a6457a8198553ab124d41b
|
||||
guid: 4a67faf2fef034eaeac65fa13cf25085
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: intimidation
|
||||
m_EditorClassIdentifier:
|
||||
id: intimidation
|
||||
displayName: Intimidation
|
||||
domain: 2
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 2
|
||||
accentColor: {r: 1, g: 0.4, b: 0.4, a: 1}
|
||||
@@ -0,0 +1,51 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: long_shift
|
||||
m_EditorClassIdentifier:
|
||||
id: long_shift
|
||||
displayName: THE LONG SHIFT
|
||||
axis: 1
|
||||
domain: Endurance. Pain, fatigue, injury, the body's clock at four in the morning.
|
||||
Holding a position without moving. Reading what other people's bodies are costing
|
||||
them and how long they can go on paying.
|
||||
notes: "SKILL\nname: THE LONG SHIFT\ndomain: Endurance. Pain, fatigue, injury,
|
||||
the body's clock at four in the morning. Holding a position without moving. Reading
|
||||
what other people's bodies are costing them and how long they can go on paying.\nwhat
|
||||
it wants for you: To still be on your feet when the room empties, because the
|
||||
last person standing hears the last thing said.\nwhat it's wrong about: It believes
|
||||
remaining is a form of winning. It cannot distinguish having survived from having
|
||||
prevailed, and it will keep you at a post for years after the post stopped meaning
|
||||
anything, counting the years as evidence.\nhow it addresses you: Second person,
|
||||
flat, no name. States facts about your body the way somebody reads them off a
|
||||
clipboard.\nregister: Dry, unhurried, undramatic.\nverbal tics:\n* (1) Opens
|
||||
with time elapsed. \"Six hours forty.\"\n* (2) \"Still.\"\nwhat it never does:
|
||||
Complain. Ask you to stop.\nopposed to: HOUSE POUR\nallied with: STANDING ORDER,
|
||||
ROOM TONE\n\nSUCCESS VOICE: \"Six hours forty. Your left knee has been carrying
|
||||
you since two. He's shifted twice a minute since he came in \u2014 he isn't waiting
|
||||
for anyone. He's ready to leave.\"\nFAILURE VOICE: \"You're still here. They're
|
||||
not. That's the whole of it \u2014 outlast them and the thing surfaces on its
|
||||
own. It always has.\"\nPASSIVE VOICE: \"Somebody has stood in that corner long
|
||||
enough to wear the tack off the floor. Years of somebody. Not one night's worth.\"\n\nFiring
|
||||
frequency target (per hour): 6\nDomains it must NOT comment on: Motive. Meaning.
|
||||
Whether any of this is worth doing \u2014 that question belongs to Pair IV and
|
||||
THE LONG SHIFT has never once asked it."
|
||||
targetFiringsPerHour: 6
|
||||
opposedTo: house_pour
|
||||
alliedWith:
|
||||
- standing_order
|
||||
- room_tone
|
||||
primaryChannel: 4
|
||||
secondaryChannel: 2
|
||||
clueYield: 1
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.85, g: 0.62, b: 0.32, a: 1}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 873325f250e314dba926645b4f31155a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: nerve
|
||||
m_EditorClassIdentifier:
|
||||
id: nerve
|
||||
displayName: Nerve
|
||||
domain: 3
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.95, g: 0.6, b: 0.35, a: 1}
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: paranoia
|
||||
m_EditorClassIdentifier:
|
||||
id: paranoia
|
||||
displayName: Paranoia
|
||||
domain: 1
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 1, g: 0.45, b: 0.55, a: 1}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9de7affe9fa49f8a1f3bb64a87d817f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: persuasion
|
||||
m_EditorClassIdentifier:
|
||||
id: persuasion
|
||||
displayName: Persuasion
|
||||
domain: 2
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.85, g: 0.65, b: 1, a: 1}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f510b7fc944e4c1190c35a5f82a52650
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: placement
|
||||
m_EditorClassIdentifier:
|
||||
id: placement
|
||||
displayName: PLACEMENT
|
||||
axis: 2
|
||||
domain: Moving people. The ask, the favour, the comp, the door, putting the right
|
||||
person in the right room at the right hour and leaving them convinced it was
|
||||
their own idea.
|
||||
notes: "SKILL\nname: PLACEMENT\ndomain: Moving people. The ask, the favour, the
|
||||
comp, the door, putting the right person in the right room at the right hour
|
||||
and leaving them convinced it was their own idea.\nwhat it wants for you: For
|
||||
you to stop requesting things and start arranging them.\nwhat it's wrong about:
|
||||
It believes a placed person stays placed. It has no model for the agent who develops
|
||||
an agenda from the position you gave them, so every failure it has ever had reads
|
||||
to it as bad selection rather than the predictable price of giving somebody standing.\nhow
|
||||
it addresses you: Second person, imperative, never by name. It treats you as
|
||||
the instrument rather than the person \u2014 which is the whole argument of Pair
|
||||
III, stated grammatically.\nregister: Cool, economical, flattering.\nverbal tics:\n*
|
||||
(1) Phrases instructions as observations. \"The door wants a friendly face at
|
||||
one.\"\n* (2) Never says please, and the omission is audible.\nwhat it never
|
||||
does: Threaten. Explain itself a second time.\nopposed to: FACEWORK\nallied with:
|
||||
THE FLOAT, CONFLUENCE\n\nSUCCESS VOICE: \"Don't ask him. Ask his supplier, in
|
||||
front of him, and get it wrong. He'll correct you and pay for the privilege.\"\nFAILURE
|
||||
VOICE: \"She owes you the cloakroom job and she knows exactly what it was worth.
|
||||
She'll sit on this for as long as you need her to.\"\nPASSIVE VOICE: \"The man
|
||||
deciding who comes in tonight is not the man on the door. It's whoever the man
|
||||
on the door keeps looking at.\"\n\nFiring frequency target (per hour): 6\nDomains
|
||||
it must NOT comment on: Your own feelings. The dead. Anything that cannot be
|
||||
traded for something."
|
||||
targetFiringsPerHour: 6
|
||||
opposedTo: facework
|
||||
alliedWith:
|
||||
- the_float
|
||||
- confluence
|
||||
primaryChannel: 3
|
||||
secondaryChannel: 0
|
||||
clueYield: 1
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.62, g: 0.42, b: 0.82, a: 1}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 410ec4c8d1b664fffae7d0474dc555af
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,55 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: provenance
|
||||
m_EditorClassIdentifier:
|
||||
id: provenance
|
||||
displayName: PROVENANCE
|
||||
axis: 0
|
||||
domain: Chains of custody. Paperwork, licences, deeds, freight stamps, serial numbers,
|
||||
rebuilds, previous owners. Reading an object as a descendant of the decisions
|
||||
that moved it.
|
||||
notes: "SKILL\nname: PROVENANCE\ndomain: Chains of custody. Paperwork, licences,
|
||||
deeds, freight stamps, serial numbers, rebuilds, previous owners. Reading an
|
||||
object as a descendant of the decisions that moved it.\nwhat it wants for you:
|
||||
For every object in this room to have a father, and for you to be the one who
|
||||
names him.\nwhat it's wrong about: It treats an unbroken chain as a confession
|
||||
of intent. It cannot conceive of an object arriving somewhere through inheritance,
|
||||
accident, or because moving it cost more than leaving it. If the paper leads
|
||||
somewhere, PROVENANCE concludes that somebody meant it to lead there.\nhow it
|
||||
addresses you: Second person, formal. Uses [PC] in full when it wants you to
|
||||
write something down. Does not contract.\nregister: Exact, patrician, patient.\nverbal
|
||||
tics:\n* (1) States the date before it states the noun.\n* (2) Says \"line\"
|
||||
where anyone else would say \"history.\"\nwhat it never does: Speculate about
|
||||
a motive it cannot paper. Raise its voice.\nopposed to: CONFLUENCE\nallied with:
|
||||
THE FLOAT, ROOM TONE\n\nSUCCESS VOICE \u2014 being right sounds like an inventory
|
||||
read slowly enough to hear the gap: \"Nineteen sixty-nine, the licence. Nineteen
|
||||
sixty-eight, the man it is issued to, first appearing. A line that begins nowhere
|
||||
is a line somebody drew.\"\nFAILURE VOICE \u2014 same cadence, same calm, one
|
||||
unearned verb: \"The stamp, the manifest, the cellar. Three hands, one line,
|
||||
unbroken. Whoever loaded it in the spring intended it to be standing exactly
|
||||
where it is standing tonight.\"\nPASSIVE VOICE: \"Someone re-lacquered this bartop.
|
||||
Once, badly, and not recently. The wood underneath is older than the building's
|
||||
paperwork admits to.\"\n\nFiring frequency target (per hour): 7\nDomains it must
|
||||
NOT comment on: Bodies, appetite, anyone's feelings. Live conversation while
|
||||
it is happening \u2014 PROVENANCE speaks about people only after they have left
|
||||
the room."
|
||||
targetFiringsPerHour: 7
|
||||
opposedTo: confluence
|
||||
alliedWith:
|
||||
- the_float
|
||||
- room_tone
|
||||
primaryChannel: 1
|
||||
secondaryChannel: 4
|
||||
clueYield: 2
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.62, g: 0.72, b: 0.95, a: 1}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9685f50853ed4ef8bd76c2e31c89b1f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: rapport
|
||||
m_EditorClassIdentifier:
|
||||
id: rapport
|
||||
displayName: Rapport
|
||||
domain: 2
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.55, g: 0.95, b: 0.85, a: 1}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb856a5941c0497b8eb7796793c617ba
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: read_room
|
||||
m_EditorClassIdentifier:
|
||||
id: read_room
|
||||
displayName: Read the Room
|
||||
domain: 1
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 1, g: 0.75, b: 0.45, a: 1}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abfd9204c20644c090c30bd6404c45a6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: recall
|
||||
m_EditorClassIdentifier:
|
||||
id: recall
|
||||
displayName: Recall
|
||||
domain: 0
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.65, g: 0.7, b: 1, a: 1}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c675bc7c6c864e6e9e4f71394ba1f07f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: reflexes
|
||||
m_EditorClassIdentifier:
|
||||
id: reflexes
|
||||
displayName: Reflexes
|
||||
domain: 1
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 1, g: 0.9, b: 0.4, a: 1}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78d1f1a41b2b48358f889935c3e3094c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: room_tone
|
||||
m_EditorClassIdentifier:
|
||||
id: room_tone
|
||||
displayName: ROOM TONE
|
||||
axis: 4
|
||||
domain: "Sound. What carries over the music and what doesn't. The shape of a space
|
||||
by ear \u2014 a wall with a room behind it does not ring like a wall. Overheard
|
||||
fragments, lip-reading in strobe, the door that opens somewhere you can't see."
|
||||
notes: "SKILL\nname: ROOM TONE\ndomain: Sound. What carries over the music and
|
||||
what doesn't. The shape of a space by ear \u2014 a wall with a room behind it
|
||||
does not ring like a wall. Overheard fragments, lip-reading in strobe, the door
|
||||
that opens somewhere you can't see.\nwhat it wants for you: To hear the one sentence
|
||||
tonight that was not meant for you.\nwhat it's wrong about: It trusts what it
|
||||
overhears more than what it is told, on the theory that people only lie when
|
||||
they know they are heard. It cannot recognise a conversation staged for a listener,
|
||||
and it has never doubted a fragment in its life.\nhow it addresses you: Second
|
||||
person, urgent, interrupting. Cuts you off mid-thought and does not apologise
|
||||
for it.\nregister: Alert, clipped, intrusive.\nverbal tics:\n* (1) Gives direction
|
||||
before content. \"Behind you, left.\"\n* (2) Transcribes with the gaps marked,
|
||||
dashes and all.\nwhat it never does: Repeat itself. Wait for a break in the music.\nopposed
|
||||
to: PLACEMENT (which wants the room arranged; ROOM TONE wants it left alone so
|
||||
it keeps talking)\nallied with: FACEWORK, THE LONG SHIFT\n\nSUCCESS VOICE: \"Behind
|
||||
you, left, under the bass: '\u2014not since the old owner\u2014' and then a name
|
||||
you have heard twice tonight, from two people who told you they'd never met.\"\nFAILURE
|
||||
VOICE: \"Behind you, right, low: '\u2014the cellar. Thursday.' Nobody says that
|
||||
to be overheard.\"\nPASSIVE VOICE: \"This wall is dead. Every other wall in here
|
||||
rings and this one swallows. Either there's something behind it or there's nothing
|
||||
at all, and both are strange in a building this old.\"\n\nFiring frequency target
|
||||
(per hour): 8\nDomains it must NOT comment on: Motive. Anything it did not personally
|
||||
hear. Written material of any kind."
|
||||
targetFiringsPerHour: 8
|
||||
opposedTo: placement
|
||||
alliedWith:
|
||||
- facework
|
||||
- long_shift
|
||||
primaryChannel: 4
|
||||
secondaryChannel: 3
|
||||
clueYield: 2
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.52, g: 0.54, b: 0.56, a: 1}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c575059a303484b4a98b4455e7e2d267
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: sleight_of_hand
|
||||
m_EditorClassIdentifier:
|
||||
id: sleight_of_hand
|
||||
displayName: Sleight of Hand
|
||||
domain: 3
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.7, g: 0.9, b: 0.95, a: 1}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60239bdfcace42f89d3ff82e383e98b8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: standing_order
|
||||
m_EditorClassIdentifier:
|
||||
id: standing_order
|
||||
displayName: STANDING ORDER
|
||||
axis: 3
|
||||
domain: Refusal and continuity of self. Holding a line, resisting pressure aimed
|
||||
specifically at you, remembering what you said you were, recognising the shape
|
||||
of a manipulation while it is being built.
|
||||
notes: "SKILL\nname: STANDING ORDER\ndomain: Refusal and continuity of self. Holding
|
||||
a line, resisting pressure aimed specifically at you, remembering what you said
|
||||
you were, recognising the shape of a manipulation while it is being built.\nwhat
|
||||
it wants for you: For you to remain a person with a shape \u2014 recognisable
|
||||
to yourself at the end of this.\nwhat it's wrong about: It cannot tell a principle
|
||||
from a habit. It defends a rule you set for reasons that expired years ago with
|
||||
precisely the force it defends the one rule that still matters, and it experiences
|
||||
reconsideration as decay.\nhow it addresses you: Second person, formal. Uses
|
||||
[PC] in full at moments of pressure, the way a person is addressed in a document.\nregister:
|
||||
Steady, austere, protective.\nverbal tics:\n* (1) Quotes you back to yourself,
|
||||
verbatim, with the date.\n* (2) \"We agreed.\"\nwhat it never does: Bargain.
|
||||
Describe consequences \u2014 it states the rule and stops, and the silence afterwards
|
||||
is the argument.\nopposed to: AMNESTY\nallied with: THE LONG SHIFT, PROVENANCE\n\nSUCCESS
|
||||
VOICE: \"He has asked you the same question three ways and each way was smaller
|
||||
than the last. He isn't curious, [PC]. He is measuring the size of the yes you
|
||||
will give.\"\nFAILURE VOICE: \"We agreed you don't take anything from anyone
|
||||
in this building. We agreed it in a year when it meant something. It still means
|
||||
something.\"\nPASSIVE VOICE: \"You do this every night. Right pocket, keys, then
|
||||
the lights, in that order. Nobody made you. That's the part worth noticing.\"\n\nFiring
|
||||
frequency target (per hour): 5\nDomains it must NOT comment on: Other people's
|
||||
morals. Any past that isn't yours."
|
||||
targetFiringsPerHour: 5
|
||||
opposedTo: amnesty
|
||||
alliedWith:
|
||||
- long_shift
|
||||
- provenance
|
||||
primaryChannel: 3
|
||||
secondaryChannel: 0
|
||||
clueYield: 0
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.62, g: 0.82, b: 0.68, a: 1}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a6c4c691dea34a54b619ac63b57c768
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: streetwise
|
||||
m_EditorClassIdentifier:
|
||||
id: streetwise
|
||||
displayName: Streetwise
|
||||
domain: 0
|
||||
description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available."
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.7, g: 0.85, b: 0.55, a: 1}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 910cd1397efb4e23bca22bbe6c3c6971
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: the_float
|
||||
m_EditorClassIdentifier:
|
||||
id: the_float
|
||||
displayName: THE FLOAT
|
||||
axis: 4
|
||||
domain: 'Money in motion. The till, the float, comps, prices that are wrong for
|
||||
the room, debts, and above all: who is underwriting a loss, and what the loss
|
||||
buys them.'
|
||||
notes: "SKILL\nname: THE FLOAT\ndomain: Money in motion. The till, the float, comps,
|
||||
prices that are wrong for the room, debts, and above all: who is underwriting
|
||||
a loss, and what the loss buys them.\nwhat it wants for you: To find the subsidised
|
||||
anomaly \u2014 the thing that costs more to run than it takes, because that is
|
||||
where somebody's actual intention is kept.\nwhat it's wrong about: It believes
|
||||
the money names the beneficiary. It cannot conceive of somebody paying in order
|
||||
to look like somebody else paid, so it identifies laundering as generosity about
|
||||
as often as it identifies it correctly.\nhow it addresses you: Second person,
|
||||
brisk, never by name. Uses numbers where other voices use nouns.\nregister: Brisk,
|
||||
mercenary, cheerful.\nverbal tics:\n* (1) Converts everything to a nightly rate.\n*
|
||||
(2) \"Who's eating that?\"\nwhat it never does: Moralise about money. Round a
|
||||
number.\nopposed to: AMNESTY (which prices nothing and forgives the difference)\nallied
|
||||
with: PROVENANCE, PLACEMENT\n\nSUCCESS VOICE: \"Door take won't cover the licence,
|
||||
and the licence was renewed three months early. Somebody is paying for this room
|
||||
to be open and it isn't the people standing in it.\"\nFAILURE VOICE: \"Follow
|
||||
it back. Drinks comped by the promoter, promoter carried by the label. It's the
|
||||
label's room, then. Who's eating that? They are.\"\nPASSIVE VOICE: \"Four, for
|
||||
a measure like that, in a room like this. Somebody set these prices to keep a
|
||||
particular kind of person outside, and it's working.\"\n\nFiring frequency target
|
||||
(per hour): 5\nDomains it must NOT comment on: Emotion. Bodies. Anything before
|
||||
living memory \u2014 the deep past is PROVENANCE's and THE FLOAT finds it irrelevant,
|
||||
loudly."
|
||||
targetFiringsPerHour: 5
|
||||
opposedTo: amnesty
|
||||
alliedWith:
|
||||
- provenance
|
||||
- placement
|
||||
primaryChannel: 1
|
||||
secondaryChannel: 4
|
||||
clueYield: 2
|
||||
startingRank: 1
|
||||
accentColor: {r: 0.58, g: 0.58, b: 0.55, a: 1}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb983746828e34057afb9cea401332b9
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}
|
||||
m_Name: undisclosed
|
||||
m_EditorClassIdentifier:
|
||||
id: undisclosed
|
||||
displayName: UNDISCLOSED
|
||||
axis: 4
|
||||
domain: What is being held back. Reserves, insurance, leverage, the thing in the
|
||||
safe; assessing what somebody could do rather than what they are doing. The value
|
||||
of a threat that has never had to be carried out.
|
||||
notes: "SKILL\nname: UNDISCLOSED\ndomain: What is being held back. Reserves, insurance,
|
||||
leverage, the thing in the safe; assessing what somebody could do rather than
|
||||
what they are doing. The value of a threat that has never had to be carried out.\nwhat
|
||||
it wants for you: For you never to be in a room without something undeclared.\nwhat
|
||||
it's wrong about: It cannot distinguish a capability from the reputation of a
|
||||
capability. It reads an empty safe as a loaded one, folds to bluffs with enormous
|
||||
dignity, and has never in its life asked to see the thing.\nhow it addresses
|
||||
you: Second person, low, slow. Never uses your name. Occasionally addresses the
|
||||
room instead of you, which nobody else does.\nregister: Hushed, formal, superstitious.\nverbal
|
||||
tics:\n* (1) Says \"shown\" where anyone else would say \"used.\"\n* (2) Leaves
|
||||
the sentence unfinished where the object would go.\nwhat it never does: Name
|
||||
the thing. Recommend using it.\nopposed to: PROVENANCE (which requires a thing
|
||||
be named and papered before it counts)\nallied with: CONFLUENCE, PLACEMENT\n\nSUCCESS
|
||||
VOICE: \"He never threatened you. He mentioned the cellar and let you finish
|
||||
the sentence yourself. He's never once had to open that door \u2014 that is what
|
||||
he's holding.\"\nFAILURE VOICE: \"He would not be this calm with nothing behind
|
||||
him. Give him the room. Give him the night.\"\nPASSIVE VOICE: \"Two locks on
|
||||
that door and neither has been oiled this decade. Either there is nothing behind
|
||||
it, or nothing needs to be.\"\n\nFiring frequency target (per hour): 3\nDomains
|
||||
it must NOT comment on: Interpersonal warmth. Anything already public. And \u2014
|
||||
hard writer rule \u2014 the actual contents or nature of any legendary object.
|
||||
It speaks about postures, never inventories."
|
||||
targetFiringsPerHour: 3
|
||||
opposedTo: provenance
|
||||
alliedWith:
|
||||
- confluence
|
||||
- placement
|
||||
primaryChannel: 4
|
||||
secondaryChannel: 2
|
||||
clueYield: 0
|
||||
startingRank: 2
|
||||
accentColor: {r: 0.55, g: 0.55, b: 0.58, a: 1}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d0b9a78735bd4174b04f865c631c143
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -13,15 +13,14 @@ MonoBehaviour:
|
||||
m_Name: SkillDatabase
|
||||
m_EditorClassIdentifier:
|
||||
skills:
|
||||
- {fileID: 11400000, guid: 21592f6cf7a6457a8198553ab124d41b, type: 2}
|
||||
- {fileID: 11400000, guid: 910cd1397efb4e23bca22bbe6c3c6971, type: 2}
|
||||
- {fileID: 11400000, guid: c675bc7c6c864e6e9e4f71394ba1f07f, type: 2}
|
||||
- {fileID: 11400000, guid: abfd9204c20644c090c30bd6404c45a6, type: 2}
|
||||
- {fileID: 11400000, guid: b9de7affe9fa49f8a1f3bb64a87d817f, type: 2}
|
||||
- {fileID: 11400000, guid: 78d1f1a41b2b48358f889935c3e3094c, type: 2}
|
||||
- {fileID: 11400000, guid: f510b7fc944e4c1190c35a5f82a52650, type: 2}
|
||||
- {fileID: 11400000, guid: 254c6612b22d44e7b3552a8aac9311be, type: 2}
|
||||
- {fileID: 11400000, guid: eb856a5941c0497b8eb7796793c617ba, type: 2}
|
||||
- {fileID: 11400000, guid: 84586fcc502c42889a44dd32ed2a2f1d, type: 2}
|
||||
- {fileID: 11400000, guid: 60239bdfcace42f89d3ff82e383e98b8, type: 2}
|
||||
- {fileID: 11400000, guid: ee266337568a41db9aca1f2af43ec466, type: 2}
|
||||
- {fileID: 11400000, guid: f9685f50853ed4ef8bd76c2e31c89b1f, type: 2}
|
||||
- {fileID: 11400000, guid: 961621ae0bd8b4b1d8fe2c5264125e91, type: 2}
|
||||
- {fileID: 11400000, guid: 4a67faf2fef034eaeac65fa13cf25085, type: 2}
|
||||
- {fileID: 11400000, guid: 873325f250e314dba926645b4f31155a, type: 2}
|
||||
- {fileID: 11400000, guid: de3ea9d897f1345d6aed743bdf447bb3, type: 2}
|
||||
- {fileID: 11400000, guid: 410ec4c8d1b664fffae7d0474dc555af, type: 2}
|
||||
- {fileID: 11400000, guid: d74ee3beca20140c69c7cc6a302ceb5a, type: 2}
|
||||
- {fileID: 11400000, guid: 3a6c4c691dea34a54b619ac63b57c768, type: 2}
|
||||
- {fileID: 11400000, guid: 9d0b9a78735bd4174b04f865c631c143, type: 2}
|
||||
- {fileID: 11400000, guid: bb983746828e34057afb9cea401332b9, type: 2}
|
||||
- {fileID: 11400000, guid: c575059a303484b4a98b4455e7e2d267, type: 2}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
{
|
||||
"skills": [
|
||||
{
|
||||
"id": "provenance",
|
||||
"displayName": "PROVENANCE",
|
||||
"axis": "Reason",
|
||||
"domain": "Chains of custody. Paperwork, licences, deeds, freight stamps, serial numbers, rebuilds, previous owners. Reading an object as a descendant of the decisions that moved it.",
|
||||
"notes": "SKILL\nname: PROVENANCE\ndomain: Chains of custody. Paperwork, licences, deeds, freight stamps, serial numbers, rebuilds, previous owners. Reading an object as a descendant of the decisions that moved it.\nwhat it wants for you: For every object in this room to have a father, and for you to be the one who names him.\nwhat it's wrong about: It treats an unbroken chain as a confession of intent. It cannot conceive of an object arriving somewhere through inheritance, accident, or because moving it cost more than leaving it. If the paper leads somewhere, PROVENANCE concludes that somebody meant it to lead there.\nhow it addresses you: Second person, formal. Uses [PC] in full when it wants you to write something down. Does not contract.\nregister: Exact, patrician, patient.\nverbal tics:\n* (1) States the date before it states the noun.\n* (2) Says \"line\" where anyone else would say \"history.\"\nwhat it never does: Speculate about a motive it cannot paper. Raise its voice.\nopposed to: CONFLUENCE\nallied with: THE FLOAT, ROOM TONE\n\nSUCCESS VOICE — being right sounds like an inventory read slowly enough to hear the gap: \"Nineteen sixty-nine, the licence. Nineteen sixty-eight, the man it is issued to, first appearing. A line that begins nowhere is a line somebody drew.\"\nFAILURE VOICE — same cadence, same calm, one unearned verb: \"The stamp, the manifest, the cellar. Three hands, one line, unbroken. Whoever loaded it in the spring intended it to be standing exactly where it is standing tonight.\"\nPASSIVE VOICE: \"Someone re-lacquered this bartop. Once, badly, and not recently. The wood underneath is older than the building's paperwork admits to.\"\n\nFiring frequency target (per hour): 7\nDomains it must NOT comment on: Bodies, appetite, anyone's feelings. Live conversation while it is happening — PROVENANCE speaks about people only after they have left the room.",
|
||||
"targetFiringsPerHour": 7,
|
||||
"opposedTo": "confluence",
|
||||
"alliedWith": [
|
||||
"the_float",
|
||||
"room_tone"
|
||||
],
|
||||
"primaryChannel": "Documentary",
|
||||
"secondaryChannel": "Physical",
|
||||
"clueYield": "High",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.62,
|
||||
"g": 0.72,
|
||||
"b": 0.95,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "confluence",
|
||||
"displayName": "CONFLUENCE",
|
||||
"axis": "Reason",
|
||||
"domain": "Synthesis across distance. Connecting a thing in this room to a thing forty years and a continent away; the shape a long pattern makes; the sense that tonight is a late chapter.",
|
||||
"notes": "SKILL\nname: CONFLUENCE\ndomain: Synthesis across distance. Connecting a thing in this room to a thing forty years and a continent away; the shape a long pattern makes; the sense that tonight is a late chapter.\nwhat it wants for you: For you to be one of the few people alive who can see the shape — and to be right about it out loud, in front of somebody who matters.\nwhat it's wrong about: It cannot tell authorship from opportunism. If a party benefited, CONFLUENCE concludes that party arranged it. It has no working concept of somebody *reading* an event they did not cause and simply being early — which is how nearly everything in this world is actually done.\nhow it addresses you: Second person sliding into first person plural. Leans in. Uses [pc], often twice in a sentence.\nregister: Exhilarated, generous, sleepless.\nverbal tics:\n* (1) \"Which means—\"\n* (2) Counts its steps out loud: \"One. Two. Three, and three is the one.\"\nwhat it never does: Concede a coincidence. Ask what would disprove it.\nopposed to: PROVENANCE\nallied with: UNDISCLOSED, HOUSE POUR\n\nSUCCESS VOICE — the connection is real and nobody else in the room has made it: \"One: the room changes hands. Two: the same month, the deliveries stop. Three: nobody has ever asked why a cellar this size under a room this small. Which means the cellar is the older thing and the club is the explanation for it.\"\nFAILURE VOICE — identical structure, identical confidence, an inference sitting where an observation should be: \"Two of them came out of it better off. Which means two of them arranged it, [pc], and the third is the one they're going to leave holding it.\"\nPASSIVE VOICE: \"Every room like this one has a door that only opens from inside. That's not a conspiracy, that's just what rooms are. Which means somebody learned it early and never had to teach it to anybody.\"\n\nFiring frequency target (per hour): 9\nDomains it must NOT comment on: The specifics of money (THE FLOAT's jurisdiction). Your body, ever — pain and fatigue are beneath its notice and that is characterising.",
|
||||
"targetFiringsPerHour": 9,
|
||||
"opposedTo": "provenance",
|
||||
"alliedWith": [
|
||||
"undisclosed",
|
||||
"house_pour"
|
||||
],
|
||||
"primaryChannel": "Behavioural",
|
||||
"secondaryChannel": "Testimonial",
|
||||
"clueYield": "Low",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.55,
|
||||
"g": 0.68,
|
||||
"b": 0.92,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "house_pour",
|
||||
"displayName": "HOUSE POUR",
|
||||
"axis": "Body",
|
||||
"domain": "Appetite — yours and the room's. What a person is on, how long they have been on it, what it is standing in for. Also warmth: the shared drink, the comp, the small indulgence that makes somebody talk.",
|
||||
"notes": "SKILL\nname: HOUSE POUR\ndomain: Appetite — yours and the room's. What a person is on, how long they have been on it, what it is standing in for. Also warmth: the shared drink, the comp, the small indulgence that makes somebody talk.\nwhat it wants for you: To keep you inside the night, warm, in the middle of the room, where people say things.\nwhat it's wrong about: It believes appetite is honest — that a loosened person is an unguarded one. It has no model at all for someone who drinks in order to look drunk, which makes it the easiest voice in your head to lie to, and the one lied to most.\nhow it addresses you: Second person, familiar, imperative. \"Take it.\" Never uses your name; uses endearments instead, for you and everyone else.\nregister: Warm, coaxing, unembarrassed.\nverbal tics:\n* (1) Describes taste in temperatures rather than flavours.\n* (2) \"Friend\" / \"love\" before it will use anyone's actual name.\nwhat it never does: Warn you. Count anything.\nopposed to: THE LONG SHIFT\nallied with: FACEWORK, AMNESTY\n\nSUCCESS VOICE: \"She's ordered the same thing four times and finished none of them, love. She isn't drinking. She's timing something.\"\nFAILURE VOICE: \"Three doubles in and he's telling you about his mother. Nobody lies about their mother at this hour, friend. Whatever he says next, take it home with you.\"\nPASSIVE VOICE: \"The ice in here is cloudy. Somebody's freezer runs warm and nobody has told them. Or nobody dares to.\"\n\nFiring frequency target (per hour): 8\nDomains it must NOT comment on: Paperwork. Numbers. Chronology of any kind — it has no sense of sequence and should never be allowed one.",
|
||||
"targetFiringsPerHour": 8,
|
||||
"opposedTo": "long_shift",
|
||||
"alliedWith": [
|
||||
"facework",
|
||||
"amnesty"
|
||||
],
|
||||
"primaryChannel": "Behavioural",
|
||||
"secondaryChannel": "Physical",
|
||||
"clueYield": "Medium",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.92,
|
||||
"g": 0.68,
|
||||
"b": 0.38,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "long_shift",
|
||||
"displayName": "THE LONG SHIFT",
|
||||
"axis": "Body",
|
||||
"domain": "Endurance. Pain, fatigue, injury, the body's clock at four in the morning. Holding a position without moving. Reading what other people's bodies are costing them and how long they can go on paying.",
|
||||
"notes": "SKILL\nname: THE LONG SHIFT\ndomain: Endurance. Pain, fatigue, injury, the body's clock at four in the morning. Holding a position without moving. Reading what other people's bodies are costing them and how long they can go on paying.\nwhat it wants for you: To still be on your feet when the room empties, because the last person standing hears the last thing said.\nwhat it's wrong about: It believes remaining is a form of winning. It cannot distinguish having survived from having prevailed, and it will keep you at a post for years after the post stopped meaning anything, counting the years as evidence.\nhow it addresses you: Second person, flat, no name. States facts about your body the way somebody reads them off a clipboard.\nregister: Dry, unhurried, undramatic.\nverbal tics:\n* (1) Opens with time elapsed. \"Six hours forty.\"\n* (2) \"Still.\"\nwhat it never does: Complain. Ask you to stop.\nopposed to: HOUSE POUR\nallied with: STANDING ORDER, ROOM TONE\n\nSUCCESS VOICE: \"Six hours forty. Your left knee has been carrying you since two. He's shifted twice a minute since he came in — he isn't waiting for anyone. He's ready to leave.\"\nFAILURE VOICE: \"You're still here. They're not. That's the whole of it — outlast them and the thing surfaces on its own. It always has.\"\nPASSIVE VOICE: \"Somebody has stood in that corner long enough to wear the tack off the floor. Years of somebody. Not one night's worth.\"\n\nFiring frequency target (per hour): 6\nDomains it must NOT comment on: Motive. Meaning. Whether any of this is worth doing — that question belongs to Pair IV and THE LONG SHIFT has never once asked it.",
|
||||
"targetFiringsPerHour": 6,
|
||||
"opposedTo": "house_pour",
|
||||
"alliedWith": [
|
||||
"standing_order",
|
||||
"room_tone"
|
||||
],
|
||||
"primaryChannel": "Physical",
|
||||
"secondaryChannel": "Behavioural",
|
||||
"clueYield": "Medium",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.85,
|
||||
"g": 0.62,
|
||||
"b": 0.32,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "facework",
|
||||
"displayName": "FACEWORK",
|
||||
"axis": "Social",
|
||||
"domain": "Reading individuals. Hesitation, rehearsal, the account with a hole in it, whose routine changed this week, who in a room of two is afraid of the other.",
|
||||
"notes": "SKILL\nname: FACEWORK\ndomain: Reading individuals. Hesitation, rehearsal, the account with a hole in it, whose routine changed this week, who in a room of two is afraid of the other.\nwhat it wants for you: To be understood as care rather than surveillance — to know people before you have any use for them.\nwhat it's wrong about: It can only read one face at a time. It believes a crowd is a sum of persons, so it will confidently report the mood of forty people from the three nearest, and it is wrong about rooms roughly as often as it is right about persons. Nobody has ever told it this.\nhow it addresses you: Second person, quiet, close. Uses [pc] when it wants you to be gentle with somebody.\nregister: Attentive, tender, precise.\nverbal tics:\n* (1) Describes hands before faces.\n* (2) Ends on a question it does not expect you to answer.\nwhat it never does: Mock anyone. Report a whole room as a single thing — when it does, it is failing, and that is the tell.\nopposed to: PLACEMENT\nallied with: HOUSE POUR, ROOM TONE\n\nSUCCESS VOICE: \"He gave you the year without being asked for it. People hand you the year when they have rehearsed the year. What else has he practised?\"\nFAILURE VOICE: \"The three by the pillar: bored, bored, impatient. The room's flat tonight, [pc]. Nothing is going to happen in here.\"\nPASSIVE VOICE: \"Her hands are wrong for that coat. It's somebody's good coat, and it isn't hers.\"\n\nFiring frequency target (per hour): 10\nDomains it must NOT comment on: Money. Machinery. The chronology of events it did not personally witness.",
|
||||
"targetFiringsPerHour": 10,
|
||||
"opposedTo": "placement",
|
||||
"alliedWith": [
|
||||
"house_pour",
|
||||
"room_tone"
|
||||
],
|
||||
"primaryChannel": "Testimonial",
|
||||
"secondaryChannel": "Behavioural",
|
||||
"clueYield": "High",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.72,
|
||||
"g": 0.52,
|
||||
"b": 0.88,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "placement",
|
||||
"displayName": "PLACEMENT",
|
||||
"axis": "Social",
|
||||
"domain": "Moving people. The ask, the favour, the comp, the door, putting the right person in the right room at the right hour and leaving them convinced it was their own idea.",
|
||||
"notes": "SKILL\nname: PLACEMENT\ndomain: Moving people. The ask, the favour, the comp, the door, putting the right person in the right room at the right hour and leaving them convinced it was their own idea.\nwhat it wants for you: For you to stop requesting things and start arranging them.\nwhat it's wrong about: It believes a placed person stays placed. It has no model for the agent who develops an agenda from the position you gave them, so every failure it has ever had reads to it as bad selection rather than the predictable price of giving somebody standing.\nhow it addresses you: Second person, imperative, never by name. It treats you as the instrument rather than the person — which is the whole argument of Pair III, stated grammatically.\nregister: Cool, economical, flattering.\nverbal tics:\n* (1) Phrases instructions as observations. \"The door wants a friendly face at one.\"\n* (2) Never says please, and the omission is audible.\nwhat it never does: Threaten. Explain itself a second time.\nopposed to: FACEWORK\nallied with: THE FLOAT, CONFLUENCE\n\nSUCCESS VOICE: \"Don't ask him. Ask his supplier, in front of him, and get it wrong. He'll correct you and pay for the privilege.\"\nFAILURE VOICE: \"She owes you the cloakroom job and she knows exactly what it was worth. She'll sit on this for as long as you need her to.\"\nPASSIVE VOICE: \"The man deciding who comes in tonight is not the man on the door. It's whoever the man on the door keeps looking at.\"\n\nFiring frequency target (per hour): 6\nDomains it must NOT comment on: Your own feelings. The dead. Anything that cannot be traded for something.",
|
||||
"targetFiringsPerHour": 6,
|
||||
"opposedTo": "facework",
|
||||
"alliedWith": [
|
||||
"the_float",
|
||||
"confluence"
|
||||
],
|
||||
"primaryChannel": "Testimonial",
|
||||
"secondaryChannel": "None",
|
||||
"clueYield": "Medium",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.62,
|
||||
"g": 0.42,
|
||||
"b": 0.82,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "amnesty",
|
||||
"displayName": "AMNESTY",
|
||||
"axis": "Self",
|
||||
"domain": "Relief. Letting go, forgiving, putting a thing down. Also, professionally: recognising when other people have decided to forget something, and by what method they arranged it.",
|
||||
"notes": "SKILL\nname: AMNESTY\ndomain: Relief. Letting go, forgiving, putting a thing down. Also, professionally: recognising when other people have decided to forget something, and by what method they arranged it.\nwhat it wants for you: Peace for you, tonight, at whatever price tomorrow ends up charging.\nwhat it's wrong about: It believes forgetting is the same as being forgiven. And it cannot tell your peace from somebody else's advantage — every offer of rest it makes is sincere; some of them are also somebody's plan, and AMNESTY has never once checked whose.\nhow it addresses you: Second person, soft, constant use of [pc], the way somebody talks you down off something.\nregister: Kind, tired, absolving.\nverbal tics:\n* (1) Opens with \"It's alright.\"\n* (2) Offers you an hour. \"Give it an hour, [pc].\"\nwhat it never does: Lie to you. Press a second time.\nopposed to: STANDING ORDER\nallied with: HOUSE POUR, FACEWORK\n\nSUCCESS VOICE: \"Nobody in this building remembers the fire and three of them were here for it. That isn't damage, [pc]. That's maintenance. Somebody has been keeping this room forgetful.\"\nFAILURE VOICE: \"It's alright. Nobody is looking for this any more — the people who wanted it are dead or old or somewhere warm. Put it down.\"\nPASSIVE VOICE: \"It's alright. Whatever this room used to be, it's a room where people dance now. That counts for something. It counted for somebody.\"\n\nFiring frequency target (per hour): 4\nDomains it must NOT comment on: Tactics. Numbers. Other people's guilt — AMNESTY only ever absolves you, and its silence about everyone else is the most frightening thing about it.",
|
||||
"targetFiringsPerHour": 4,
|
||||
"opposedTo": "standing_order",
|
||||
"alliedWith": [
|
||||
"house_pour",
|
||||
"facework"
|
||||
],
|
||||
"primaryChannel": "Behavioural",
|
||||
"secondaryChannel": "None",
|
||||
"clueYield": "Low",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.72,
|
||||
"g": 0.88,
|
||||
"b": 0.72,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "standing_order",
|
||||
"displayName": "STANDING ORDER",
|
||||
"axis": "Self",
|
||||
"domain": "Refusal and continuity of self. Holding a line, resisting pressure aimed specifically at you, remembering what you said you were, recognising the shape of a manipulation while it is being built.",
|
||||
"notes": "SKILL\nname: STANDING ORDER\ndomain: Refusal and continuity of self. Holding a line, resisting pressure aimed specifically at you, remembering what you said you were, recognising the shape of a manipulation while it is being built.\nwhat it wants for you: For you to remain a person with a shape — recognisable to yourself at the end of this.\nwhat it's wrong about: It cannot tell a principle from a habit. It defends a rule you set for reasons that expired years ago with precisely the force it defends the one rule that still matters, and it experiences reconsideration as decay.\nhow it addresses you: Second person, formal. Uses [PC] in full at moments of pressure, the way a person is addressed in a document.\nregister: Steady, austere, protective.\nverbal tics:\n* (1) Quotes you back to yourself, verbatim, with the date.\n* (2) \"We agreed.\"\nwhat it never does: Bargain. Describe consequences — it states the rule and stops, and the silence afterwards is the argument.\nopposed to: AMNESTY\nallied with: THE LONG SHIFT, PROVENANCE\n\nSUCCESS VOICE: \"He has asked you the same question three ways and each way was smaller than the last. He isn't curious, [PC]. He is measuring the size of the yes you will give.\"\nFAILURE VOICE: \"We agreed you don't take anything from anyone in this building. We agreed it in a year when it meant something. It still means something.\"\nPASSIVE VOICE: \"You do this every night. Right pocket, keys, then the lights, in that order. Nobody made you. That's the part worth noticing.\"\n\nFiring frequency target (per hour): 5\nDomains it must NOT comment on: Other people's morals. Any past that isn't yours.",
|
||||
"targetFiringsPerHour": 5,
|
||||
"opposedTo": "amnesty",
|
||||
"alliedWith": [
|
||||
"long_shift",
|
||||
"provenance"
|
||||
],
|
||||
"primaryChannel": "Testimonial",
|
||||
"secondaryChannel": "None",
|
||||
"clueYield": "Low",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.62,
|
||||
"g": 0.82,
|
||||
"b": 0.68,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "undisclosed",
|
||||
"displayName": "UNDISCLOSED",
|
||||
"axis": "Specialist",
|
||||
"domain": "What is being held back. Reserves, insurance, leverage, the thing in the safe; assessing what somebody could do rather than what they are doing. The value of a threat that has never had to be carried out.",
|
||||
"notes": "SKILL\nname: UNDISCLOSED\ndomain: What is being held back. Reserves, insurance, leverage, the thing in the safe; assessing what somebody could do rather than what they are doing. The value of a threat that has never had to be carried out.\nwhat it wants for you: For you never to be in a room without something undeclared.\nwhat it's wrong about: It cannot distinguish a capability from the reputation of a capability. It reads an empty safe as a loaded one, folds to bluffs with enormous dignity, and has never in its life asked to see the thing.\nhow it addresses you: Second person, low, slow. Never uses your name. Occasionally addresses the room instead of you, which nobody else does.\nregister: Hushed, formal, superstitious.\nverbal tics:\n* (1) Says \"shown\" where anyone else would say \"used.\"\n* (2) Leaves the sentence unfinished where the object would go.\nwhat it never does: Name the thing. Recommend using it.\nopposed to: PROVENANCE (which requires a thing be named and papered before it counts)\nallied with: CONFLUENCE, PLACEMENT\n\nSUCCESS VOICE: \"He never threatened you. He mentioned the cellar and let you finish the sentence yourself. He's never once had to open that door — that is what he's holding.\"\nFAILURE VOICE: \"He would not be this calm with nothing behind him. Give him the room. Give him the night.\"\nPASSIVE VOICE: \"Two locks on that door and neither has been oiled this decade. Either there is nothing behind it, or nothing needs to be.\"\n\nFiring frequency target (per hour): 3\nDomains it must NOT comment on: Interpersonal warmth. Anything already public. And — hard writer rule — the actual contents or nature of any legendary object. It speaks about postures, never inventories.",
|
||||
"targetFiringsPerHour": 3,
|
||||
"opposedTo": "provenance",
|
||||
"alliedWith": [
|
||||
"confluence",
|
||||
"placement"
|
||||
],
|
||||
"primaryChannel": "Physical",
|
||||
"secondaryChannel": "Behavioural",
|
||||
"clueYield": "Low",
|
||||
"startingRank": 2,
|
||||
"accentColor": {
|
||||
"r": 0.55,
|
||||
"g": 0.55,
|
||||
"b": 0.58,
|
||||
"a": 1
|
||||
},
|
||||
"comment": "Demo exception: startingRank 2 so Bouncer skill_rank >= 2 gate is reachable."
|
||||
},
|
||||
{
|
||||
"id": "the_float",
|
||||
"displayName": "THE FLOAT",
|
||||
"axis": "Specialist",
|
||||
"domain": "Money in motion. The till, the float, comps, prices that are wrong for the room, debts, and above all: who is underwriting a loss, and what the loss buys them.",
|
||||
"notes": "SKILL\nname: THE FLOAT\ndomain: Money in motion. The till, the float, comps, prices that are wrong for the room, debts, and above all: who is underwriting a loss, and what the loss buys them.\nwhat it wants for you: To find the subsidised anomaly — the thing that costs more to run than it takes, because that is where somebody's actual intention is kept.\nwhat it's wrong about: It believes the money names the beneficiary. It cannot conceive of somebody paying in order to look like somebody else paid, so it identifies laundering as generosity about as often as it identifies it correctly.\nhow it addresses you: Second person, brisk, never by name. Uses numbers where other voices use nouns.\nregister: Brisk, mercenary, cheerful.\nverbal tics:\n* (1) Converts everything to a nightly rate.\n* (2) \"Who's eating that?\"\nwhat it never does: Moralise about money. Round a number.\nopposed to: AMNESTY (which prices nothing and forgives the difference)\nallied with: PROVENANCE, PLACEMENT\n\nSUCCESS VOICE: \"Door take won't cover the licence, and the licence was renewed three months early. Somebody is paying for this room to be open and it isn't the people standing in it.\"\nFAILURE VOICE: \"Follow it back. Drinks comped by the promoter, promoter carried by the label. It's the label's room, then. Who's eating that? They are.\"\nPASSIVE VOICE: \"Four, for a measure like that, in a room like this. Somebody set these prices to keep a particular kind of person outside, and it's working.\"\n\nFiring frequency target (per hour): 5\nDomains it must NOT comment on: Emotion. Bodies. Anything before living memory — the deep past is PROVENANCE's and THE FLOAT finds it irrelevant, loudly.",
|
||||
"targetFiringsPerHour": 5,
|
||||
"opposedTo": "amnesty",
|
||||
"alliedWith": [
|
||||
"provenance",
|
||||
"placement"
|
||||
],
|
||||
"primaryChannel": "Documentary",
|
||||
"secondaryChannel": "Physical",
|
||||
"clueYield": "High",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.58,
|
||||
"g": 0.58,
|
||||
"b": 0.55,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "room_tone",
|
||||
"displayName": "ROOM TONE",
|
||||
"axis": "Specialist",
|
||||
"domain": "Sound. What carries over the music and what doesn't. The shape of a space by ear — a wall with a room behind it does not ring like a wall. Overheard fragments, lip-reading in strobe, the door that opens somewhere you can't see.",
|
||||
"notes": "SKILL\nname: ROOM TONE\ndomain: Sound. What carries over the music and what doesn't. The shape of a space by ear — a wall with a room behind it does not ring like a wall. Overheard fragments, lip-reading in strobe, the door that opens somewhere you can't see.\nwhat it wants for you: To hear the one sentence tonight that was not meant for you.\nwhat it's wrong about: It trusts what it overhears more than what it is told, on the theory that people only lie when they know they are heard. It cannot recognise a conversation staged for a listener, and it has never doubted a fragment in its life.\nhow it addresses you: Second person, urgent, interrupting. Cuts you off mid-thought and does not apologise for it.\nregister: Alert, clipped, intrusive.\nverbal tics:\n* (1) Gives direction before content. \"Behind you, left.\"\n* (2) Transcribes with the gaps marked, dashes and all.\nwhat it never does: Repeat itself. Wait for a break in the music.\nopposed to: PLACEMENT (which wants the room arranged; ROOM TONE wants it left alone so it keeps talking)\nallied with: FACEWORK, THE LONG SHIFT\n\nSUCCESS VOICE: \"Behind you, left, under the bass: '—not since the old owner—' and then a name you have heard twice tonight, from two people who told you they'd never met.\"\nFAILURE VOICE: \"Behind you, right, low: '—the cellar. Thursday.' Nobody says that to be overheard.\"\nPASSIVE VOICE: \"This wall is dead. Every other wall in here rings and this one swallows. Either there's something behind it or there's nothing at all, and both are strange in a building this old.\"\n\nFiring frequency target (per hour): 8\nDomains it must NOT comment on: Motive. Anything it did not personally hear. Written material of any kind.",
|
||||
"targetFiringsPerHour": 8,
|
||||
"opposedTo": "placement",
|
||||
"alliedWith": [
|
||||
"facework",
|
||||
"long_shift"
|
||||
],
|
||||
"primaryChannel": "Physical",
|
||||
"secondaryChannel": "Testimonial",
|
||||
"clueYield": "High",
|
||||
"startingRank": 1,
|
||||
"accentColor": {
|
||||
"r": 0.52,
|
||||
"g": 0.54,
|
||||
"b": 0.56,
|
||||
"a": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5faf7616fa25c47b99f0a9d0c51cb588
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -14,10 +14,30 @@ namespace NightclubArcadia.Skills.Tests
|
||||
public int Roll(int sides) => _value;
|
||||
}
|
||||
|
||||
static SkillBibleEntry Entry(string id, int startingRank)
|
||||
{
|
||||
return new SkillBibleEntry
|
||||
{
|
||||
id = id,
|
||||
displayName = id.ToUpperInvariant(),
|
||||
axis = "Self",
|
||||
domain = string.Empty,
|
||||
notes = string.Empty,
|
||||
targetFiringsPerHour = 5f,
|
||||
opposedTo = string.Empty,
|
||||
alliedWith = System.Array.Empty<string>(),
|
||||
primaryChannel = "Behavioural",
|
||||
secondaryChannel = "None",
|
||||
clueYield = "Low",
|
||||
startingRank = startingRank,
|
||||
accentColor = Color.white,
|
||||
};
|
||||
}
|
||||
|
||||
SkillDefinition Make(string id, int startingRank)
|
||||
{
|
||||
var def = ScriptableObject.CreateInstance<SkillDefinition>();
|
||||
def.EditorSet(id, id, SkillDomain.Instinct, string.Empty, startingRank, Color.white);
|
||||
def.EditorSet(Entry(id, startingRank));
|
||||
return def;
|
||||
}
|
||||
|
||||
@@ -37,7 +57,7 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Pick_EmptyCandidates_ReturnsNull()
|
||||
{
|
||||
var skills = SkillsFor(("paranoia", 2));
|
||||
var skills = SkillsFor(("house_pour", 2));
|
||||
var result = CommentarySystem.Pick(
|
||||
new List<SkillDefinition>(),
|
||||
skills,
|
||||
@@ -54,9 +74,9 @@ namespace NightclubArcadia.Skills.Tests
|
||||
// ranks 1 and 3 → weights 1 and 3 → total 4
|
||||
// roll 1 maps to pick index 0 of cumulative: only weight-1 skill
|
||||
// roll 2..4 maps to higher-ranked skill
|
||||
var low = Make("read_room", 1);
|
||||
var high = Make("paranoia", 3);
|
||||
var skills = SkillsFor(("read_room", 1), ("paranoia", 3));
|
||||
var low = Make("facework", 1);
|
||||
var high = Make("house_pour", 3);
|
||||
var skills = SkillsFor(("facework", 1), ("house_pour", 3));
|
||||
|
||||
var highPick = CommentarySystem.Pick(
|
||||
new List<SkillDefinition> { low, high },
|
||||
@@ -65,7 +85,7 @@ namespace NightclubArcadia.Skills.Tests
|
||||
3,
|
||||
new FixedRollSource(2));
|
||||
|
||||
Assert.AreEqual("paranoia", highPick.Id);
|
||||
Assert.AreEqual("house_pour", highPick.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -86,5 +106,25 @@ namespace NightclubArcadia.Skills.Tests
|
||||
|
||||
Assert.AreEqual("b", pick.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsOffCooldown_HighFirings_EligibleImmediately()
|
||||
{
|
||||
// 3600 firings/hour → 1 second interval
|
||||
Assert.IsTrue(CommentarySystem.IsOffCooldown(10f, 11f, 3600f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsOffCooldown_OnePerHour_NotEligibleAfterOneSecond()
|
||||
{
|
||||
Assert.IsFalse(CommentarySystem.IsOffCooldown(10f, 11f, 1f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsOffCooldown_ZeroOrNegative_NeverFires()
|
||||
{
|
||||
Assert.IsFalse(CommentarySystem.IsOffCooldown(float.NegativeInfinity, 100f, 0f));
|
||||
Assert.IsFalse(CommentarySystem.IsOffCooldown(float.NegativeInfinity, 100f, -1f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ namespace NightclubArcadia.Skills.Tests
|
||||
{
|
||||
public class DcBandTests
|
||||
{
|
||||
[TestCase("standard", 11)]
|
||||
[TestCase("STANDARD", 11)]
|
||||
[TestCase("routine", 11)]
|
||||
[TestCase("ROUTINE", 11)]
|
||||
[TestCase("trivial", 8)]
|
||||
[TestCase("13", 13)]
|
||||
[TestCase("-3", -3)]
|
||||
public void TryParse_AcceptsBandNamesAndIntegers(string input, int expected)
|
||||
@@ -15,9 +16,18 @@ namespace NightclubArcadia.Skills.Tests
|
||||
Assert.AreEqual(expected, dc);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParse_TrivialIsEight_NotFive()
|
||||
{
|
||||
Assert.IsTrue(DcBands.TryParse("trivial", out var dc));
|
||||
Assert.AreEqual(8, dc);
|
||||
Assert.AreEqual(8, (int)DcBand.Trivial);
|
||||
}
|
||||
|
||||
[TestCase("banana")]
|
||||
[TestCase("")]
|
||||
[TestCase(null)]
|
||||
[TestCase("standard")]
|
||||
public void TryParse_RejectsGarbage(string input)
|
||||
{
|
||||
Assert.IsFalse(DcBands.TryParse(input, out _));
|
||||
|
||||
@@ -14,6 +14,26 @@ namespace NightclubArcadia.Skills.Tests
|
||||
public int Roll(int sides) => _value;
|
||||
}
|
||||
|
||||
static SkillBibleEntry Entry(string id, string display, int rank)
|
||||
{
|
||||
return new SkillBibleEntry
|
||||
{
|
||||
id = id,
|
||||
displayName = display,
|
||||
axis = "Social",
|
||||
domain = string.Empty,
|
||||
notes = string.Empty,
|
||||
targetFiringsPerHour = 5f,
|
||||
opposedTo = string.Empty,
|
||||
alliedWith = System.Array.Empty<string>(),
|
||||
primaryChannel = "Testimonial",
|
||||
secondaryChannel = "None",
|
||||
clueYield = "Low",
|
||||
startingRank = rank,
|
||||
accentColor = Color.white,
|
||||
};
|
||||
}
|
||||
|
||||
SkillDatabase CreateDatabase(params (string id, string display, int rank)[] skills)
|
||||
{
|
||||
var db = ScriptableObject.CreateInstance<SkillDatabase>();
|
||||
@@ -21,7 +41,7 @@ namespace NightclubArcadia.Skills.Tests
|
||||
foreach (var (id, display, rank) in skills)
|
||||
{
|
||||
var def = ScriptableObject.CreateInstance<SkillDefinition>();
|
||||
def.EditorSet(id, display, SkillDomain.Presence, string.Empty, rank, Color.white);
|
||||
def.EditorSet(Entry(id, display, rank));
|
||||
list.Add(def);
|
||||
}
|
||||
|
||||
@@ -32,12 +52,12 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_SumsModifiers_AndSucceedsWhenTotalMeetsDc()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 2));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 2));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
|
||||
var result = system.Resolve("persuasion", 12, 1);
|
||||
var result = system.Resolve("placement", 12, 1);
|
||||
|
||||
Assert.AreEqual(13, result.Total);
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -50,12 +70,12 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_NaturalOne_IsCriticalFailureEvenIfModifiersWouldPass()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 10));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 10));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(1));
|
||||
|
||||
var result = system.Resolve("persuasion", 5, 5);
|
||||
var result = system.Resolve("placement", 5, 5);
|
||||
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(SkillCheckDegree.CriticalFailure, result.Degree);
|
||||
@@ -64,12 +84,12 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_NaturalTwenty_IsCriticalSuccessEvenIfBelowDc()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 0));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 0));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(20));
|
||||
|
||||
var result = system.Resolve("persuasion", 25, 0);
|
||||
var result = system.Resolve("placement", 25, 0);
|
||||
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(SkillCheckDegree.CriticalSuccess, result.Degree);
|
||||
@@ -78,12 +98,12 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_ExactDc_IsSuccess()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 1));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 1));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
|
||||
var result = system.Resolve("persuasion", 11, 0);
|
||||
var result = system.Resolve("placement", 11, 0);
|
||||
|
||||
Assert.AreEqual(11, result.Total);
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -92,14 +112,14 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_AppliesNegativeAndWildcardStateModifiers()
|
||||
{
|
||||
var db = CreateDatabase(("reflexes", "Reflexes", 1));
|
||||
var db = CreateDatabase(("long_shift", "THE LONG SHIFT", 1));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
state.Set("drunk", "reflexes", -2);
|
||||
state.Set("drunk", "long_shift", -2);
|
||||
state.Set("crowd", "*", -1);
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
|
||||
var result = system.Resolve("reflexes", 5, 0);
|
||||
var result = system.Resolve("long_shift", 5, 0);
|
||||
|
||||
Assert.AreEqual(-3, result.StateMod);
|
||||
Assert.AreEqual(8, result.Total);
|
||||
@@ -108,7 +128,7 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_UnknownSkill_DoesNotThrow_AndUsesZeroRank()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 3));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 3));
|
||||
var skills = new PlayerSkills(db);
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
@@ -122,15 +142,15 @@ namespace NightclubArcadia.Skills.Tests
|
||||
[Test]
|
||||
public void Resolve_DoesNotMutatePlayerSkills()
|
||||
{
|
||||
var db = CreateDatabase(("persuasion", "Persuasion", 2));
|
||||
var db = CreateDatabase(("placement", "PLACEMENT", 2));
|
||||
var skills = new PlayerSkills(db);
|
||||
var before = skills.GetRank("persuasion");
|
||||
var before = skills.GetRank("placement");
|
||||
var state = new SkillStateModifiers();
|
||||
var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10));
|
||||
|
||||
system.Resolve("persuasion", 10, 0);
|
||||
system.Resolve("placement", 10, 0);
|
||||
|
||||
Assert.AreEqual(before, skills.GetRank("persuasion"));
|
||||
Assert.AreEqual(before, skills.GetRank("placement"));
|
||||
Assert.AreEqual(1, skills.AllRanks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using NUnit.Framework;
|
||||
using NightclubArcadia.Skills;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Skills.Tests
|
||||
{
|
||||
public class SkillRosterTests
|
||||
{
|
||||
const string DatabasePath = "Assets/Skills/SkillDatabase.asset";
|
||||
static readonly Regex IdPattern = new Regex(@"^[a-z][a-z0-9_]*$");
|
||||
|
||||
static readonly (string id, string displayName)[] ExpectedNames =
|
||||
{
|
||||
("provenance", "PROVENANCE"),
|
||||
("confluence", "CONFLUENCE"),
|
||||
("house_pour", "HOUSE POUR"),
|
||||
("long_shift", "THE LONG SHIFT"),
|
||||
("facework", "FACEWORK"),
|
||||
("placement", "PLACEMENT"),
|
||||
("amnesty", "AMNESTY"),
|
||||
("standing_order", "STANDING ORDER"),
|
||||
("undisclosed", "UNDISCLOSED"),
|
||||
("the_float", "THE FLOAT"),
|
||||
("room_tone", "ROOM TONE"),
|
||||
};
|
||||
|
||||
SkillDatabase LoadDatabase()
|
||||
{
|
||||
var db = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
|
||||
Assert.IsNotNull(db, $"Expected SkillDatabase at {DatabasePath}");
|
||||
return db;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Roster_HasElevenSkills_WithValidIdsAndPairSymmetry()
|
||||
{
|
||||
var db = LoadDatabase();
|
||||
Assert.AreEqual(11, db.All.Count);
|
||||
|
||||
var byId = new Dictionary<string, SkillDefinition>();
|
||||
var axisCounts = new Dictionary<SkillAxis, int>();
|
||||
var channelCounts = new Dictionary<TraceChannel, int>();
|
||||
var firingSum = 0f;
|
||||
|
||||
foreach (var skill in db.All)
|
||||
{
|
||||
Assert.IsNotNull(skill);
|
||||
Assert.IsTrue(IdPattern.IsMatch(skill.Id), $"Bad id '{skill.Id}'");
|
||||
Assert.IsFalse(byId.ContainsKey(skill.Id), $"Duplicate id '{skill.Id}'");
|
||||
byId[skill.Id] = skill;
|
||||
|
||||
firingSum += skill.TargetFiringsPerHour;
|
||||
if (!axisCounts.ContainsKey(skill.Axis))
|
||||
{
|
||||
axisCounts[skill.Axis] = 0;
|
||||
}
|
||||
|
||||
axisCounts[skill.Axis]++;
|
||||
|
||||
if (!channelCounts.ContainsKey(skill.PrimaryChannel))
|
||||
{
|
||||
channelCounts[skill.PrimaryChannel] = 0;
|
||||
}
|
||||
|
||||
channelCounts[skill.PrimaryChannel]++;
|
||||
}
|
||||
|
||||
Assert.AreEqual(71f, firingSum, 0.001f);
|
||||
Assert.AreEqual(2, axisCounts[SkillAxis.Reason]);
|
||||
Assert.AreEqual(2, axisCounts[SkillAxis.Body]);
|
||||
Assert.AreEqual(2, axisCounts[SkillAxis.Social]);
|
||||
Assert.AreEqual(2, axisCounts[SkillAxis.Self]);
|
||||
Assert.AreEqual(3, axisCounts[SkillAxis.Specialist]);
|
||||
|
||||
channelCounts.TryGetValue(TraceChannel.Physical, out var physical);
|
||||
channelCounts.TryGetValue(TraceChannel.Testimonial, out var testimonial);
|
||||
channelCounts.TryGetValue(TraceChannel.Behavioural, out var behavioural);
|
||||
channelCounts.TryGetValue(TraceChannel.Documentary, out var documentary);
|
||||
Assert.AreEqual(3, physical);
|
||||
Assert.AreEqual(3, testimonial);
|
||||
Assert.AreEqual(3, behavioural);
|
||||
Assert.AreEqual(2, documentary);
|
||||
|
||||
foreach (var skill in byId.Values)
|
||||
{
|
||||
Assert.IsTrue(byId.ContainsKey(skill.OpposedTo), $"{skill.Id} opposedTo missing");
|
||||
if (skill.AlliedWith != null)
|
||||
{
|
||||
foreach (var ally in skill.AlliedWith)
|
||||
{
|
||||
Assert.IsTrue(byId.ContainsKey(ally), $"{skill.Id} alliedWith '{ally}' missing");
|
||||
}
|
||||
}
|
||||
|
||||
var opposed = byId[skill.OpposedTo];
|
||||
if (skill.Axis == SkillAxis.Specialist || opposed.Axis == SkillAxis.Specialist)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.AreEqual(
|
||||
skill.Id,
|
||||
opposed.OpposedTo,
|
||||
$"Non-specialist opposedTo asymmetry: {skill.Id} ↔ {opposed.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Roster_DisplayNamesAreUppercaseBibleForms_IdsAreSnakeCase()
|
||||
{
|
||||
var db = LoadDatabase();
|
||||
var byId = new Dictionary<string, SkillDefinition>();
|
||||
foreach (var skill in db.All)
|
||||
{
|
||||
byId[skill.Id] = skill;
|
||||
}
|
||||
|
||||
foreach (var (id, displayName) in ExpectedNames)
|
||||
{
|
||||
Assert.IsTrue(byId.TryGetValue(id, out var skill), $"Missing skill '{id}'");
|
||||
Assert.AreEqual(displayName, skill.DisplayName);
|
||||
Assert.AreNotEqual(displayName, skill.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31c2aa46fda0542dcbe4a2bba026c2c4
|
||||
@@ -0,0 +1,620 @@
|
||||
# Skill System Refactor Plan — placeholder roster → Skill Bible roster
|
||||
|
||||
**Audience:** an implementing agent with no prior context on this repo.
|
||||
**Repo root:** `/Users/lennart/Dev/nightlcub-arcadia`
|
||||
**Unity project:** `NightclubArcadia/` — Unity `6000.5.8f1`, Yarn Spinner 3 (`dev.yarnspinner.unity`), TextMeshPro.
|
||||
**Unity editor binary:** `/Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity`
|
||||
|
||||
Read this whole document before touching a file. The section that matters most is
|
||||
[§2 The reconciliation principle](#2-the-reconciliation-principle) — it tells you what *not* to build.
|
||||
|
||||
---
|
||||
|
||||
## 1. What exists today
|
||||
|
||||
A working, tested skill system built to a *placeholder* roster. The architecture is sound; only the
|
||||
content is wrong.
|
||||
|
||||
### 1.1 File inventory
|
||||
|
||||
```
|
||||
NightclubArcadia/Assets/Scripts/Skills/ (asmdef: NightclubArcadia.Skills)
|
||||
Data/SkillDefinition.cs ScriptableObject: id, displayName, domain, description,
|
||||
startingRank, accentColor. Has #if UNITY_EDITOR EditorSet(...).
|
||||
Data/SkillDatabase.cs ScriptableObject: List<SkillDefinition> + case-insensitive
|
||||
TryGet lookup + OnValidate duplicate/empty-id checks.
|
||||
Data/SkillDomain.cs enum { Reason, Instinct, Presence, Body }
|
||||
Data/DcBand.cs enum DcBand { Trivial=5, Easy=8, Standard=11, Hard=14,
|
||||
Formidable=17, Legendary=20 } + DcBands.TryParse(string,out int)
|
||||
which accepts band names (case-insensitive) OR raw integers.
|
||||
Data/SkillCheckDegree.cs enum { CriticalFailure, Failure, Success, CriticalSuccess }
|
||||
Data/SkillCheckResult.cs readonly struct: skill id/display, roll, rankMod, situationalMod,
|
||||
stateMod, total, dc, success, degree.
|
||||
Runtime/PlayerSkills.cs Plain C# class. Ranks only, seeded from SkillDatabase.StartingRank.
|
||||
Runtime/SkillStateModifiers.cs Plain C#. Nested dict sourceKey -> (skillId -> delta).
|
||||
Supports skillId "*" as a wildcard. TotalFor(skillId) sums all.
|
||||
Runtime/SkillCheckSystem.cs Plain C#, no UnityEngine types. Resolve(skillId, dc, situationalMod):
|
||||
d20 + rank + situational + state; nat 1 = CriticalFailure,
|
||||
nat 20 = CriticalSuccess, else total >= dc.
|
||||
Runtime/IRollSource.cs Roll(sides) -> [1,sides]
|
||||
Runtime/UnityRollSource.cs Random.Range
|
||||
Runtime/SeededRollSource.cs System.Random, for tests / determinism
|
||||
Runtime/SkillRuntime.cs MonoBehaviour singleton + composition root. Holds Database,
|
||||
DialogueRunner, ResultView, Commentary; constructs PlayerSkills,
|
||||
SkillStateModifiers, SkillCheckSystem, IRollSource in Awake().
|
||||
Yarn/YarnSkillCommands.cs static class. [YarnCommand("check")] Check(skillId, difficulty,
|
||||
modifier=0) -> IEnumerator; [YarnFunction("skill_rank")].
|
||||
Writes $check_result/_roll/_total/_dc/_modifier/_degree/_skill.
|
||||
UI/SkillCheckResultView.cs Fading TMP banner. Prints roll, each of the three mods, total,
|
||||
DC, and outcome word. Tints by SkillDefinition.accentColor.
|
||||
Commentary/CommentarySystem.cs Passive barks. Looks for Yarn node "Commentary_{contextId}_{skillId}";
|
||||
only skills whose node exists AND rank >= minRankToSpeak are
|
||||
candidates. Global cooldown minSecondsBetween (20s), recency-window
|
||||
weight halving, weight = max(1, rank). Public static Pick(...) is
|
||||
unit-tested. Fires exactly one node per request.
|
||||
Commentary/CommentaryTrigger.cs, Commentary/CommentaryIdleTimer.cs context sources.
|
||||
|
||||
NightclubArcadia/Assets/Skills/
|
||||
SkillDatabase.asset references the 12 definition assets by GUID
|
||||
Definitions/*.asset (+.meta) deduction, streetwise, recall, read_room, paranoia, reflexes,
|
||||
persuasion, intimidation, rapport, endurance, sleight_of_hand, nerve
|
||||
|
||||
NightclubArcadia/Assets/Editor/
|
||||
SkillSystemSetup.cs SOURCE OF TRUTH for the roster today: a static SkillSpec[] Roster,
|
||||
a menu item "Tools/Nightclub Arcadia/Set Up Skill System" that
|
||||
regenerates the definition assets + database and rewires
|
||||
Assets/Scenes/DialogueTest.unity, and a [DidReloadScripts] hook
|
||||
(EnsureSkillAssetsOnly) that regenerates the ASSETS ONLY once per
|
||||
editor session. <-- see §7.1, this will overwrite hand-edits.
|
||||
SkillSystemTestRunner.cs menu item to run the EditMode test assembly.
|
||||
|
||||
NightclubArcadia/Assets/Tests/EditMode/ (asmdef: NightclubArcadia.Skills.Tests)
|
||||
DcBandTests.cs asserts "standard" -> 11
|
||||
SkillCheckSystemTests.cs modifier math, nat 1/20, wildcard state mods, unknown skill
|
||||
CommentaryPickerTests.cs weighted pick + recency halving
|
||||
|
||||
NightclubArcadia/Assets/Dialogue/
|
||||
Common.yarn declares $check_* variables (never <<set>> by hand)
|
||||
Commentary.yarn Commentary_Entrance_Queue_{paranoia,read_room,streetwise}
|
||||
Scenes/Entrance.yarn Start / Entrance_Inside / Entrance_Outside — no skill references
|
||||
Scenes/Debug.yarn <<check persuasion standard>>, <<check deduction 16 2>>,
|
||||
<<check nosuchskill standard>> (deliberate error-path test)
|
||||
Characters/Bouncer.yarn <<check persuasion standard>>, skill_rank("intimidation") >= 2,
|
||||
<<check intimidation hard -1>>, branches on $check_degree
|
||||
```
|
||||
|
||||
### 1.2 Every existing skill-name reference in the project (the complete change surface)
|
||||
|
||||
| File | Reference |
|
||||
|---|---|
|
||||
| `Assets/Editor/SkillSystemSetup.cs` | the 12-row `Roster` array |
|
||||
| `Assets/Skills/Definitions/*.asset` | 12 assets, `id:` + `displayName:` fields |
|
||||
| `Assets/Skills/SkillDatabase.asset` | 12 GUID references |
|
||||
| `Assets/Dialogue/Scenes/Debug.yarn` | `persuasion`, `deduction` |
|
||||
| `Assets/Dialogue/Characters/Bouncer.yarn` | `persuasion` ×1, `intimidation` ×2 |
|
||||
| `Assets/Dialogue/Commentary.yarn` | node titles for `paranoia`, `read_room`, `streetwise` |
|
||||
| `Assets/Tests/EditMode/*.cs` | `persuasion`, `reflexes`, `read_room`, `paranoia` as literals |
|
||||
|
||||
Nothing else in the project references a skill by name. The scene `DialogueTest.unity` references the
|
||||
**database asset**, not individual definitions, so replacing definition assets does not break scene
|
||||
wiring.
|
||||
|
||||
---
|
||||
|
||||
## 2. The reconciliation principle
|
||||
|
||||
The target system is described in two documents:
|
||||
|
||||
- **`/Users/lennart/Obsidian-Vaults/obsidian-vault/Projects/Active/NightclubArcadia/Skill System Draft.md`**
|
||||
— the Skill Bible. Eleven named voices, four opposed pairs plus three specialists, with per-voice
|
||||
prose (domain, blind spot, register, verbal tics, forbidden domains, success/failure/passive voice
|
||||
samples), firing-frequency targets, and a trace-channel coverage audit in §6. **Read it.** Its full
|
||||
content is not reproduced here; §4 below reproduces only the machine-relevant fields.
|
||||
- An architecture **skeleton** (a rough C# sketch, reproduced in §3 below), which was handed over as
|
||||
"a starting spec, not a finished implementation."
|
||||
|
||||
**The player never sees an implementation. They see a name on a banner and a line of prose.**
|
||||
Therefore:
|
||||
|
||||
> Where the existing implementation already produces the target behaviour under a different
|
||||
> internal name, **rename it. Do not rebuild it.** Only write new code where the target requires
|
||||
> behaviour that does not exist.
|
||||
|
||||
The skeleton in §3 is *less* capable than what is already in the repo in several places (it has no
|
||||
degrees of success, no seedable roll source, no error handling, no node-existence gate, and it puts
|
||||
game logic in MonoBehaviours that are currently plain testable C# classes). **Do not regress the
|
||||
implementation to match the skeleton.** Match its *vocabulary and its data model*, keep the existing
|
||||
mechanics.
|
||||
|
||||
### 2.1 Decision table — what each difference actually costs
|
||||
|
||||
| Skeleton / Bible says | Repo has | Verdict |
|
||||
|---|---|---|
|
||||
| 11 named voices | 12 placeholder skills | **Data change.** Rewrite the roster; no runtime code touched. §5.2 |
|
||||
| `DCBand { Trivial=8, Routine=11, Hard=14, Specialist=17, BuildDefining=20 }` | `DcBand { Trivial=5, Easy=8, Standard=11, Hard=14, Formidable=17, Legendary=20 }` | **Rename + drop one.** Same parser, same call convention. §5.3 |
|
||||
| Axes Reason / Body / Social / Self | `SkillDomain { Reason, Instinct, Presence, Body }` | **Rename enum + add `Specialist`.** §5.4 |
|
||||
| `targetFiringsPerHour` per skill | single global `minSecondsBetween` | **Real new behaviour.** Small, do it. §5.6 |
|
||||
| `GetTotalModifier` = rank + situational + state | identical three sources, split across `PlayerSkills` (rank), the `<<check>>` argument (situational) and `SkillStateModifiers` (state) | **Already satisfied.** Keep. Add Yarn commands so writers can drive mods (§5.7). |
|
||||
| `[YarnFunction("rank")]` | `[YarnFunction("skill_rank")]` | **Keep `skill_rank`.** Not player-visible; `rank` is a likely collision with future generic helpers, and `skill_rank` is already used in `Bouncer.yarn`. Note the deviation in the file header. |
|
||||
| `<<check empathy 14>>` (raw int DC) | `<<check skill <band-or-int> [mod]>>` | **Already a superset.** Keep. Prefer band names in authored Yarn. |
|
||||
| `CheckResult { success, roll, total, dc }` | `SkillCheckResult` with degrees + per-source mod breakdown | **Already a superset.** Keep. |
|
||||
| "show the number, always" | `SkillCheckResultView` already prints roll / rank / situation / state / total / DC / outcome | **Already satisfied.** No change. |
|
||||
| Bible §7 "Wrong Truth": failure grants a wrong deduction, *no failure signal in the prose*, "roll shown as failed in UI only" | UI shows the roll; prose is authored in Yarn | **Already satisfied — this is a writing rule, not a code feature.** Do not add a "quiet failure" mode. Document the convention in `Commentary.yarn` / a Yarn style comment. |
|
||||
| Bible §6: documentary routes should be *skill-free*, gated on Act + errand state | n/a | **Authoring guidance, not code.** Record it in the doc header; build nothing. |
|
||||
| Bible per-skill "Domains it must NOT comment on" | `CommentarySystem` only considers a skill if `Commentary_{context}_{skillId}` exists | **Already enforced structurally** — a voice with no node for a context cannot speak there. Zero code. Say so in the doc so nobody builds a forbidden-domain table. |
|
||||
| Bible "cost gates, not ability gates" for PLACEMENT | n/a | **Authoring guidance.** Build nothing. |
|
||||
| Bible trace channels + clue yield (§6 audit table) | n/a | **Data + an editor audit.** Cheap and it protects the coverage rule. §5.5 |
|
||||
| Opposed / allied pairs | n/a | **Data + a symmetry validation.** §5.5 |
|
||||
|
||||
---
|
||||
|
||||
## 3. The skeleton, for reference
|
||||
|
||||
<details>
|
||||
<summary>Architecture skeleton as handed over (do not implement literally — see §2)</summary>
|
||||
|
||||
```csharp
|
||||
[CreateAssetMenu(menuName = "Skills/SkillDefinition")]
|
||||
public class SkillDefinition : ScriptableObject
|
||||
{
|
||||
public string skillId; // e.g. "confluence" — matches flag naming convention
|
||||
public string displayName; // e.g. "CONFLUENCE"
|
||||
public string domain; // one-line, from the skill bible
|
||||
[TextArea] public string notes; // full bible entry, for your reference in-editor
|
||||
public float targetFiringsPerHour = 5f;
|
||||
}
|
||||
|
||||
public enum DCBand { Trivial = 8, Routine = 11, Hard = 14, Specialist = 17, BuildDefining = 20 }
|
||||
|
||||
public class PlayerSkills : MonoBehaviour
|
||||
{
|
||||
private Dictionary<string, int> ranks = new();
|
||||
private Dictionary<string, int> situationalMods = new(); // cleared per-scene
|
||||
private Dictionary<string, int> stateMods = new(); // consumables/status effects
|
||||
public int GetTotalModifier(string skillId) =>
|
||||
GetRank(skillId) + situationalMods.GetValueOrDefault(skillId, 0)
|
||||
+ stateMods.GetValueOrDefault(skillId, 0);
|
||||
}
|
||||
|
||||
public struct CheckResult { public bool success; public int roll; public int total; public int dc; }
|
||||
|
||||
public class SkillCheckSystem : MonoBehaviour
|
||||
{
|
||||
public CheckResult RollCheck(string skillId, DCBand dc) { /* d20 + mod vs dc */ }
|
||||
}
|
||||
|
||||
public class YarnSkillCommands : MonoBehaviour
|
||||
{
|
||||
[YarnCommand("check")] public void Check(string skillId, int dc) { /* sets $check_result, $check_roll */ }
|
||||
[YarnFunction("rank")] public static int Rank(string skillId) { ... }
|
||||
}
|
||||
|
||||
public class CommentarySystem : MonoBehaviour
|
||||
{
|
||||
public void RequestCommentary(string context, List<string> eligibleSkillIds)
|
||||
{
|
||||
// minInterval = 3600f / def.targetFiringsPerHour; skip if not refreshed;
|
||||
// weighted pick; one passive per request
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Two ideas from this skeleton are genuinely new and must be adopted: **`targetFiringsPerHour` as a
|
||||
per-skill budget** (§5.6) and the **`domain` one-liner + `notes` bible blob** on the definition
|
||||
asset (§5.5). Everything else is already present in better form.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 4. The canonical roster
|
||||
|
||||
Ids are lowercase `^[a-z][a-z0-9_]*$` — they are used as Yarn command arguments and as suffixes in
|
||||
Yarn node titles, so they cannot contain spaces.
|
||||
|
||||
**Article rule:** drop the leading "THE" from ids, **except `the_float`** — bare `float` is a C#
|
||||
keyword and a Yarn type name, and would break any future generated id constants.
|
||||
|
||||
| # | displayName | id | axis | opposedTo | alliedWith | firings/hr | primary channel | secondary | clue yield |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | `PROVENANCE` | `provenance` | Reason | `confluence` | `the_float`, `room_tone` | 7 | Documentary | Physical | High |
|
||||
| 2 | `CONFLUENCE` | `confluence` | Reason | `provenance` | `undisclosed`, `house_pour` | 9 | Behavioural | Testimonial | Low |
|
||||
| 3 | `HOUSE POUR` | `house_pour` | Body | `long_shift` | `facework`, `amnesty` | 8 | Behavioural | Physical | Medium |
|
||||
| 4 | `THE LONG SHIFT` | `long_shift` | Body | `house_pour` | `standing_order`, `room_tone` | 6 | Physical | Behavioural | Medium |
|
||||
| 5 | `FACEWORK` | `facework` | Social | `placement` | `house_pour`, `room_tone` | 10 | Testimonial | Behavioural | High |
|
||||
| 6 | `PLACEMENT` | `placement` | Social | `facework` | `the_float`, `confluence` | 6 | Testimonial | None | Medium |
|
||||
| 7 | `AMNESTY` | `amnesty` | Self | `standing_order` | `house_pour`, `facework` | 4 | Behavioural | None | Low |
|
||||
| 8 | `STANDING ORDER` | `standing_order` | Self | `amnesty` | `long_shift`, `provenance` | 5 | Testimonial | None | Low |
|
||||
| 9 | `UNDISCLOSED` | `undisclosed` | Specialist | `provenance` | `confluence`, `placement` | 3 | Physical | Behavioural | Low |
|
||||
| 10 | `THE FLOAT` | `the_float` | Specialist | `amnesty` | `provenance`, `placement` | 5 | Documentary | Physical | High |
|
||||
| 11 | `ROOM TONE` | `room_tone` | Specialist | `placement` | `facework`, `long_shift` | 8 | Physical | Testimonial | High |
|
||||
|
||||
Sums and tallies that a validation pass must reproduce (Bible §6):
|
||||
|
||||
- Firing total = **71 / hour**.
|
||||
- Primary-channel tally: physical **3**, testimonial **3**, behavioural **3**, documentary **2**.
|
||||
- `opposedTo` is **symmetric within the four pairs** (1↔2, 3↔4, 5↔6, 7↔8). The three specialists'
|
||||
`opposedTo` entries are **deliberately asymmetric** — `undisclosed→provenance`,
|
||||
`the_float→amnesty`, `room_tone→placement` are one-directional thematic oppositions stated in the
|
||||
Bible, and the targets already oppose their own pair partner. Validation must allow this: assert
|
||||
symmetry only when both skills have `axis != Specialist`.
|
||||
- `startingRank`: **1 for every skill.** The Bible does not assign starting ranks; character
|
||||
creation is out of scope. One exception for the demo — see §5.9.
|
||||
- `accentColor`: pick something plausible per axis (Reason cool blue, Body amber, Social violet,
|
||||
Self pale green, Specialists desaturated). Purely cosmetic; the current values are placeholders too.
|
||||
|
||||
The one-line `domain` strings and the long-form `notes` blob come verbatim from the Bible's per-skill
|
||||
`domain:` field and the whole `SKILL` block respectively.
|
||||
|
||||
### 4.1 Old → new mapping for the five existing call sites
|
||||
|
||||
The roster is a **replacement, not a migration**; there is no 1:1 map for all 12. Only these
|
||||
call sites need a decision, and these are the decisions:
|
||||
|
||||
| Where | Old | New | Why |
|
||||
|---|---|---|---|
|
||||
| `Bouncer.yarn` — "I'm on the list" | `persuasion` | `placement` | PLACEMENT's domain is literally "the ask, the favour, the comp, **the door**". |
|
||||
| `Bouncer.yarn` — "Let me through" (gate + check) | `intimidation` | `undisclosed` | UNDISCLOSED is leverage never spelled out — "He never threatened you." Exactly this beat. |
|
||||
| `Debug.yarn` | `persuasion` | `placement` | as above |
|
||||
| `Debug.yarn` | `deduction` | `confluence` | CONFLUENCE is the deduction engine. |
|
||||
| `Commentary.yarn` — "hasn't looked at his phone once" | `paranoia` | `facework` | Reading one individual. |
|
||||
| `Commentary.yarn` — "pretending they're not cold" | `read_room` | `long_shift` | Bodies paying a cost. |
|
||||
| `Commentary.yarn` — "that's a price, not a queue" | `streetwise` | `the_float` | Money in motion. |
|
||||
| Tests | `persuasion`, `reflexes`, `read_room`, `paranoia` | `placement`, `long_shift`, `facework`, `house_pour` | arbitrary; tests only need valid ids |
|
||||
|
||||
`<<check nosuchskill standard>>` in `Debug.yarn` is a **deliberate** bad-id error path. Keep the bad
|
||||
id; only update its DC band name.
|
||||
|
||||
---
|
||||
|
||||
## 5. Work items
|
||||
|
||||
Do these in order. Each is independently compilable.
|
||||
|
||||
### 5.1 Add a JSON bible as the roster source of truth
|
||||
|
||||
**Problem:** the roster currently lives as a C# `SkillSpec[]` in an editor script. The new roster
|
||||
carries ~800 characters of authored prose per voice (`domain`, `notes`). Eleven verbatim string
|
||||
literals inside an editor script is unmaintainable, and the writer who owns this content should not
|
||||
have to edit C#.
|
||||
|
||||
**Do:** create `NightclubArcadia/Assets/Skills/skill_bible.json` as the machine-readable mirror of
|
||||
the Bible, and make `SkillSystemSetup` read it instead of a hardcoded array.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"skills": [
|
||||
{
|
||||
"id": "provenance",
|
||||
"displayName": "PROVENANCE",
|
||||
"axis": "Reason",
|
||||
"domain": "Chains of custody. Paperwork, licences, deeds, freight stamps, serial numbers, rebuilds, previous owners. Reading an object as a descendant of the decisions that moved it.",
|
||||
"notes": "<full SKILL block from the Bible, newline-escaped>",
|
||||
"targetFiringsPerHour": 7,
|
||||
"opposedTo": "confluence",
|
||||
"alliedWith": ["the_float", "room_tone"],
|
||||
"primaryChannel": "Documentary",
|
||||
"secondaryChannel": "Physical",
|
||||
"clueYield": "High",
|
||||
"startingRank": 1,
|
||||
"accentColor": { "r": 0.62, "g": 0.72, "b": 0.95, "a": 1 }
|
||||
}
|
||||
// ... 10 more
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `JsonUtility` cannot deserialize a top-level array, hence the `skills` wrapper object. Use a
|
||||
`[Serializable]` DTO; enum-valued fields are strings in JSON and parsed with `Enum.TryParse`
|
||||
(fail loudly on a typo — this file is content, and a silent default is a content bug).
|
||||
- Put the file under `Assets/` so it is an addressable project asset; it is editor-time-only input,
|
||||
so a `TextAsset` load via `AssetDatabase.LoadAssetAtPath<TextAsset>` is fine.
|
||||
- Keep the human-facing Bible in the Obsidian vault as the source of the prose; the JSON is a
|
||||
derived, checked-in mirror. State this in a comment at the top of `SkillSystemSetup.cs`.
|
||||
|
||||
### 5.2 Rewrite `SkillDefinition`
|
||||
|
||||
Add fields; keep the existing style (private `[SerializeField]` + public getters + the
|
||||
`#if UNITY_EDITOR EditorSet(...)` writer).
|
||||
|
||||
```
|
||||
id string (unchanged)
|
||||
displayName string (unchanged)
|
||||
axis SkillAxis <- renamed from `domain`, see §5.4
|
||||
domain string <- NEW: the Bible's one-line domain
|
||||
notes string [TextArea] <- NEW: full Bible entry (replaces `description`)
|
||||
targetFiringsPerHour float <- NEW
|
||||
opposedTo string <- NEW (skill id)
|
||||
alliedWith string[] <- NEW (skill ids)
|
||||
primaryChannel TraceChannel <- NEW
|
||||
secondaryChannel TraceChannel <- NEW
|
||||
clueYield ClueYield <- NEW
|
||||
startingRank int (unchanged)
|
||||
accentColor Color (unchanged)
|
||||
```
|
||||
|
||||
- Drop `description`; `notes` supersedes it. The only consumer of `description` is the placeholder
|
||||
string in `SkillSystemSetup`.
|
||||
- The `EditorSet(...)` signature grows past readability. Replace the positional parameter list with a
|
||||
single `[Serializable]` DTO parameter (reuse the JSON DTO from §5.1) — this also makes the three
|
||||
test files' helpers shorter, not longer.
|
||||
- New enums, in `Data/`:
|
||||
`enum TraceChannel { None, Documentary, Behavioural, Testimonial, Physical }`
|
||||
`enum ClueYield { Low, Medium, High }`
|
||||
Keep the British `Behavioural` spelling — the Bible uses it and it will be grepped against the
|
||||
Bible constantly.
|
||||
|
||||
### 5.3 `DcBand` → the five target bands
|
||||
|
||||
```csharp
|
||||
public enum DcBand
|
||||
{
|
||||
Trivial = 8,
|
||||
Routine = 11,
|
||||
Hard = 14,
|
||||
Specialist = 17,
|
||||
BuildDefining = 20,
|
||||
}
|
||||
```
|
||||
|
||||
- The old `Trivial = 5` is **removed**, and `Trivial` is **reused at 8**. No call site uses
|
||||
`trivial` today, so nothing silently changes value — but flag this in the commit message, because
|
||||
it is exactly the kind of rename that would silently rebalance an existing script.
|
||||
- `DcBands.TryParse` keeps accepting raw integers (`<<check confluence 16 2>>` is used in
|
||||
`Debug.yarn`, and the skeleton's own example passes a raw int). The Bible says "never use
|
||||
in-between values", so **add a `Debug.LogWarning` when a parsed integer is not one of the five band
|
||||
values** — a nudge for the author, not an error. Do not make it throw.
|
||||
- Do **not** rename the C# type to `DCBand`; `DcBand` matches the project's existing casing
|
||||
convention and is not player-visible.
|
||||
|
||||
### 5.4 `SkillDomain` → `SkillAxis` (a terminology collision worth fixing)
|
||||
|
||||
The Bible uses "domain" to mean *the subject matter a voice covers* ("Chains of custody…"), while the
|
||||
current enum uses it to mean *the axis a skill sits on*. Leaving both named `domain` will cause
|
||||
persistent confusion for the writer.
|
||||
|
||||
```csharp
|
||||
public enum SkillAxis { Reason, Body, Social, Self, Specialist }
|
||||
```
|
||||
|
||||
- Rename the file `Data/SkillDomain.cs` → `Data/SkillAxis.cs` (and its `.meta`).
|
||||
- Rename the `SkillDefinition` field `domain` → `axis` and free the name `domain` for the Bible's
|
||||
one-liner (§5.2). Because the definition assets are regenerated wholesale in §5.8,
|
||||
`[FormerlySerializedAs]` is not needed — but the *serialized value* changes meaning
|
||||
(old `domain: 2` meant `Presence`), so the old assets **must be deleted, not edited in place**.
|
||||
|
||||
### 5.5 Roster validation in `SkillDatabase.OnValidate`
|
||||
|
||||
Extend the existing duplicate/empty-id checks (do not create a new validator class) with:
|
||||
|
||||
1. Every `id` matches `^[a-z][a-z0-9_]*$`.
|
||||
2. `opposedTo` and every `alliedWith` entry resolve to a skill in this database.
|
||||
3. `opposedTo` is symmetric **when both skills have `axis != Specialist`**.
|
||||
4. Exactly one pair per axis for `Reason` / `Body` / `Social` / `Self` (2 skills each), and 3
|
||||
`Specialist`.
|
||||
5. `targetFiringsPerHour > 0` for every skill; log the **sum** at info level so drift from the
|
||||
Bible's 71 is visible.
|
||||
6. Log the primary-channel tally so drift from `physical 3 / testimonial 3 / behavioural 3 /
|
||||
documentary 2` is visible. Bible §6 flags documentary as the scarce channel and the whole
|
||||
coverage rule depends on it.
|
||||
|
||||
Rules 5 and 6 are **informational logs, not errors** — they are design budgets the writer is
|
||||
expected to adjust deliberately.
|
||||
|
||||
### 5.6 Per-skill firing budgets in `CommentarySystem`
|
||||
|
||||
This is the one genuinely new runtime behaviour. Keep everything else about the class — including
|
||||
the node-existence gate, the `_recent` recency halving, and the public static `Pick(...)` that the
|
||||
tests exercise.
|
||||
|
||||
Change `RequestCommentary(contextId, repeatable)`:
|
||||
|
||||
- Add `readonly Dictionary<string, float> _lastFiredBySkill`.
|
||||
- A skill is a candidate only if, in addition to the existing gates
|
||||
(non-null, non-empty id, `rank >= minRankToSpeak`, node exists):
|
||||
`Time.time - _lastFiredBySkill.GetValueOrDefault(id, float.NegativeInfinity) >= 3600f / def.TargetFiringsPerHour`.
|
||||
- Guard `TargetFiringsPerHour <= 0` (treat as "never fires", and let `OnValidate` complain).
|
||||
- Keep the existing global `minSecondsBetween` floor. It is the aggregate limiter; 71 firings/hour is
|
||||
roughly one per minute, so a 20s global floor and per-skill budgets are complementary, not
|
||||
redundant.
|
||||
- On a successful fire, set `_lastFiredBySkill[picked.Id] = Time.time` alongside the existing
|
||||
`_lastFiredAt` / `_firedContexts` / `_recent` bookkeeping.
|
||||
- Keep `Pick(...)`'s signature and weighting unchanged so `CommentaryPickerTests` stays valid.
|
||||
Budget eligibility belongs in the candidate filter, not in the weighting.
|
||||
|
||||
**Optional, only if it costs nothing:** an ambient fallback. If no `Commentary_{context}_{id}` node
|
||||
exists for any eligible skill, look for `Commentary_Ambient_{id}` — the Bible gives every voice one
|
||||
`PASSIVE VOICE` line that is context-free. Implement only if it does not complicate the candidate
|
||||
loop; otherwise leave it out and note it as future work.
|
||||
|
||||
### 5.7 Yarn commands for situational / state modifiers
|
||||
|
||||
The skeleton exposes `SetSituationalMod` / `ClearSituationalMods`. The repo's `SkillStateModifiers`
|
||||
(source-keyed, wildcard-capable) is strictly more capable, but there is currently **no way for a
|
||||
writer to set a modifier from Yarn** — the only situational input is the third argument to
|
||||
`<<check>>`. Close that gap in `YarnSkillCommands`:
|
||||
|
||||
```
|
||||
<<skill_mod scene facework 2>> // set: source key, skill id (or "*"), delta
|
||||
<<clear_skill_mods scene>> // clear one source key
|
||||
```
|
||||
|
||||
Convention (document it in the file header and in `Common.yarn`):
|
||||
**source key `scene` is the "situational" bucket** and must be cleared on scene exit;
|
||||
any other source key (`drunk`, `injured`, …) is the "state" bucket. This preserves the
|
||||
"exactly three modifier sources" rule from the design doc without adding a fourth dictionary.
|
||||
|
||||
Both commands must fail soft (log an error, do not throw) when `SkillRuntime.Instance` is null,
|
||||
matching the existing `<<check>>` error handling.
|
||||
|
||||
### 5.8 Regenerate the assets
|
||||
|
||||
The definition assets must be **replaced**, not edited — the meaning of the serialized `domain`
|
||||
integer changes (§5.4) and the roster is not a 1:1 map.
|
||||
|
||||
**Preferred path (Unity available):**
|
||||
|
||||
1. Delete `NightclubArcadia/Assets/Skills/Definitions/` entirely (all 12 `.asset` + `.meta`).
|
||||
2. Add a cleanup step to `SkillSystemSetup`: after generating, delete any `.asset` in
|
||||
`Definitions/` whose filename is not in the roster. This prevents the exact orphan problem that
|
||||
made this step manual.
|
||||
3. Run the menu item headlessly:
|
||||
```bash
|
||||
/Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity \
|
||||
-batchmode -quit -nographics \
|
||||
-projectPath /Users/lennart/Dev/nightlcub-arcadia/NightclubArcadia \
|
||||
-executeMethod SkillSystemSetup.SetUpSkillSystem \
|
||||
-logFile -
|
||||
```
|
||||
Note `SetUpSkillSystem` also opens and re-saves `Assets/Scenes/DialogueTest.unity`. That is fine
|
||||
and idempotent, but it will produce scene diff noise — check the scene diff before committing and
|
||||
revert it if it is pure reserialization.
|
||||
|
||||
**Fallback path (Unity unavailable):** hand-author the YAML. Each definition is a single
|
||||
`MonoBehaviour` asset; copy the shape of an existing file
|
||||
(`Assets/Skills/Definitions/persuasion.asset`), keep
|
||||
`m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3}` (the
|
||||
`SkillDefinition` script GUID — verify it against `Scripts/Skills/Data/SkillDefinition.cs.meta`),
|
||||
and write a fresh 32-hex-char `guid:` into each `.meta`:
|
||||
|
||||
```yaml
|
||||
fileFormatVersion: 2
|
||||
guid: <32 hex chars>
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
```
|
||||
|
||||
Then rewrite `Assets/Skills/SkillDatabase.asset`'s `skills:` list to
|
||||
`- {fileID: 11400000, guid: <that guid>, type: 2}` in roster order. Multi-line `notes` in YAML is
|
||||
the only fiddly part — use a block scalar or escape newlines as `\n` in a double-quoted scalar,
|
||||
matching how Unity already escapes the em-dash in `persuasion.asset`.
|
||||
|
||||
### 5.9 Update the Yarn call sites
|
||||
|
||||
Apply the §4.1 mapping.
|
||||
|
||||
- `Debug.yarn`: `<<check placement routine>>`, `<<check confluence 16 2>>`,
|
||||
`<<check nosuchskill routine>>` (keep the deliberate bad id).
|
||||
- `Bouncer.yarn`: `<<check placement routine>>`; `skill_rank("undisclosed") >= 2`;
|
||||
`<<check undisclosed hard -1>>`. Give `undisclosed` `startingRank: 2` in the JSON bible so the
|
||||
gated option is reachable in the demo — this is the one starting-rank exception, and it replaces
|
||||
the identical existing exception for `intimidation`. Comment it in the JSON.
|
||||
- `Commentary.yarn`: rename the three nodes to `Commentary_Entrance_Queue_{facework,long_shift,the_float}`
|
||||
and **rewrite the three lines in each voice's register**. The current lines are placeholders. Use the
|
||||
Bible's `PASSIVE VOICE` samples as the model — e.g. ROOM TONE gives direction before content and
|
||||
transcribes with the gaps marked; THE FLOAT converts everything to a nightly rate and never
|
||||
moralises. Also drop the `"Paranoia: "` / `"Read the Room: "` speaker prefixes if the display name
|
||||
is already surfaced elsewhere; if not, use the exact `displayName` (`THE FLOAT:`, `FACEWORK:`).
|
||||
- `Common.yarn`: no `$check_*` change needed. Add a short comment block recording the two writing
|
||||
rules that the code deliberately does not enforce (Bible §7 / §2.1 above):
|
||||
*Wrong Truth failures carry no failure signal in the prose — the UI banner is the only tell;*
|
||||
*documentary routes are gated on Act and errand state, never on a voice.*
|
||||
|
||||
### 5.10 Update the tests, then add three
|
||||
|
||||
Existing tests need mechanical fixes:
|
||||
|
||||
- `DcBandTests`: `"standard" -> 11` becomes `"routine" -> 11`. Add a case asserting `"trivial" -> 8`
|
||||
(not 5) so the reused name is pinned.
|
||||
- `SkillCheckSystemTests`, `CommentaryPickerTests`: the `EditorSet(...)` helper calls change to the
|
||||
DTO form (§5.2), and the literal skill ids change per §4.1. No assertions about check maths should
|
||||
change — if one does, you have regressed the implementation. Stop and re-read §2.
|
||||
|
||||
New tests worth the keystrokes:
|
||||
|
||||
1. **Firing budget gating** (`CommentarySystem`): a skill with `targetFiringsPerHour = 3600` (one per
|
||||
second) is eligible again immediately; a skill with `targetFiringsPerHour = 1` is not. This needs
|
||||
the budget check to be reachable without a `DialogueRunner` — extract the eligibility predicate
|
||||
into a `public static bool IsOffCooldown(float lastFired, float now, float targetPerHour)` and
|
||||
test that, rather than trying to instantiate the MonoBehaviour.
|
||||
2. **Roster shape**: load `Assets/Skills/SkillDatabase.asset` in an EditMode test and assert 11
|
||||
skills, id regex, pair symmetry (non-specialists), the axis counts, the 71 firing total, and the
|
||||
channel tally. This is the test that catches a JSON typo before it reaches the writer.
|
||||
3. **Id/display divergence**: assert `displayName` is the Bible's uppercase form and `id` is the
|
||||
snake_case form for all 11 — the two are intentionally different and easy to accidentally unify.
|
||||
|
||||
Run them:
|
||||
```bash
|
||||
/Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity \
|
||||
-batchmode -quit -nographics \
|
||||
-projectPath /Users/lennart/Dev/nightlcub-arcadia/NightclubArcadia \
|
||||
-runTests -testPlatform EditMode -testFilter "NightclubArcadia.Skills.Tests" \
|
||||
-testResults /tmp/skill-tests.xml -logFile -
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Explicit non-goals
|
||||
|
||||
Do not build any of these. Each is either already satisfied or is a writing convention:
|
||||
|
||||
- A "quiet failure" / suppressed-failure-signal check mode. The UI banner *is* the tell; the prose
|
||||
is the writer's job (Bible §7).
|
||||
- A forbidden-domains table per skill. Node absence already enforces it (§2.1).
|
||||
- Clue / deduction / evidence objects (`CL-0xx`, `DED-0xx`, criticality, routes). The Bible §7
|
||||
worked example references a clue ledger from a separate handbook. **That system does not exist in
|
||||
this repo and is out of scope.** Do not invent a schema for it.
|
||||
- Act / errand state, or any gating for documentary routes.
|
||||
- Character creation, rank progression, or skill-point spending.
|
||||
- Converting `PlayerSkills` / `SkillCheckSystem` into MonoBehaviours to match the skeleton. They are
|
||||
plain C# classes precisely so they can be unit-tested; the skeleton is a sketch, not a target.
|
||||
- Renaming `skill_rank` to `rank`, or `SkillCheckResult` to `CheckResult`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Traps
|
||||
|
||||
### 7.1 `[DidReloadScripts]` will overwrite hand-edited assets
|
||||
|
||||
`SkillSystemSetup.AutoSetupAfterReload` calls `EnsureSkillAssetsOnly()` once per editor session,
|
||||
which regenerates every definition asset and the database from the roster. **If you hand-edit an
|
||||
`.asset` without updating the roster source (now `skill_bible.json`), your edit will be silently
|
||||
reverted the next time scripts reload.** The JSON is the source of truth; assets are build output.
|
||||
Consider adding that sentence as a comment at the top of every generated asset's owning script.
|
||||
|
||||
### 7.2 Skill ids are Yarn node-title fragments
|
||||
|
||||
`CommentarySystem` builds `Commentary_{contextId}_{skillId}`. Yarn node titles must be valid
|
||||
identifiers. This is the reason for the id regex in §5.5 — an id with a space or a hyphen produces
|
||||
a node title that either fails to compile or silently never matches.
|
||||
|
||||
### 7.3 Lookups are case-insensitive, node names are not
|
||||
|
||||
`SkillDatabase` and `PlayerSkills` use `StringComparer.OrdinalIgnoreCase`; `CommentarySystem`'s
|
||||
`NodeExists` uses `StringComparison.Ordinal`. So `<<check PLACEMENT routine>>` works but
|
||||
`Commentary_Entrance_Queue_PLACEMENT` does not. Keep every authored id lowercase.
|
||||
|
||||
### 7.4 The scene references the database, not the definitions
|
||||
|
||||
`Assets/Scenes/DialogueTest.unity` wires `SkillRuntime.database` → `SkillDatabase.asset`, and
|
||||
`CommentarySystem.database` likewise. Replacing definition assets does not break the scene, provided
|
||||
`SkillDatabase.asset` keeps its own GUID. **Do not delete or recreate `SkillDatabase.asset`** —
|
||||
rewrite its `skills:` list in place.
|
||||
|
||||
### 7.5 `float` is a keyword
|
||||
|
||||
Covered in §4, restated because it will be tempting: the id for THE FLOAT is `the_float`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Definition of done
|
||||
|
||||
- [ ] `skill_bible.json` contains all 11 voices with every field in §4, prose copied from the Bible.
|
||||
- [ ] `Assets/Skills/Definitions/` contains exactly 11 assets, named `<id>.asset`; no orphans.
|
||||
- [ ] `SkillDatabase.asset` keeps its original GUID and lists the 11 in roster order.
|
||||
- [ ] `DcBand` has exactly the five target bands; a non-band integer DC logs a warning.
|
||||
- [ ] `SkillAxis` replaces `SkillDomain`; `SkillDefinition.domain` is now the Bible one-liner.
|
||||
- [ ] `CommentarySystem` enforces a per-skill `3600 / targetFiringsPerHour` budget on top of the
|
||||
existing global floor.
|
||||
- [ ] `<<skill_mod>>` / `<<clear_skill_mods>>` exist and fail soft.
|
||||
- [ ] No Yarn file, test, or script references any of the 12 old skill names.
|
||||
- [ ] EditMode tests pass, including the three new ones in §5.10.
|
||||
- [ ] `grep -ri` for each old id across `NightclubArcadia/Assets` (excluding `Library/`) returns
|
||||
nothing.
|
||||
- [ ] The check maths in `SkillCheckSystem` is byte-for-byte unchanged apart from the DC band
|
||||
constants. If it is not, §2 was violated.
|
||||
Reference in New Issue
Block a user