Use this skill to build custom NPCs with S1API using the documented two-phase model and the underlying AvatarFramework constraints.
Workflow
Follow this order:
- Classify the NPC as
physical,non-physical,customer,dealer, or a mixed interaction/UI contact. - Put persistent prefab-time data in
ConfigurePrefab(...). - Put runtime behavior in
OnCreated()and addOnDestroyed(),OnLoaded(), orOnResponseLoaded(...)when needed. - Prefer S1API builder and wrapper APIs over direct low-level object manipulation.
- Return minimal, reviewable changes plus explicit manual verification steps.
Core Model
Keep these responsibilities separate:
ConfigurePrefab(...): identity, icon, spawn position, relationship defaults, customer defaults, dealer defaults, inventory defaults, schedule, and action-specificEnsure*calls such asplan.EnsureDealSignal(). Role infrastructure comes automatically fromIsCustomer,IsDealer, andIsSupplier; do not addEnsureCustomer(),EnsureDealer(), orEnsureSupplier().OnCreated():base.OnCreated(),Appearance.Build(),Schedule.Enable(),Schedule.InitializeActions()when needed, dialogue wiring, event subscriptions, text messages, and runtime state.
Do not move persistent customer, dealer, relationship, or schedule defaults into runtime code.
Decision Rules
Physical vs non-physical
IsPhysical => true: world entity, avatar, spawn point, schedule, direct interaction.IsPhysical => false: messaging/contact-focused NPC, usually no spawn point and no schedule.
Customer vs dealer
- Customer NPCs declare
public override bool IsCustomer => true;, then optionally useWithCustomerDefaults(...). - Customer schedules usually need
plan.EnsureDealSignal(). - Dealer NPCs declare
public override bool IsDealer => true;, then optionally useWithDealerDefaults(...). - Dealer schedules need
plan.EnsureDealSignal()to function correctly, and may useplan.HandleDeal(...)when that better fits the role.
Hard Rules
- Prefer the parameterless NPC constructor for new code.
- Never manually instantiate custom NPCs with
new; let S1API own instancing. - Treat
WithIdentity(id, ...)as stable save data. Changing theideffectively creates a different NPC. - Configure customer, dealer, relationship, and schedule defaults only in
ConfigurePrefab(...). - Always call
Appearance.Build()after runtime appearance changes. - For physical NPCs, call
Schedule.Enable()inOnCreated(); addSchedule.InitializeActions()when actions require it. - Unsubscribe from events in
OnDestroyed(). - Reattach saved text-message callbacks in
OnResponseLoaded(...)when messages must keep working after load. - Restore load-sensitive runtime state in
OnLoaded(). - Call
ClearConversationCategories()for contacts that should not show the default Customer/Supplier/Dealer badge. - Prefer schedule wrapper methods like
WalkTo(...),StayInBuilding(...),UseVendingMachine(...),LocationDialogue(...), andHandleDeal(...); useplan.Add(new ...Spec())only for advanced cases. - Before using
LocationBased(...).OnArriveSmokeBreak(),OnArriveGraffiti(),OnArriveDrinking(), orOnArriveHoldItem(), add the corresponding prefab-timeEnsure*component. - Do not call
Dialogue.StopOverride()fromOnNodeDisplayed(...); only call it from safe callback points such asOnChoiceSelected(...).
Minimal Skeletons
Physical NPC
using S1API.Entities;
using UnityEngine;
public sealed class MyCustomNpc : NPC
{
public override bool IsPhysical => true;
protected override void ConfigurePrefab(NPCPrefabBuilder builder)
{
var spawnPos = new Vector3(0f, 0f, 0f);
builder.WithIdentity("my_custom_npc", "Alex", "Example")
.WithSpawnPosition(spawnPos)
.WithRelationshipDefaults(r =>
{
r.WithDelta(1.0f)
.SetUnlocked(true)
.SetUnlockType(NPCRelationship.UnlockType.DirectApproach);
})
.WithSchedule(plan =>
{
plan.WalkTo(spawnPos, 900);
});
}
protected override void OnCreated()
{
base.OnCreated();
Appearance.Build();
Schedule.Enable();
}
}
Non-physical contact NPC
using S1API.Entities;
public sealed class ContactNpc : NPC
{
public override bool IsPhysical => false;
protected override void ConfigurePrefab(NPCPrefabBuilder builder)
{
builder.WithIdentity("contact_npc", "Unknown", "Contact")
.WithIcon(null);
}
protected override void OnCreated()
{
base.OnCreated();
ClearConversationCategories();
SendTextMessage("Hello from the contact.");
}
}
Appearance Rules
Read references/s1api-custom-npc-reference.md for the detailed appearance and AvatarFramework notes. The most important constraints are:
Gender: treat as normalized0.0fto1.0f;Avatar.IsMale()uses< 0.5f.Weight: treat as normalized0.0fto1.0f; AvatarFramework applies it as a blendshape percentage.Height: AvatarFramework does not clamp it, but S1API defaults and random generation center around0.98fto1.0f; use0.8fto1.2fas the safe practical range unless you intentionally want exaggerated scaling.PupilDilation: use0.0fto1.0f.EyeLidRestingStatevalues: each lid value should stay in0.0fto1.0f.EyebrowRestingHeight: runtime code clamps this to-1.1fthrough1.5f.- Practical layer limits: keep face layers to 6, body layers to 6, and accessories to 9.
- Asset paths must match
Resources.Load(...)style paths, such asAvatar/Hair/Spiky/SpikyorAvatar/Layers/Top/T-Shirt, with no file extension.
Reference Files
- Read
references/s1api-custom-npc-reference.mdfor API guidance, runtime lifecycle behavior, schedule rules, and AvatarFramework-backed appearance constraints. - Read
references/example-project-patterns.mdfor reusable implementation patterns without depending on any local sample repository.
Output Expectations
When producing code or guidance, include:
- The NPC type and why it fits.
- What belongs in
ConfigurePrefab(...)versusOnCreated(). - Which files need to change.
- Manual checks for spawn, mugshot/icon, interaction, schedule execution, customer/dealer behavior, save/load restoration, and any message-response callbacks.
Common Pitfalls
- Setting appearance defaults but forgetting
Appearance.Build(). - Making
IsCustomer,IsDealer, orIsSupplierdepend on constructor or initialized field state. - Calling
WithDealerDefaults(...)withoutIsDealer => true. - Omitting
EnsureDealSignal()for customer or dealer schedules that need deals/contracts. - Using advanced location-based actions without the matching
Ensure*call. - Using
plan.Add(new ...Spec())for simple cases where a wrapper method is clearer. - Modifying persistent defaults in
OnCreated()instead ofConfigurePrefab(...). - Forgetting to restore message response callbacks in
OnResponseLoaded(...). - Changing the NPC
idwithout realizing it changes save identity.