Extending the Library
One class per new word: derive the right base, add a [Doc] attribute, and your condition, instruction or resolver appears in every picker, dropdown and generated reference — registered nowhere.
How extension works
The library is discovered by reflection over type inheritance — there is no registry, no manifest, no enum to extend. Anything deriving ConditionBase, InstructionBase (or its ImmediateInstruction shortcut), or one of the five resolver bases (GameObjectResolver, NumberResolver, StringResolver, BoolResolver, PositionResolver) is picked up automatically — including from your own assemblies. A pinned test proves the claim with a resolver in an assembly the toolkit has never heard of.
The [Doc]attribute is the one place your word describes itself: title, category, description, search keywords and an example. The same attribute feeds the in-editor picker, this site's generated stock-library reference, and tooltips double as parameter documentation.
A custom condition
Conditions answer one question, synchronously. Derive ConditionBase, override Check and Summary (the live sentence shown on the authored row):
using System;
using Curitor.PartyCoreKit.Scripting;
using UnityEngine;
[Doc(
Title = "Has Tag",
Category = "Object",
Description = "Passes while the subject carries the given tag.",
Keywords = "tag compare label marked kind")]
[Serializable]
public class HasTagCondition : ConditionBase
{
[Tooltip("The tag to test the subject for.")]
[SerializeField] private string tag = "Player";
public override string Summary => $"Has Tag ({tag})";
public override bool Check(ConditionContext context)
=> context.Subject != null && context.Subject.CompareTag(tag);
}A custom resolver
Resolvers are what make parameters composable: adding one number resolver makes it available to everynumber parameter on every node, stock or custom. Resolvers can themselves take resolved parameters — this one takes an object parameter, so "occupants of whatever triggered this" composes for free:
using System;
using Curitor.PartyCoreKit.Scripting;
using Curitor.PartyCoreKit.Scripting.Properties;
using UnityEngine;
[Doc(
Title = "Occupants Of",
Category = "World",
Description = "How many objects are currently inside a trigger volume.",
Keywords = "occupants inside count zone volume trigger")]
[Serializable]
public class NumberOccupants : NumberResolver
{
[Doc("Which trigger volume to count. Defaults to whatever this event is about.")]
[SerializeField] private PropertyGetGameObject of = new PropertyGetGameObject();
public override string Summary => $"Occupants of {(of != null ? of.Summary : "Subject")}";
public override float Resolve(ConditionContext context)
{
GameObject target = of != null ? of.Get(context) : context.Subject;
if (target == null) return 0f;
TriggerBehaviour trigger = context.GetComponentInParent<TriggerBehaviour>(target);
return trigger != null ? trigger.OccupantCount : 0f;
}
}A custom destination
Destinations are the write side of the same idea, and they extend the same way: derive NumberSetter, add [Doc], and your destination appears in every number destination in the project — starting with the Set Value instruction.
using System;
using Curitor.PartyCoreKit.Scripting;
using Curitor.PartyCoreKit.Scripting.Properties;
using UnityEngine;
[Doc(
Title = "Player Pref",
Category = "Value",
Description = "Stores the number in a PlayerPrefs key on this machine.",
Keywords = "preference setting store local remember machine")]
[Serializable]
public class NumberPlayerPrefSetter : NumberSetter
{
[Tooltip("The PlayerPrefs key to write.")]
public string key = "score";
// Shown on the node's row, with the current values in it -- without this
// the destination reads as a class name.
public override string Summary => $"Player Pref '{key}'";
public override void Set(ConditionContext context, float value)
{
// Local machine state only, so no authority gate is needed here.
PlayerPrefs.SetFloat(key, value);
}
}Instructions and the abort contract
Derive ImmediateInstruction and override protected override bool Run(ConditionContext context) for anything that finishes in the frame it starts — which is almost everything. Note the protected: unlike a condition's Check, this one is not public, and writing public override will not compile. Return falseonly when the instruction's core purpose failed and the rest of the list must not run (the stock Add Item aborts on a full inventory, so a pickup's Destroy Self never fires for nothing). Derive InstructionBase directly only when you must wait across frames — there you override Task<bool> Execute(context, cancellation) instead, and wait exclusively through the provided WaitSeconds / WaitFrames / WaitUntil helpers. They run on scaled game time, so pausing the game pauses the wait, and they honour the cancellation token, which fires when the object is destroyed or its scene unloads.
Documenting your fields
Every serialized field on your node becomes a parameter in the generated reference, and there are two ways to describe one. They are not alternatives — one is the fallback for the other:
// [Tooltip] is enough, and it is what the stock nodes use.
// One sentence, doing double duty: inspector hover AND reference docs.
[Tooltip("How many to give.")]
[SerializeField] private PropertyGetNumber amount = 1;
// [Doc] on a field WINS over [Tooltip] when both are present. Reach for
// it when a reader of the docs needs more than a hover tip should carry.
[Doc("Which trigger volume to count. Defaults to whatever this event is about.")]
[SerializeField] private PropertyGetGameObject of = new PropertyGetGameObject();Fields marked [HideInInspector] are skipped entirely: they are row chrome the editor owns, not parameters an author reads about.
Checklist
[Serializable]on the class.[Doc]on the class with a title, a category, a description, and search keywords — the words for the problem rather than the solution, since that is what makes search find things.- A
[Tooltip]on every serialized field, or a[Doc]where that is not enough. - An overridden
Summary, interpolating the current values. Without it the authored row shows the class name, and a list of class names is not the "reads like prose" effect the inspectors are built for. - If you rename or move a class that is already in someone's scenes, add Unity's
[MovedFrom]— the fully-qualified form, since it has nousingin these files:
[UnityEngine.Scripting.APIUpdating.MovedFrom(
true, "YourOldNamespace", "YourOldAssembly")]
[Serializable]
public class RenamedCondition : ConditionBase { }