|
| 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. |
0 commit comments