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
|
||||
Reference in New Issue
Block a user