Skip to content

Commit 0251ef8

Browse files
committed
feat: Custom NPCs Agent Skill
1 parent 8c1bb67 commit 0251ef8

3 files changed

Lines changed: 772 additions & 0 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
name: schedule-one-custom-npcs
3+
description: 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+
---
5+
6+
# Schedule One Custom NPCs
7+
8+
Use this skill to build custom NPCs with `S1API` using the documented two-phase model and the underlying `AvatarFramework` constraints.
9+
10+
## Workflow
11+
12+
Follow this order:
13+
14+
1. Classify the NPC as `physical`, `non-physical`, `customer`, `dealer`, or a mixed interaction/UI contact.
15+
2. Put persistent prefab-time data in `ConfigurePrefab(...)`.
16+
3. Put runtime behavior in `OnCreated()` and add `OnDestroyed()`, `OnLoaded()`, or `OnResponseLoaded(...)` when needed.
17+
4. Prefer S1API builder and wrapper APIs over direct low-level object manipulation.
18+
5. Return minimal, reviewable changes plus explicit manual verification steps.
19+
20+
## Core Model
21+
22+
Keep these responsibilities separate:
23+
24+
- `ConfigurePrefab(...)`: identity, icon, spawn position, relationship defaults, customer defaults, dealer defaults, inventory defaults, schedule, and required `Ensure*` components.
25+
- `OnCreated()`: `base.OnCreated()`, `Appearance.Build()`, `Schedule.Enable()`, `Schedule.InitializeActions()` when needed, dialogue wiring, event subscriptions, text messages, and runtime state.
26+
27+
Do not move persistent customer, dealer, relationship, or schedule defaults into runtime code.
28+
29+
## Decision Rules
30+
31+
### Physical vs non-physical
32+
33+
- `IsPhysical => true`: world entity, avatar, spawn point, schedule, direct interaction.
34+
- `IsPhysical => false`: messaging/contact-focused NPC, usually no spawn point and no schedule.
35+
36+
### Customer vs dealer
37+
38+
- Customer NPCs need `EnsureCustomer()` before `WithCustomerDefaults(...)`.
39+
- Customer schedules usually need `plan.EnsureDealSignal()`.
40+
- Dealer NPCs need `public override bool IsDealer => true;` plus `EnsureDealer()` and `WithDealerDefaults(...)`.
41+
- Dealer schedules need `plan.EnsureDealSignal()` to function correctly, and may use `plan.HandleDeal(...)` when that better fits the role.
42+
43+
## Hard Rules
44+
45+
- 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(...)`.
58+
59+
## Minimal Skeletons
60+
61+
### Physical NPC
62+
63+
```csharp
64+
using S1API.Entities;
65+
using UnityEngine;
66+
67+
public sealed class MyCustomNpc : NPC
68+
{
69+
public override bool IsPhysical => true;
70+
71+
protected override void ConfigurePrefab(NPCPrefabBuilder builder)
72+
{
73+
var spawnPos = new Vector3(0f, 0f, 0f);
74+
75+
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+
}
88+
89+
protected override void OnCreated()
90+
{
91+
base.OnCreated();
92+
Appearance.Build();
93+
Schedule.Enable();
94+
}
95+
}
96+
```
97+
98+
### Non-physical contact NPC
99+
100+
```csharp
101+
using S1API.Entities;
102+
103+
public sealed class ContactNpc : NPC
104+
{
105+
public override bool IsPhysical => false;
106+
107+
protected override void ConfigurePrefab(NPCPrefabBuilder builder)
108+
{
109+
builder.WithIdentity("contact_npc", "Unknown", "Contact")
110+
.WithIcon(null);
111+
}
112+
113+
protected override void OnCreated()
114+
{
115+
base.OnCreated();
116+
ClearConversationCategories();
117+
SendTextMessage("Hello from the contact.");
118+
}
119+
}
120+
```
121+
122+
## Appearance Rules
123+
124+
Read `references/s1api-custom-npc-reference.md` for the detailed appearance and AvatarFramework notes. The most important constraints are:
125+
126+
- `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.
134+
135+
## Reference Files
136+
137+
- 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.
139+
140+
## Output Expectations
141+
142+
When producing code or guidance, include:
143+
144+
- 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.
148+
149+
## Common Pitfalls
150+
151+
- Setting appearance defaults but forgetting `Appearance.Build()`.
152+
- Calling `WithCustomerDefaults(...)` without `EnsureCustomer()`.
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.
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# Implementation Patterns
2+
3+
## Purpose
4+
5+
This file captures reusable S1API custom NPC implementation patterns without assuming any local sample repository exists.
6+
7+
## Pattern 1: Physical Customer NPC
8+
9+
Use when the NPC is visible in the world, directly interactable, and participates in the customer economy.
10+
11+
Recommended structure:
12+
13+
```csharp
14+
builder.WithIdentity(...)
15+
.WithAppearanceDefaults(...)
16+
.WithSpawnPosition(...)
17+
.EnsureCustomer()
18+
.WithCustomerDefaults(...)
19+
.WithRelationshipDefaults(...)
20+
.WithSchedule(plan =>
21+
{
22+
plan.EnsureDealSignal()
23+
.WalkTo(...)
24+
.StayInBuilding(...);
25+
});
26+
```
27+
28+
Typical runtime work:
29+
30+
- `base.OnCreated()`
31+
- `Appearance.Build()`
32+
- `Dialogue.BuildAndRegisterContainer(...)`
33+
- `Dialogue.OnChoiceSelected(...)`
34+
- `Schedule.Enable()`
35+
36+
Extra checks:
37+
38+
- Confirm `EnsureCustomer()` exists before `WithCustomerDefaults(...)`.
39+
- Confirm the schedule includes `EnsureDealSignal()` when the customer should actively deal.
40+
- Keep spending, standards, and relationship requirements internally consistent.
41+
42+
## Pattern 2: Event-wired Customer NPC
43+
44+
Use when customer events drive other behavior such as messages, relationship gains, recommendations, or rewards.
45+
46+
Recommended structure:
47+
48+
```csharp
49+
private Action _customerDealCompletedHandler;
50+
51+
protected override void OnCreated()
52+
{
53+
base.OnCreated();
54+
55+
_customerDealCompletedHandler ??= HandleDealCompleted;
56+
Customer.OnDealCompleted -= _customerDealCompletedHandler;
57+
Customer.OnDealCompleted += _customerDealCompletedHandler;
58+
}
59+
60+
protected override void OnDestroyed()
61+
{
62+
base.OnDestroyed();
63+
64+
if (Customer != null && _customerDealCompletedHandler != null)
65+
{
66+
Customer.OnDealCompleted -= _customerDealCompletedHandler;
67+
}
68+
}
69+
```
70+
71+
Guidelines:
72+
73+
- Cache delegates in private fields.
74+
- Always clean up subscriptions in `OnDestroyed()`.
75+
- Keep persistent customer configuration in `ConfigurePrefab(...)`, not in runtime event code.
76+
77+
## Pattern 3: Dealer NPC
78+
79+
Use when the NPC is recruitable, handles contracts, or participates in the dealer economy.
80+
81+
Minimum structure:
82+
83+
```csharp
84+
public override bool IsDealer => true;
85+
86+
builder.EnsureDealer()
87+
.WithDealerDefaults(dd =>
88+
{
89+
dd.WithSigningFee(1000f)
90+
.WithCut(0.15f)
91+
.WithDealerType(DealerType.PlayerDealer)
92+
.WithHomeName("North Apartments");
93+
});
94+
```
95+
96+
Recommended event wiring:
97+
98+
```csharp
99+
private Action _dealerRecruitedHandler;
100+
101+
private void WireDealerEvents()
102+
{
103+
if (Dealer == null)
104+
return;
105+
106+
_dealerRecruitedHandler ??= HandleDealerRecruited;
107+
Dealer.OnRecruited -= _dealerRecruitedHandler;
108+
Dealer.OnRecruited += _dealerRecruitedHandler;
109+
}
110+
111+
protected override void OnDestroyed()
112+
{
113+
base.OnDestroyed();
114+
115+
if (Dealer != null && _dealerRecruitedHandler != null)
116+
{
117+
Dealer.OnRecruited -= _dealerRecruitedHandler;
118+
}
119+
}
120+
```
121+
122+
Dealer checks:
123+
124+
- `IsDealer => true` is present.
125+
- Dealer defaults are configured in prefab-time code.
126+
- The schedule includes `EnsureDealSignal()`.
127+
- Use `HandleDeal(...)` when you need an explicit dealer-handling slot.
128+
129+
## Pattern 4: Non-physical Contact NPC
130+
131+
Use for text-message contacts, phone contacts, story contacts, and other NPCs that do not need a world entity.
132+
133+
Recommended structure:
134+
135+
```csharp
136+
public sealed class ContactNpc : NPC
137+
{
138+
public override bool IsPhysical => false;
139+
140+
protected override void ConfigurePrefab(NPCPrefabBuilder builder)
141+
{
142+
builder.WithIdentity("contact_npc", "Unknown", "Contact")
143+
.WithIcon(null);
144+
}
145+
146+
protected override void OnCreated()
147+
{
148+
base.OnCreated();
149+
ClearConversationCategories();
150+
SendTextMessage("Hello from the contact.");
151+
}
152+
}
153+
```
154+
155+
Guidelines:
156+
157+
- Usually skip `WithSpawnPosition(...)`.
158+
- Usually skip `WithSchedule(...)`.
159+
- Consider `ClearConversationCategories()` for contacts that should not show the default badge.
160+
- If message choices must survive load, implement `OnResponseLoaded(...)` and rebind callbacks there.
161+
162+
## Pattern 5: NPC as UI entry point
163+
164+
Use when talking to the NPC should open a UI, app, editor, or other interface.
165+
166+
Recommended flow:
167+
168+
1. Register the dialogue container in `OnCreated()`.
169+
2. Do any required state changes in `OnChoiceSelected(...)`.
170+
3. End or exit dialogue safely.
171+
4. Open the UI after dialogue cleanup is in the correct state.
172+
173+
This keeps camera restoration, input focus, and override lifetime under control.
174+
175+
## Pattern 6: Styled narrative NPC
176+
177+
Use when the NPC should feel authored rather than purely functional.
178+
179+
Guidelines:
180+
181+
- Store key route points in `static readonly Vector3` fields.
182+
- Make schedule choices reflect the character's role.
183+
- Write dialogue that matches the role, not generic test text.
184+
- Avoid maintaining two conflicting appearance definitions unless both are intentionally needed.
185+
186+
## Pattern 7: Conditional-presence NPC
187+
188+
Use when the NPC should only exist for certain story states, region states, or progression states.
189+
190+
Safe approach:
191+
192+
- Check the relevant external state early in `OnCreated()`.
193+
- If the NPC should not continue, stop runtime initialization cleanly.
194+
195+
Be careful not to confuse:
196+
197+
- "this NPC should not exist right now"
198+
- with
199+
- "this NPC should exist but not yet be interactable"
200+
201+
If the NPC should simply be gated, prefer relationship locks, dialogue gates, or unlock conditions over destroying it.
202+
203+
## Stable architectural conclusions
204+
205+
1. Put one NPC class per role file under `NPCs/`.
206+
2. Keep role-specific logic inside the NPC class instead of bloating `Core.cs`.
207+
3. The most natural internal split for complex NPCs is: appearance, schedule, dialogue, customer/dealer, and event wiring.
208+
4. For multi-capability NPCs, keep the logic cohesive inside the role rather than scattering it across unrelated services.
209+
210+
## Default implementation order
211+
212+
When the request is just "make a custom NPC," default to this order:
213+
214+
1. Decide whether it is physical or non-physical.
215+
2. Decide whether it is plain, customer, dealer, or UI/story focused.
216+
3. Write `ConfigurePrefab(...)` first.
217+
4. Write `OnCreated()` second.
218+
5. Add cleanup and save/load restoration last.

0 commit comments

Comments
 (0)