Schedule One Custom NPCs
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 add OnDestroyed(), OnLoaded(), or OnResponseLoaded(...) 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-specific Ensure* calls such as plan.EnsureDealSignal(). Role infrastructure comes automatically from IsCustomer, IsDealer, and IsSupplier; do not add EnsureCustomer(), EnsureDealer(), or EnsureSupplier().
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 use WithCustomerDefaults(...).
- Customer schedules usually need
plan.EnsureDealSignal().
- Dealer NPCs declare
public override bool IsDealer => true;, then optionally use WithDealerDefaults(...).
- Dealer schedules need
plan.EnsureDealSignal() to function correctly, and may use plan.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 the id effectively 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() in OnCreated(); add Schedule.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(...), and HandleDeal(...); use plan.Add(new ...Spec()) only for advanced cases.
- Before using
LocationBased(...).OnArriveSmokeBreak(), OnArriveGraffiti(), OnArriveDrinking(), or OnArriveHoldItem(), add the corresponding prefab-time Ensure* component.
- Do not call
Dialogue.StopOverride() from OnNodeDisplayed(...); only call it from safe callback points such as OnChoiceSelected(...).
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 normalized 0.0f to 1.0f; Avatar.IsMale() uses < 0.5f.
Weight: treat as normalized 0.0f to 1.0f; AvatarFramework applies it as a blendshape percentage.
Height: AvatarFramework does not clamp it, but S1API defaults and random generation center around 0.98f to 1.0f; use 0.8f to 1.2f as the safe practical range unless you intentionally want exaggerated scaling.
PupilDilation: use 0.0f to 1.0f.
EyeLidRestingState values: each lid value should stay in 0.0f to 1.0f.
EyebrowRestingHeight: runtime code clamps this to -1.1f through 1.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 as Avatar/Hair/Spiky/Spiky or Avatar/Layers/Top/T-Shirt, with no file extension.
Reference Files
- Read
references/s1api-custom-npc-reference.md for API guidance, runtime lifecycle behavior, schedule rules, and AvatarFramework-backed appearance constraints.
- Read
references/example-project-patterns.md for 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(...) versus OnCreated().
- 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, or IsSupplier depend on constructor or initialized field state.
- Calling
WithDealerDefaults(...) without IsDealer => 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 of ConfigurePrefab(...).
- Forgetting to restore message response callbacks in
OnResponseLoaded(...).
- Changing the NPC
id without realizing it changes save identity.
1---2name: schedule-one-custom-npcs3description: Focused Schedule One S1API custom NPC creation skill. Use when creating, editing, debugging, or reviewing custom NPC classes for Schedule 1 mods, including physical or non-physical NPCs, ConfigurePrefab builder setup, appearance, schedules, dialogue, customer behavior, dealer behavior, runtime lifecycle hooks, save/load callback restoration, and AvatarFramework-backed appearance ranges and asset paths.4---56# Schedule One Custom NPCs78Use this skill to build custom NPCs with `S1API` using the documented two-phase model and the underlying `AvatarFramework` constraints.910## Workflow1112Follow this order:13141. Classify the NPC as `physical`, `non-physical`, `customer`, `dealer`, or a mixed interaction/UI contact.152. Put persistent prefab-time data in `ConfigurePrefab(...)`.163. Put runtime behavior in `OnCreated()` and add `OnDestroyed()`, `OnLoaded()`, or `OnResponseLoaded(...)` when needed.174. Prefer S1API builder and wrapper APIs over direct low-level object manipulation.185. Return minimal, reviewable changes plus explicit manual verification steps.1920## Core Model2122Keep these responsibilities separate:2324- `ConfigurePrefab(...)`: identity, icon, spawn position, relationship defaults, customer defaults, dealer defaults, inventory defaults, schedule, and action-specific `Ensure*` calls such as `plan.EnsureDealSignal()`. Role infrastructure comes automatically from `IsCustomer`, `IsDealer`, and `IsSupplier`; do not add `EnsureCustomer()`, `EnsureDealer()`, or `EnsureSupplier()`.25- `OnCreated()`: `base.OnCreated()`, `Appearance.Build()`, `Schedule.Enable()`, `Schedule.InitializeActions()` when needed, dialogue wiring, event subscriptions, text messages, and runtime state.2627Do not move persistent customer, dealer, relationship, or schedule defaults into runtime code.2829## Decision Rules3031### Physical vs non-physical3233- `IsPhysical => true`: world entity, avatar, spawn point, schedule, direct interaction.34- `IsPhysical => false`: messaging/contact-focused NPC, usually no spawn point and no schedule.3536### Customer vs dealer3738- Customer NPCs declare `public override bool IsCustomer => true;`, then optionally use `WithCustomerDefaults(...)`.39- Customer schedules usually need `plan.EnsureDealSignal()`.40- Dealer NPCs declare `public override bool IsDealer => true;`, then optionally use `WithDealerDefaults(...)`.41- Dealer schedules need `plan.EnsureDealSignal()` to function correctly, and may use `plan.HandleDeal(...)` when that better fits the role.4243## Hard Rules4445- Prefer the parameterless NPC constructor for new code.46- Never manually instantiate custom NPCs with `new`; let S1API own instancing.47- Treat `WithIdentity(id, ...)` as stable save data. Changing the `id` effectively creates a different NPC.48- Configure customer, dealer, relationship, and schedule defaults only in `ConfigurePrefab(...)`.49- Always call `Appearance.Build()` after runtime appearance changes.50- For physical NPCs, call `Schedule.Enable()` in `OnCreated()`; add `Schedule.InitializeActions()` when actions require it.51- Unsubscribe from events in `OnDestroyed()`.52- Reattach saved text-message callbacks in `OnResponseLoaded(...)` when messages must keep working after load.53- Restore load-sensitive runtime state in `OnLoaded()`.54- Call `ClearConversationCategories()` for contacts that should not show the default Customer/Supplier/Dealer badge.55- Prefer schedule wrapper methods like `WalkTo(...)`, `StayInBuilding(...)`, `UseVendingMachine(...)`, `LocationDialogue(...)`, and `HandleDeal(...)`; use `plan.Add(new ...Spec())` only for advanced cases.56- Before using `LocationBased(...).OnArriveSmokeBreak()`, `OnArriveGraffiti()`, `OnArriveDrinking()`, or `OnArriveHoldItem()`, add the corresponding prefab-time `Ensure*` component.57- Do not call `Dialogue.StopOverride()` from `OnNodeDisplayed(...)`; only call it from safe callback points such as `OnChoiceSelected(...)`.5859## Minimal Skeletons6061### Physical NPC6263```csharp64using S1API.Entities;65using UnityEngine;6667public sealed class MyCustomNpc : NPC68{69 public override bool IsPhysical => true;7071 protected override void ConfigurePrefab(NPCPrefabBuilder builder)72 {73 var spawnPos = new Vector3(0f, 0f, 0f);7475 builder.WithIdentity("my_custom_npc", "Alex", "Example")76 .WithSpawnPosition(spawnPos)77 .WithRelationshipDefaults(r =>78 {79 r.WithDelta(1.0f)80 .SetUnlocked(true)81 .SetUnlockType(NPCRelationship.UnlockType.DirectApproach);82 })83 .WithSchedule(plan =>84 {85 plan.WalkTo(spawnPos, 900);86 });87 }8889 protected override void OnCreated()90 {91 base.OnCreated();92 Appearance.Build();93 Schedule.Enable();94 }95}96```9798### Non-physical contact NPC99100```csharp101using S1API.Entities;102103public sealed class ContactNpc : NPC104{105 public override bool IsPhysical => false;106107 protected override void ConfigurePrefab(NPCPrefabBuilder builder)108 {109 builder.WithIdentity("contact_npc", "Unknown", "Contact")110 .WithIcon(null);111 }112113 protected override void OnCreated()114 {115 base.OnCreated();116 ClearConversationCategories();117 SendTextMessage("Hello from the contact.");118 }119}120```121122## Appearance Rules123124Read `references/s1api-custom-npc-reference.md` for the detailed appearance and AvatarFramework notes. The most important constraints are:125126- `Gender`: treat as normalized `0.0f` to `1.0f`; `Avatar.IsMale()` uses `< 0.5f`.127- `Weight`: treat as normalized `0.0f` to `1.0f`; AvatarFramework applies it as a blendshape percentage.128- `Height`: AvatarFramework does not clamp it, but S1API defaults and random generation center around `0.98f` to `1.0f`; use `0.8f` to `1.2f` as the safe practical range unless you intentionally want exaggerated scaling.129- `PupilDilation`: use `0.0f` to `1.0f`.130- `EyeLidRestingState` values: each lid value should stay in `0.0f` to `1.0f`.131- `EyebrowRestingHeight`: runtime code clamps this to `-1.1f` through `1.5f`.132- Practical layer limits: keep face layers to 6, body layers to 6, and accessories to 9.133- Asset paths must match `Resources.Load(...)` style paths, such as `Avatar/Hair/Spiky/Spiky` or `Avatar/Layers/Top/T-Shirt`, with no file extension.134135## Reference Files136137- Read `references/s1api-custom-npc-reference.md` for API guidance, runtime lifecycle behavior, schedule rules, and AvatarFramework-backed appearance constraints.138- Read `references/example-project-patterns.md` for reusable implementation patterns without depending on any local sample repository.139140## Output Expectations141142When producing code or guidance, include:143144- The NPC type and why it fits.145- What belongs in `ConfigurePrefab(...)` versus `OnCreated()`.146- Which files need to change.147- Manual checks for spawn, mugshot/icon, interaction, schedule execution, customer/dealer behavior, save/load restoration, and any message-response callbacks.148149## Common Pitfalls150151- Setting appearance defaults but forgetting `Appearance.Build()`.152- Making `IsCustomer`, `IsDealer`, or `IsSupplier` depend on constructor or initialized field state.153- Calling `WithDealerDefaults(...)` without `IsDealer => true`.154- Omitting `EnsureDealSignal()` for customer or dealer schedules that need deals/contracts.155- Using advanced location-based actions without the matching `Ensure*` call.156- Using `plan.Add(new ...Spec())` for simple cases where a wrapper method is clearer.157- Modifying persistent defaults in `OnCreated()` instead of `ConfigurePrefab(...)`.158- Forgetting to restore message response callbacks in `OnResponseLoaded(...)`.159- Changing the NPC `id` without realizing it changes save identity.