Npc template
"Plug and play" NPC templates
Hello! Aviroen here.
Below I have constructed a ROMANCEABLE NPC template which are all standalone .json(s)
This is assumed that you understand Content Patcher to some degree.
"What does standalone mean?"
It means it can be it's own separate json like "NPCNAME.json" or "Dialogue.json" or "MarriageDialogue.json", and must be included in your content.json:
See the "EdelIncludes" section above as to how all of these codeblocks work.
"What is all of this
{{ModId}}_
business?"
Internalizing your NPC's name with YOURMOD.UNIQUEID helps keep clarity just incase you have the same name as any other NPC. It makes it so you don't run into complications with dialogues/schedules/etc.
You DO NOT need to remove that part of the code, Content Patcher will automatically replace it with your manifest's UNIQUE ID.
You just HAVE TO REMEMBER that anytime you're referencing your NPC, you need to append it with the {{ModId}}_NPCNAME.
While it is not 100% necessary, if you don't have a very unique name, it's good for compatibility.
Sidenote from Ichor (author of Lacey and Nightshade):
"i find it very helpful in CP to set up a dynamic token with your NPC's internal name, so that writing events (in particular) is less painful. e.g. Lacey is {{ModId}}_Lacey
which expands to ichortower.HatMouseLacey_Lacey
, so i set up a token that's just {{Lacey}}
for events"
This is a MODULAR EXAMPLE for the content.json
Content.json
{
"Format": "2.3.0", //version of content patcher
"ConfigSchema": { //whether you have dynamic tokens that can be configured by the player used in generic mod config menu
//EXAMPLE GMCM CONFIG
"Unwipeable": { //name of token
"AllowValues": "true, false", //allowvalues can be any subset, any amount
"Default": "true", //sets basic default to sit on
"Section": "Content", //section is optional, but it does help organization
"Description": "Default true to have Edelweiss' memory be unwipeable from the witch statue."
},
}
"DynamicTokens": [
//For the internal NPC's name
{
"Name": "<NPCNAME>",
"Value": "{{ModId}}_<NPCNAME>"
},
{
"Name": "Weekday",
"When": { "DayOfWeek": "Monday" },
"Value": "Mon"
},
{
"Name": "Weekday",
"When": { "DayOfWeek": "Tuesday" },
"Value": "Tue"
},
{
"Name": "Weekday",
"When": { "DayOfWeek": "Wednesday" },
"Value": "Wed"
},
{
"Name": "Weekday",
"When": { "DayOfWeek": "Thursday" },
"Value": "Thu"
},
{
"Name": "Weekday",
"When": { "DayOfWeek": "Friday" },
"Value": "Fri"
},
{
"Name": "Weekday",
"When": { "DayOfWeek": "Saturday" },
"Value": "Sat"
},
{
"Name": "Weekday",
"When": { "DayOfWeek": "Sunday" },
"Value": "Sun"
},
]
"Changes": [
//////////////////////////////////////////////loads
{ //blank loads so that i can target edit
"LogName": "BlankLoads",
"Action": "Load",
"Priority": "Low",
"Target": "Characters/Dialogue/{{<NPCNAME>}}, Characters/Dialogue/MarriageDialogue{{<NPCNAME>}}, Characters/schedules/{{<NPCNAME>}}, Strings/Schedules/{{<NPCNAME>}}",
"FromFile": "data/Blank.json"
},
//////////////////////////////////////////////includes
{
"Logname": "EdelIncludes",
"Action": "Include",
"FromFile": "data/<Data/Character>.json"
}
]
}
Data/Characters, Formerly known as "NPCDispositions" pre 1.6
{
"Changes": [
{
"LogName": "Data/Character edit",
"Action": "EditData",
"Target": "Data/Characters",
"Entries": {
"{{ModId}}_<NPCNAME>": { //unique id of the npc, must be used when referencing the npc at all
"DisplayName": "{{i18n:{{ModId}}_<NPCNAME>}}", //translation capability
"BirthSeason": "Fall",
"BirthDay": 3,
"HomeRegion": "Other", //town / desert / other
"Language": "Default", //default or dwarvish
"Gender": "Undefined", //male, female, undefined
"Age": "Adult", //child, teen, adult changes reactions from stringsfromcsfiles
"Manner": "Rude", //polite, neutral, rude changes stringsfromcsfiles
"SocialAnxiety": "Shy", //outgoing, neutral, shy changes stringsfromcsfiles
"Optimism": "Negative", //positive, neutral, negative changes stringsfromcsfiles
"IsDarkSkinned": false, //determines for children
"CanBeRomanced": true, //checks to see if a bouquet can be gifted to this npc
"LoveInterest": null, //sort of flavor text
"Calendar": "HiddenUntilMet", //AlwaysShown, HiddenUntilMet, HiddenAlways for valid conditions of when their lil mugshot shows up on the calendar
"SocialTab": "HiddenUntilMet", //UnknownUntilMet, AlwaysShown, HiddenUntilMet, HiddenAlways for valid conditions of when their information on the social tab will show up
"CanSocialize": null, //boolean, whether this npc can even have a dialogue box with the player [true/false]
"CanReceiveGifts": true, //whether this npc is antisocial or not
"CanGreetNearbyCharacters": true, //reacts to townspeople when they pass by
"CanCommentOnPurchasedShopItems": null, //needs i18n or dialogue strings to react to farmer's purchase if true
"CanVisitIsland": null, //game state query-able if npc can visit island randomly
"IntroductionsQuest": false, //sets if this npc is included into the intro quest of introducing yourself to everyone
"ItemDeliveryQuests": null, //sets if this npc gets put on the bulletin board for randomly generated quests
"PerfectionScore": false, //checks if this npc is required for perfection for friendship
"EndSlideShow": "TrailingGroup", //shows this npc following behind in perfection cutscene
"SpouseAdopts": null, //game state query-able whether this npc will always adopt or not https://stardewvalleywiki.com/Modding:Game_state_queries
"SpouseWantsChildren": true, //game state query-able about the npc will want children at all
"SpouseGiftJealousy": null, //specific gendered jealousy male-male female-female about this npc whether you gift nonbirthday/quest/festival
"SpouseGiftJealousyFriendshipChange": -30, //friendship loss or gain for jealousy gifting of above
"SpouseRoom": { //the square in the farmhouse that gets dedicated to this npc
"MapAsset": null, //null for vanilla, set to loaded filename for custom npc
"MapSourceRect": {
"X": 0, //this is if you're wanting to move it to a different spot in the farmhouse
"Y": 0,
"Width": 6, //the custom spouse tmx width
"Height": 9 //custom spouse room tmx height
}
},
"SpousePatio": { //this is the spot outdoors on the farm where the spouse sits if they don't have a schedule for the day
"MapAsset": null, //null for vanilla, set to loaded filename for custom npc
"MapSourceRect": {
"X": 0, //this is for if you want it to be someplace different on the farm
"Y": 0,
"Width": 4, //this is the width of the plot on the farm
"Height": 4 //this is the height of the plot on the farm
},
"SpriteAnimationFrames": [ //this is the animation that loops when your custom npc is standing on their spousepatio, [frame, miliseconds] REMEMBER! these loop!
[ 16, 1500 ]
],
"SpriteAnimationPixelOffset": { //this is if your npc is slightly odd shaped, like taller than normal, or shorter than normal
"X": 0,
"Y": 0
}
},
"SpouseFloors": [], //this is the subset of floors that the npc will plaster in the farmhouse
"SpouseWallpapers": [], //this is the subset of wallpapers that the npc will plaster in the farmhouse
"DumpsterDiveFriendshipEffect": -5, //whether the npc loses friendship with you if they catch you digging in the trash
"DumpsterDiveEmote": null, //the emote above their head when they catch you digging in the trash
"FriendsAndFamily": { //whether any other npcs are related
"<NPCNAME>": "{{i18n:<RELATIONSHIP>}}", //i18n is the default.json used for translations
},
"FlowerDanceCanDance": null, //sets if the npc will dance at the flower festival on spring 24
"WinterStarGifts": [], //items that this npc can gift during the winter star festival https://stardewvalleywiki.com/Modding:Item_queries#Item_spawn_fields
"WinterStarParticipant": null, //whether the npc even participates in winter star, can be game state query-able
"UnlockConditions": null, //game state query-able about whether the npc has an unlock condidion before seen
"SpawnIfMissing": true, //forcibly spawns the npc if they're stuck
"Home": [ //sets the npc's spawn point
{
"Id": "Default", //the default of where they spawn
"Condition": null, //game state query-able condition if they change homes
"Location": null, //specific location from data/location
"Tile": {
"X": 0, //x position where the npc idles
"Y": 0 //y position where the npc idles
},
"Direction": "right" //direction that they face while idling
}
],
"TextureName": null,
"Appearance": null, //new appearance system, highly customizable, see: Outfits.json
"MugShotSourceRect": null, //change the position of their headshot in the calendar/map/etc if they're shorter/taller than normal
"Size": {
"X": 16, //this is for how wide each character sprite is
"Y": 32 //this is for how tall each character sprite is
},
"Breather": true, //whether this places the breathing animation over the npc
"BreathChestRect": null, //changeable if you want to enlarge/shrink the breathing animation
"BreathChestPosition": null, //changeable if the npc is taller/shorter than normal
"Shadow": null, //changeable to visible, offset, or scale / visible is default true whether null or not, offset to offset the shadow, scale to enlarge/shrink
"EmoteOffset": { //whether to offset the emote bubble over the npc or not
"X": 0,
"Y": 0
},
"ShakePortraits": [], //the index of the portraits/npc to shake during dialogue, will always trigger
"KissSpriteIndex": null, //whether this npc has a kissing or hugging sprite, reminder that characters/npc and portraits/npc starts at 0
"KissSpriteFacingRight": false, //whether the sprite is facing to the right
"HiddenProfileEmoteSound": null, /* https://docs.google.com/spreadsheets/d/1CpDrw23peQiq-C7F2FjYOMePaYe0Rc9BwQsj3h6sjyo/edit?gid=239695361#gid=239695361 for soundbank / hiddenprofile when clicking on their social tab after 4 hearts shows a different animation if changed */
"HiddenProfileEmoteDuration": 4000, //how long the animation will run in miliseconds
"HiddenProfileEmoteStartFrame": 32, //whichever sprite index the frame is on
"HiddenProfileEmoteFrameCount": 2, //how many frames the sprite index will go through, going left to right
"HiddenProfileEmoteFrameDuration": 200.0, //how long each frame will last measured in miliseconds
"FormerCharacterNames": [], //used for older saves if the npc has changed names and must be globally unique
"CustomFields": null //custom fields is used for c# implementation
}
}
}
]
}
Continuing on, if you were following Tutorial: Making a Custom NPC
Dialogue.json
NOTE: NOT ALL OF THESE ARE NECESSARY.
https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.6#Dialogue_changes (Current as of August 26, 2024)
{
"Changes": [
{
"LogName": "Characters/Dialogue/NPCNAME Edit",
"Action": "EditData",
"Target": "Characters/Dialogue/{{ModId}}_<NPCNAME>",
"Entries": {
//green rain
"GreenRain": "{{i18n or straight dialogue}}",
"GreenRainFinished": "{{i18n or straight dialogue}}",
//movie
"MovieInvitation": "{{i18n or straight dialogue}}",
"RejectMovieTicket_AlreadyInvitedBySomeoneElse": "{{i18n or straight dialogue}}",
"RejectMovieTicket_AlreadyWatchedThisWeek": "{{i18n or straight dialogue}}",
"RejectMovieTicket_Divorced": "{{i18n or straight dialogue}}",
"RejectMovieTicket_DontWantToSeeThatMovie": "{{i18n or straight dialogue}}",
"RejectMovieTicket": "{{i18n or straight dialogue}}",
//dating
"breakUp": "{{i18n or straight dialogue}}",
"dating_EdelweissSeung_memory_oneyear": "{{i18n or straight dialogue}}",
"RejectBouquet_NotDatable": "{{i18n or straight dialogue}}",
"RejectBouquet_NpcAlreadyMarried": "{{i18n or straight dialogue}}",
"RejectBouquet_VeryLowHearts": "{{i18n or straight dialogue}}",
"RejectBouquet_LowHearts": "{{i18n or straight dialogue}}",
"RejectBouquet": "{{i18n or straight dialogue}}",
//marriage
"SpouseFarmhouseClutter": "{{i18n or straight dialogue}}",
"Spouse_MonstersInHouse": "{{i18n or straight dialogue}}",
"SpouseStardrop": "{{i18n or straight dialogue}}",
"RejectMermaidPendant_NeedHouseUpgrade": "{{i18n or straight dialogue}}",
"RejectMermaidPendant_NotDatable": "{{i18n or straight dialogue}}",
"RejectMermaidPendant_NpcWithSomeoneElse": "{{i18n or straight dialogue}}",
"RejectMermaidPendant_PlayerWithSomeoneElse": "{{i18n or straight dialogue}}",
"RejectMermaidPendant_Under8Hearts": "{{i18n or straight dialogue}}",
"RejectMermaidPendant_Under10Hearts": "{{i18n or straight dialogue}}",
"RejectMermaidPendant_Under10Hearts_AskedAgain": "{{i18n or straight dialogue}}",
"RejectMermaidPendant": "{{i18n or straight dialogue}}",
//divorced
"divorced": "{{i18n or straight dialogue}}",
"RejectGift_Divorced": "{{i18n or straight dialogue}}",
"RejectMermaidPendant_Divorced": "{{i18n or straight dialogue}}",
"RejectBouquet_Divorced": "{{i18n or straight dialogue}}",
"WipedMemory": "{{i18n or straight dialogue}}",
//island
"Resort": "{{i18n or straight dialogue}}",
"Resort_Bar": "{{i18n or straight dialogue}}",
"Resort_Chair": "{{i18n or straight dialogue}}",
"Resort_Dance": "{{i18n or straight dialogue}}",
"Resort_Entering": "{{i18n or straight dialogue}}",
"Resort_Leaving": "{{i18n or straight dialogue}}",
"Resort_Shore": "{{i18n or straight dialogue}}",
"Resort_Towel": "{{i18n or straight dialogue}}",
"Resort_Umbrella": "{{i18n or straight dialogue}}",
"Resort_Wander": "{{i18n or straight dialogue}}",
//birthday
"AcceptBirthdayGift_Negative": "{{i18n or straight dialogue}}",
"AcceptBirthdayGift_Positive": "{{i18n or straight dialogue}}",
"AcceptBirthdayGift": "{{i18n or straight dialogue}}",
//dating proposal
"AcceptBouquet": "{{i18n or straight dialogue}}",
//trashcan
"DumpsterDiveComment": "{{i18n or straight dialogue}}",
//slingshot
"HitBySlingshot": "{{i18n or straight dialogue}}",
////////////////////////////////////////////////seasonals
//spring
"spring_Mon": "{{i18n or straight dialogue}}",
"spring_Tue": "{{i18n or straight dialogue}}",
"spring_Wed": "{{i18n or straight dialogue}}",
"spring_Thu": "{{i18n or straight dialogue}}",
"spring_Fri": "{{i18n or straight dialogue}}",
"spring_Sat": "{{i18n or straight dialogue}}",
"spring_Sun": "{{i18n or straight dialogue}}",
//flower festival
"FlowerDance_Accept_Spouse": "{{i18n or straight dialogue}}",
"FlowerDance_Accept": "{{i18n or straight dialogue}}",
"FlowerDance_Decline": "{{i18n or straight dialogue}}",
//summer
"summer_Mon": "{{i18n or straight dialogue}}",
"summer_Tue": "{{i18n or straight dialogue}}",
"summer_Wed": "{{i18n or straight dialogue}}",
"summer_Thu": "{{i18n or straight dialogue}}",
"summer_Fri": "{{i18n or straight dialogue}}",
"summer_Sat": "{{i18n or straight dialogue}}",
"summer_Sun": "{{i18n or straight dialogue}}",
//fall
"fall_Mon": "{{i18n or straight dialogue}}",
"fall_Tue": "{{i18n or straight dialogue}}",
"fall_Wed": "{{i18n or straight dialogue}}",
"fall_Thu": "{{i18n or straight dialogue}}",
"fall_Fri": "{{i18n or straight dialogue}}",
"fall_Sat": "{{i18n or straight dialogue}}",
"fall_Sun": "{{i18n or straight dialogue}}",
//fair
"Fair_Judging": "{{i18n or straight dialogue}}",
"Fair_Judged_PlayerLost_PurpleShorts": "{{i18n or straight dialogue}}",
"Fair_Judged_PlayerLost_Skipped": "{{i18n or straight dialogue}}",
"Fair_Judged_PlayerLost": "{{i18n or straight dialogue}}",
"Fair_Judged_PlayerWon": "{{i18n or straight dialogue}}",
"Fair_Judged": "{{i18n or straight dialogue}}",
//winter
"winter_Mon": "{{i18n or straight dialogue}}",
"winter_Tue": "{{i18n or straight dialogue}}",
"winter_Wed": "{{i18n or straight dialogue}}",
"winter_Thu": "{{i18n or straight dialogue}}",
"winter_Fri": "{{i18n or straight dialogue}}",
"winter_Sat": "{{i18n or straight dialogue}}",
"winter_Sun": "{{i18n or straight dialogue}}",
//winter star
"WinterStar_GiveGift_Before_Spouse": "{{i18n or straight dialogue}}",
"WinterStar_GiveGift_Before": "{{i18n or straight dialogue}}",
"WinterStar_GiveGift_After_Spouse": "{{i18n or straight dialogue}}",
"WinterStar_GiveGift_After": "{{i18n or straight dialogue}}",
"WinterStar_ReceiveGift": "{{i18n or straight dialogue}}",
//////////////////////////////////////////////////accept gifts example
"AcceptGift_(O)96": "{{i18n or straight dialogue}}",
}
}
{
"LogName": "Summit Dialogue",
"Action": "EditData",
"Target": "Data/ExtraDialogue",
"Entries": {
"SummitEvent_Dialogue3_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
}
}
]
}
"I don't want all of that fancy stuff, I want just seasonal and weekday dialogue!"
Well I got news for you: I now give you, a randomized dialogue that generates 210 dialogue keys at once.
This one REQUIRES THE DYNAMIC TOKEN: Weekday.
{
"Changes": [
{
"Action": "EditData",
"Target": "Characters/Dialogue/<NPCNAME>",
"Entries": {
"{{Weekday}}": "{{i18n:{{Weekday}}_{{Random:{{Range:0,5}}}}}}",
"{{Season}}_{{Weekday}}": "{{i18n:{{Season}}_{{Weekday}}_{{Random:{{Range:0,5}}}}}}"
}
}
]
}
Fancy default.json
Anytime you've seen me use i18n, that means I've made a folder at the same level as my assets, named "i18n", with a default.json inside. This is for keeping all of your dialogue in one spot, as well as translation compatibility.
Below is what the default.json keys will end up looking like.
{
"Mon_0": "",
"Mon_1": "",
"Mon_2": "",
"Mon_3": "",
"Mon_4": "",
"Mon_5": "",
"Tue_0": "",
"Tue_1": "",
"Tue_2": "",
"Tue_3": "",
"Tue_4": "",
"Tue_5": "",
"Wed_0": "",
"Wed_1": "",
"Wed_2": "",
"Wed_3": "",
"Wed_4": "",
"Wed_5": "",
"Thu_0": "",
"Thu_1": "",
"Thu_2": "",
"Thu_3": "",
"Thu_4": "",
"Thu_5": "",
"Fri_0": "",
"Fri_1": "",
"Fri_2": "",
"Fri_3": "",
"Fri_4": "",
"Fri_5": "",
"Sat_0": "",
"Sat_1": "",
"Sat_2": "",
"Sat_3": "",
"Sat_4": "",
"Sat_5": "",
"Sun_0": "",
"Sun_1": "",
"Sun_2": "",
"Sun_3": "",
"Sun_4": "",
"Sun_5": "",
"Spring_Mon_0": "",
"Spring_Mon_1": "",
"Spring_Mon_2": "",
"Spring_Mon_3": "",
"Spring_Mon_4": "",
"Spring_Mon_5": "",
"Spring_Tue_0": "",
"Spring_Tue_1": "",
"Spring_Tue_2": "",
"Spring_Tue_3": "",
"Spring_Tue_4": "",
"Spring_Tue_5": "",
"Spring_Wed_0": "",
"Spring_Wed_1": "",
"Spring_Wed_2": "",
"Spring_Wed_3": "",
"Spring_Wed_4": "",
"Spring_Wed_5": "",
"Spring_Thu_0": "",
"Spring_Thu_1": "",
"Spring_Thu_2": "",
"Spring_Thu_3": "",
"Spring_Thu_4": "",
"Spring_Thu_5": "",
"Spring_Fri_0": "",
"Spring_Fri_1": "",
"Spring_Fri_2": "",
"Spring_Fri_3": "",
"Spring_Fri_4": "",
"Spring_Fri_5": "",
"Spring_Sat_0": "",
"Spring_Sat_1": "",
"Spring_Sat_2": "",
"Spring_Sat_3": "",
"Spring_Sat_4": "",
"Spring_Sat_5": "",
"Spring_Sun_0": "",
"Spring_Sun_1": "",
"Spring_Sun_2": "",
"Spring_Sun_3": "",
"Spring_Sun_4": "",
"Spring_Sun_5": "",
"Summer_Mon_0": "",
"Summer_Mon_1": "",
"Summer_Mon_2": "",
"Summer_Mon_3": "",
"Summer_Mon_4": "",
"Summer_Mon_5": "",
"Summer_Tue_0": "",
"Summer_Tue_1": "",
"Summer_Tue_2": "",
"Summer_Tue_3": "",
"Summer_Tue_4": "",
"Summer_Tue_5": "",
"Summer_Wed_0": "",
"Summer_Wed_1": "",
"Summer_Wed_2": "",
"Summer_Wed_3": "",
"Summer_Wed_4": "",
"Summer_Wed_5": "",
"Summer_Thu_0": "",
"Summer_Thu_1": "",
"Summer_Thu_2": "",
"Summer_Thu_3": "",
"Summer_Thu_4": "",
"Summer_Thu_5": "",
"Summer_Fri_0": "",
"Summer_Fri_1": "",
"Summer_Fri_2": "",
"Summer_Fri_3": "",
"Summer_Fri_4": "",
"Summer_Fri_5": "",
"Summer_Sat_0": "",
"Summer_Sat_1": "",
"Summer_Sat_2": "",
"Summer_Sat_3": "",
"Summer_Sat_4": "",
"Summer_Sat_5": "",
"Summer_Sun_0": "",
"Summer_Sun_1": "",
"Summer_Sun_2": "",
"Summer_Sun_3": "",
"Summer_Sun_4": "",
"Summer_Sun_5": "",
"Fall_Mon_0": "",
"Fall_Mon_1": "",
"Fall_Mon_2": "",
"Fall_Mon_3": "",
"Fall_Mon_4": "",
"Fall_Mon_5": "",
"Fall_Tue_0": "",
"Fall_Tue_1": "",
"Fall_Tue_2": "",
"Fall_Tue_3": "",
"Fall_Tue_4": "",
"Fall_Tue_5": "",
"Fall_Wed_0": "",
"Fall_Wed_1": "",
"Fall_Wed_2": "",
"Fall_Wed_3": "",
"Fall_Wed_4": "",
"Fall_Wed_5": "",
"Fall_Thu_0": "",
"Fall_Thu_1": "",
"Fall_Thu_2": "",
"Fall_Thu_3": "",
"Fall_Thu_4": "",
"Fall_Thu_5": "",
"Fall_Fri_0": "",
"Fall_Fri_1": "",
"Fall_Fri_2": "",
"Fall_Fri_3": "",
"Fall_Fri_4": "",
"Fall_Fri_5": "",
"Fall_Sat_0": "",
"Fall_Sat_1": "",
"Fall_Sat_2": "",
"Fall_Sat_3": "",
"Fall_Sat_4": "",
"Fall_Sat_5": "",
"Fall_Sun_0": "",
"Fall_Sun_1": "",
"Fall_Sun_2": "",
"Fall_Sun_3": "",
"Fall_Sun_4": "",
"Fall_Sun_5": "",
"Winter_Mon_0": "",
"Winter_Mon_1": "",
"Winter_Mon_2": "",
"Winter_Mon_3": "",
"Winter_Mon_4": "",
"Winter_Mon_5": "",
"Winter_Tue_0": "",
"Winter_Tue_1": "",
"Winter_Tue_2": "",
"Winter_Tue_3": "",
"Winter_Tue_4": "",
"Winter_Tue_5": "",
"Winter_Wed_0": "",
"Winter_Wed_1": "",
"Winter_Wed_2": "",
"Winter_Wed_3": "",
"Winter_Wed_4": "",
"Winter_Wed_5": "",
"Winter_Thu_0": "",
"Winter_Thu_1": "",
"Winter_Thu_2": "",
"Winter_Thu_3": "",
"Winter_Thu_4": "",
"Winter_Thu_5": "",
"Winter_Fri_0": "",
"Winter_Fri_1": "",
"Winter_Fri_2": "",
"Winter_Fri_3": "",
"Winter_Fri_4": "",
"Winter_Fri_5": "",
"Winter_Sat_0": "",
"Winter_Sat_1": "",
"Winter_Sat_2": "",
"Winter_Sat_3": "",
"Winter_Sat_4": "",
"Winter_Sat_5": "",
"Winter_Sun_0": "",
"Winter_Sun_1": "",
"Winter_Sun_2": "",
"Winter_Sun_3": "",
"Winter_Sun_4": "",
"Winter_Sun_5": ""
}
We trudge along!
MarriageDialogue.json
{
"Changes": [
{
"LogName": "Engaged Dialogue",
"Action": "EditData",
"Target": "data/EngagementDialogue",
"Entries": {
"{{ModId}}_<NPCNAME>0": "{{i18n or straight dialogue}}",
"{{ModId}}_<NPCNAME>1": "{{i18n or straight dialogue}}"
}
},
{
"LogName": "Marriage Dialogue - Base",
"Action": "EditData",
"Target": "Characters/Dialogue/MarriageDialogue{{ModId}}_<NPCNAME>",
"Entries": {
"Spring_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"Summer_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"Fall_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"Winter_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"patio_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"Rainy_Day_0": "{{i18n or straight dialogue}}",
"Rainy_Day_1": "{{i18n or straight dialogue}}",
"Rainy_Day_2": "{{i18n or straight dialogue}}",
"Rainy_Day_3": "{{i18n or straight dialogue}}",
"Rainy_Day_4": "{{i18n or straight dialogue}}",
"Rainy_Day_5": "{{i18n or straight dialogue}}",
"Indoor_Day_0": "{{i18n or straight dialogue}}",
"Indoor_Day_1": "{{i18n or straight dialogue}}",
"Indoor_Day_2": "{{i18n or straight dialogue}}",
"Indoor_Day_3": "{{i18n or straight dialogue}}",
"Indoor_Day_4": "{{i18n or straight dialogue}}",
"Indoor_Day_5": "{{i18n or straight dialogue}}",
"Rainy_Night_0": "{{i18n or straight dialogue}}",
"Rainy_Night_1": "{{i18n or straight dialogue}}",
"Rainy_Night_2": "{{i18n or straight dialogue}}",
"Rainy_Night_3": "{{i18n or straight dialogue}}",
"Rainy_Night_4": "{{i18n or straight dialogue}}",
"Rainy_Night_5": "{{i18n or straight dialogue}}",
"Indoor_Night_0": "{{i18n or straight dialogue}}",
"Indoor_Night_1": "{{i18n or straight dialogue}}",
"Indoor_Night_2": "{{i18n or straight dialogue}}",
"Indoor_Night_3": "{{i18n or straight dialogue}}",
"Indoor_Night_4": "{{i18n or straight dialogue}}",
"Indoor_Night_5": "{{i18n or straight dialogue}}",
"funLeave_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"funReturn_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"Outdoor_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"Outdoor_0": "{{i18n or straight dialogue}}",
"Outdoor_1": "{{i18n or straight dialogue}}",
"Outdoor_2": "{{i18n or straight dialogue}}",
"Outdoor_3": "{{i18n or straight dialogue}}",
"Outdoor_4": "{{i18n or straight dialogue}}",
"spouseRoom_{{ModId}}_<NPCNAME>": "{{i18n or straight dialogue}}",
"OneKid_0": "{{i18n or straight dialogue}}",
"OneKid_1": "{{i18n or straight dialogue}}",
"OneKid_2": "{{i18n or straight dialogue}}",
"OneKid_3": "{{i18n or straight dialogue}}",
"OneKid_4": "{{i18n or straight dialogue}}",
"TwoKids_0": "{{i18n or straight dialogue}}",
"TwoKids_1": "{{i18n or straight dialogue}}",
"TwoKids_2": "{{i18n or straight dialogue}}",
"TwoKids_3": "{{i18n or straight dialogue}}",
"TwoKids_4": "{{i18n or straight dialogue}}",
"Good_0": "{{i18n or straight dialogue}}",
"Good_1": "{{i18n or straight dialogue}}",
"Good_2": "{{i18n or straight dialogue}}",
"Good_3": "{{i18n or straight dialogue}}",
"Good_4": "{{i18n or straight dialogue}}",
"Good_5": "{{i18n or straight dialogue}}",
"Good_6": "{{i18n or straight dialogue}}",
"Good_7": "{{i18n or straight dialogue}}",
"Good_8": "{{i18n or straight dialogue}}",
"Good_9": "{{i18n or straight dialogue}}",
"Neutral_0": "{{i18n or straight dialogue}}",
"Neutral_1": "{{i18n or straight dialogue}}",
"Neutral_2": "{{i18n or straight dialogue}}",
"Neutral_3": "{{i18n or straight dialogue}}",
"Neutral_4": "{{i18n or straight dialogue}}",
"Neutral_5": "{{i18n or straight dialogue}}",
"Neutral_6": "{{i18n or straight dialogue}}",
"Neutral_7": "{{i18n or straight dialogue}}",
"Neutral_8": "{{i18n or straight dialogue}}",
"Neutral_9": "{{i18n or straight dialogue}}",
"Bad_0": "{{i18n or straight dialogue}}",
"Bad_1": "{{i18n or straight dialogue}}",
"Bad_2": "{{i18n or straight dialogue}}",
"Bad_3": "{{i18n or straight dialogue}}",
"Bad_4": "{{i18n or straight dialogue}}",
"Bad_5": "{{i18n or straight dialogue}}",
"Bad_6": "{{i18n or straight dialogue}}",
"Bad_7": "{{i18n or straight dialogue}}",
"Bad_8": "{{i18n or straight dialogue}}",
"Bad_9": "{{i18n or straight dialogue}}",
}
}
]
}
NPCGiftTastes.json
These are absolutely necessary for an NPC to exist, if this doesn't exist, the NPC doesn't exist.
This is a string field, delimited by "/"
Index 0 = LOVED DIALOGUE REACTION
Index 1 = LOVED GIFTS
Index 2 = LIKED DIALOGUE REACTION
Index 3 = LIKED GIFTS
Index 4 = DISLIKED DIALOGUE REACTION
Index 5 = DISLIKED GIFTS
Index 6 = HATED DIALOGUE REACTION
Index 7 = HATED GIFTS
Index 8 = NEUTRAL GIFT REACTION
Index 9 = NEUTRAL GIFTS
Note: You do not have to have your i18n token be Gifts.Loved/Gifts.Liked, etc, you can make it quite literally Loved, Liked, as long as you're targeting the token correctly
{
"Changes": [
{
"LogName": "NPCGiftTastes Edit",
"Action": "EditData",
"Target": "Data/NPCGiftTastes",
"Entries": {
"{{<NPCNAME>}}": "{{i18n:Gifts.Loved}}/96 97 98 99 104 109 114 121 122 124 125 MysticSyrup MysticTreeSeed/{{i18n:Gifts.Liked}}/198 202 212 213 214 218 219 225 226 227 228 242 265 727 728 730 732 873 904 905 906 907 921 FlashShifter.StardewValleyExpandedCP_Void_Salmon_Sushi FlashShifter.StardewValleyExpandedCP_Frog_Legs category_flowers/{{i18n:Gifts.Disliked}}/78 259 256 685 769/{{i18n:Gifts.Hated}}/889 909 910 Moss 203 209 306 307 308 FlashShifter.StardewValleyExpandedCP_Void_Soul FlashShifter.StardewValleyExpandedCP_Void_Shard FlashShifter.StardewValleyExpandedCP_Void_Pebble FlashShifter.StardewValleyExpandedCP_Swirl_Stone category_junk/{{i18n:Gifts.Neutral}}/334 335 336 337 338 378 380 382 384 386 388 390 394 FlashShifter.StardewValleyExpandedCP_Void_Root FlashShifter.StardewValleyExpandedCP_Void_Delight FlashShifter.StardewValleyExpandedCP_Slime_Berry FlashShifter.StardewValleyExpandedCP_Poison_Mushroom FlashShifter.StardewValleyExpandedCP_Monster_Mushroom FlashShifter.StardewValleyExpandedCP_Monster_Fruit category_trinket/"
}
}
]
}
BONUS CONTENT
Ripped straight from Lance Super Sweet Expanded, here is a TextOperation that adds in gifts to an NPC's "LOVED" gifts, see above for where to replace the "1" if you want to add into different sections like, "LIKED", "HATED", etc. This is for NPCs that ALREADY EXIST.
{
//Lance's gift tastes
"Action": "EditData",
"Target": "Data/NPCGiftTastes",
"When": { "HasSeenEvent": "6951319" },
"TextOperations": [
{
"Operation": "Append",
"Target": [ "Fields", "Lance", "1" ],
"Value": "218 {{ModId}}_Khanjar {{ModId}}_Book {{ModId}}_camping {{ModId}}_mishaps {{ModId}}_Rope {{ModId}}_shenanigans {{ModId}}_Razor",
"Delimiter": " "
}
]
},
Schedule.json
Note that you must have a spring schedule, it defaults to spring if it cannot find any other schedule.
{
"Changes": [
{
"LogName": "<NPCNAME>'s Schedule",
"Action": "EditData",
"Target": "Characters/schedules/{{<NPCNAME>}}",
"Entries": {
"spring": "610 {{ModId}}_<CustomLocationIfNecessary> 18 4 <NPCNAME>_<animationDescriptionKey> \"Strings\\schedules\\<NPCNAME>:<KeyIdentifierIni18n>\"/2600 {{ModId}}_<CustomLocationIfNecessary> 20 3 <NPCNAME>_<animationDescriptionKey>",
}
}
]
}
ScheduleDialogue.json
If you have any strings in your schedule from above, they follow a very strict layout for the token. It needs to be the NPCNAME:<what you've used>
{
"Changes": [
{
"LogName": "<NPCNAME>'s schedule dialogue",
"Action": "EditData",
"Target": "Strings/schedules/{{<NPCNAME>}}",
"Entries": {
//spring
"{{<NPCNAME>}}:<KeyIdentifierIni18n>": "{{i18n:<NPCNAME>:<KeyIdentifierIni18n>}}",
}
}
]
}
animationDescriptions.json
Note that the <NPCNAME>_sleep needs to be lower-cased, this is hardcoded in the game, you can simply do
{{Lowercase:{{<NPCNAME>}}}}
and it will come out to be =
{{modid}}_<npcname>
in all lowercase
The ones that are affected by the lower case are:
"<npc internal name lowercased>_sleep": "#/#/#",
"<npc internal name lowercased>_beach_towel": "#/#/#",
"<npc internal name lowercased>_beach_dance": "#/#/#",
"<npc internal name lowercased>_beach_drink": "#/#/#",
"<npc internal name lowercased>_beach_umbrella": "#/#/#",
{
"Changes": [
{
"LogName": "NPC animationDescriptions edit",
"Action": "EditData",
"Target": "Data/animationdescriptions",
"Entries": {
"<NPCNAME>_stand": "0/17 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 0/0"
}
}
]
}
Appearance.json
If you've set your appearance to null in the Data/Character, you can still target an edit it, say if you want seasonal outfits, different overlays, etc. Appearance Data
Obviously be sure to load your appropriate data, you can also use InternalAssetKey which is further down, it requires no loads but it does require you to understand what you're doing.
{
"Changes": [
{
"LogName": "Load Base",
"Action": "Load",
"Target": "Portraits/{{<NPCNAME>}}, Characters/{{<NPCNAME>}}, Portraits/{{<NPCNAME>}}_Spring, Characters/{{<NPCNAME>}}_Spring, Portraits/{{<NPCNAME>}}_Flower, Portraits/{{<NPCNAME>}}_Beach, Characters/{{<NPCNAME>}}_Beach",
"FromFile": "assets/{{TargetPathOnly}}/{{TargetWithoutPath}}.png"
},
{
"LogName": "<NPCNAME> Appearance Data",
"Action": "EditData",
"Target": "Data/Characters",
"TargetField": [ "{{<NPCNAME>}}", "Appearance" ],
"Entries": {
//Regular Outfits
"{{ModId}}.Spring": {
"Id": "{{ModId}}.Spring",
"Condition": "SEASON spring",
"Sprite": "Characters/<NPCNAME>_Spring",
"Portrait": "Portraits/<NPCNAME>_Spring",
"Precedence": -100
},
//festivals
"{{ModId}}.<NPCNAME>Flower": {
"Id": "{{ModId}}.<NPCNAME>Flower",
"Condition": "IS_EVENT festival_spring24",
"Sprite": "Characters/<NPCNAME>_Flower",
"Portrait": "Portraits/<NPCNAME>_Flower",
"Precedence": -100
},
//beach
"{{ModId}}.<NPCNAME>Beach": {
"Id": "{{ModId}}.<NPCNAME>Beach",
"Sprite": "Characters/<NPCNAME>_Beach",
"Portrait": "Portraits/<NPCNAME>_Beach",
"IsIslandAttire": true,
"Precedence": -1000
}
}
}
]
}
InternalAssetKey
There aren't that many docs for InternalAssetKey, but it checks for your uniqueid (manifest), uses the top folder, and checks through the folders that you give in the key.
Note how the precedence has been removed. This is necessary for a PURELY CUSTOM NPC. This is so other mods can set their precedences lower to overwrite the default.
Again, this form is not beginner friendly, this is purely for expansions with multiple NPCs to prioritize load times.
{
"Changes": [
{
"LogName": "<NPCNAME> Appearance Data",
"Action": "EditData",
"Target": "Data/Characters",
"TargetField": [ "{{<NPCNAME>}}", "Appearance" ],
"Entries": {
//Regular Outfits
"{{ModId}}.Spring": {
"Id": "{{ModId}}.Spring",
"Condition": "SEASON spring",
"Sprite": "{{InternalAssetKey: Assets/Characters/<NPCNAME>_Spring.png}}",
"Portrait": "{{InternalAssetKey: Assets/Portraits/<NPCNAME>_Spring.png}}"
},
//festivals
"{{ModId}}.<NPCNAME>Flower": {
"Id": "{{ModId}}.<NPCNAME>Flower",
"Condition": "IS_EVENT festival_spring24",
"Sprite": "{{InternalAssetKey: Assets/Characters/<NPCNAME>_Flower.png}}",
"Portrait": "{{InternalAssetKey: Assets/Portraits/<NPCNAME>_Flower.png}}"
},
//beach
"{{ModId}}.<NPCNAME>Beach": {
"Id": "{{ModId}}.<NPCNAME>Beach",
"Sprite": "{{InternalAssetKey: Assets/Characters/<NPCNAME>_Beach.png}}",
"Portrait": "{{InternalAssetKey: Assets/Portraits/<NPCNAME>_Beach.png}}",
"IsIslandAttire": true
}
}
}
]
}
Festivals.json
This is one of the hardest to code for, since if you want your Custom NPC to go to festivals, you have to fight other expansions, other custom NPCS if you want true compatibility.
But if you do want to add your NPC to a festival it is assumed you understand Text Operations
Note: DO NOT, UNDER ANY CIRCUMSTANCES, GO BEYOND Y2. YOU WILL BREAK ALL OTHER CUSTOM NPCS.
{
"Changes": [
//////////////////////////////////////egg
{
"LogName": "Egg Festival edit",
"Action": "EditData",
"Target": "Data/Festivals/spring13",
"Entries": {
"{{<NPCNAME>}}": "{{i18n:<NPCNAME>.EggFestival}}",
"{{<NPCNAME>}}_y2": "{{i18n:<NPCNAME>.EggFestival_y2}}"
},
"TextOperations": [
{
"Operation": "Append",
"Target": [
"Entries",
"Set-Up_additionalCharacters"
],
"Value": "{{<NPCNAME>}} 47 77 down",
"Delimiter": "/"
},
{
"Operation": "Append",
"Target": [
"Entries",
"Set-Up_additionalCharacters_y2"
],
"Value": "{{<NPCNAME>}} 47 77 down",
"Delimiter": "/"
}
]
}
]
}