Someone was kind enough to send me a PM with a question that involves rules + some other stuff. I'm going to go ahead and answer it here.
Here's what they wanted to know:
- How to create an NPC
- How to assign said NPC to a station/planet
- Basic interactions with NPC (via rules.csv) (Saying hello/goodbye)
The weird thing is that
I don't know how to make mods because that's not what I do. I think the following should be correct - I asked Alex for help with a couple items - but feel free to let me know if something is wrong and I'll fix it.
Creating an NPCSo presumably your mod has some kind of code it runs to set itself up, like
ModPluginOnNewGameAfterEconomyLoad(). You could create a call to make the character in there. Let's use Rayan Arroyo as an example.
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.PersonImportance;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import com.fs.starfarer.api.characters.FullName.Gender;
import com.fs.starfarer.api.characters.ImportantPeopleAPI;
import com.fs.starfarer.api.characters.PersonAPI;
import com.fs.starfarer.api.impl.campaign.ids
These all get used when we make a character.
public static String ARROYO = "arroyo";
ModPluginOnNewGameAfterEconomyLoad() {
ImportantPeopleAPI ip = Global.getSector().getImportantPeople();
MarketAPI market = Global.getSector().getEconomy().getMarket("eochu_bres");
if (market != null) {
PersonAPI person = Global.getFactory().createPerson();
person.setId(ARROYO);
person.setFaction(Factions.TRITACHYON);
person.setGender(Gender.MALE);
person.setRankId(Ranks.SENIOR_EXECUTIVE);
person.setPostId(Ranks.POST_ENTREPRENEUR);
person.setImportance(PersonImportance.HIGH);
person.getName().setFirst("Rayan");
person.getName().setLast("Arroyo");
person.setPortraitSprite(Global.getSettings().getSpriteName("characters", person.getId()));
person.getStats().setSkillLevel(Skills.BULK_TRANSPORT, 1);
person.getStats().setSkillLevel(Skills.INDUSTRIAL_PLANNING, 1);
person.addTag(Tags.CONTACT_TRADE);
person.addTag(Tags.CONTACT_MILITARY);
person.setVoice(Voices.BUSINESS);
market.getCommDirectory().addPerson(person, 2);
market.getCommDirectory().getEntryForPerson(person).setHidden(true);
market.addPerson(person);
ip.addPerson(person);
}
}
Some of this is self-explanatory, like "ImportantPeopleAPI" is used for important characters in the game you will want to be able to access via code. Likewise, the market contains the comm directory where the person can be contacted. (You don't need to add a character to a market. Some characters who cruise around in their own fleets like Caden and Hyder don't get added to a market.)
Character skills aren't really used unless they appear in combat, but I like adding them for flavor.
Voice sets a value on the character called $voice which is used in some generic dialog interactions that may show up if you don't override them. Mostly used in procedurally generated character interactions (planet admins, bar encounters, patrol leaders). They are:
- aristo : a rich, stuck-up aristocrat
- official : an official-sounding bureaucrat
- business : cheerfully interested in profits and making deals
- villain : a slightly over the top evil-sounding villain
- soldier : a gruff military professional
- faithful : a Luddic faithful, into blessings and prayer
- pather : like the Luddic, but more militant and aggressive
- spacer : halfway between working class and talks-like-a-pirate
- scientist : a slightly egotistical, distracted scientist
Assigning an NPC to a marketmarket.getCommDirectory().addPerson(person, 2);
This adds Rayan Arroyo to the third position in the comms directory (we start counting from 0, of course).
market.getCommDirectory().getEntryForPerson(person).setHidden(true);
This makes Rayan Arroyo invisible at the start of the game. To make him visible, we call "SetPersonHidden arroyo false" in a rules script block.
(We can call "SetPersonHidden arroyo true" to make him hidden again, if we like. We could also call "MovePersonToMarket arroyo kantas_den" to make Arroyo set up shop at Kanta's Den instead of Eochu Bres at some point during the game.)
Also important: Rayan Arroyo's portrait.
Presumably there's a settings.json in your mod. It needs something like this in it:
"graphics":{
"characters":{
"arroyo":"graphics/portraits/characters/arroyo.png",
},
}
That way when we set the portrait to "arroyo", graphics/portraits/characters/arroyo.png will show up.
Basic NPC InteractionsLet's start with a simple greeting and goodbye. This block goes into the rules csv for your mod.
(Sorry about the tables - this forum doesn't make for very readable formatting.)
id | trigger | conditions | script | text | options |
testArroyoGreeting | PickGreeting | $id == arroyo score:1000 | ShowPersonVisual SetShortcut cutCommLink "ESCAPE" | "This better be good." Rayan Arroyo looks you over disapprovingly, "Remind me to take you shopping for a proper suit. I know a guy who can do wonders." | cutCommLink:"I've heard enough. Good day, sir!" |
With just this, you'll open the comm link to Arroyo, he comments on your fashion sense, and you can cut the link in a huff.
Let's go over this block by block.
idtestArroyoGreeting is our unique id for the rule
trigger PickGreeting is called when you begin a conversation with a character via the comm directory (or by opening comms with their fleet). If we did not include this override for PickGreeting, the dialog would automatically use the rule with id
convDefaultGreetingBusiness for Arroyo (because he has $voice == business attached to his memory context).
scriptsShowPersonVisual displays Arroyo's portrait. The default PickGreeting rule would handle this, but we need to call it because we're overriding that rule.
Similarly,
SetShortcut cutCommLink "ESCAPE" attaches the option with the key "cutCommLink" to the ESCAPE key on the keyboard so we have a default hotkey to exit the conversation.
textThis is shown in the text box.
optionsWe set one option,
cutCommLink, which is a default option to exit a conversation and close the dialog box.
It's not a very interesting conversation, so let's mix it up just slightly while appreciating some of the default dialog structure!
Here's a version of Arroyo that will use the default dialog greeting and exit. We're just adding a new dialog option which will not appear again after the player chooses it once:
id | trigger | conditions | script | text | options |
testArroyoAskAboutSuit | PopulateOptions | $id == arroyo !$askedAboutSuit | | Arroyo says he knows about where to get a nice suit. Maybe if you asked nicely... | testArroyo_askAboutSuit2:"Say, could you tell me where to get a nice suit?" |
testArroyoAskAboutSuit2 | DialogOptionSelected | $option == testArroyo_askAboutSuit2 | $askedAboutSuit = true $player.gotArroyoTailorAddress = true AddTextSmall "Received address for Arroyo's tailor on Eochu Bres" highlight FireAll PopulateOptions
| Arroyo raises an eyebrow. "I thought you'd never ask. Here."
He taps something, and with a satisfied ping the address of a tailor appears on your TriPad. | |
Remember, the default PickGreeting calls all valid rules under the trigger PopulateOptions. So if we added more options with $id == arroyo in the conditions block, we could add more unique options. Here, we've added just one more option which will appear in addition to the default cutCommLink rule.
$id == arroyo:
This condition requires that the active character must have the id "arroyo"
!$askedAboutSuit:
This means that the condition $askedAboutSuit must be false. That variable gets set to true after you use the option asking about the suit so the option to ask doesn't show up again.
Once we fire the
testArroyoAskAboutSuit2 rule, some interesting stuff happens in the script block:
$askedAboutSuit = true:
$askedAboutSuit is set to true on Arroyo, which will stop the condition for getting to this dialog option again.
$player.gotArroyoTailorAddress = true:
This sets a memory flag on the player memory context so (maybe) we can add a new option elsewhere in the game based on the condition $player.gotArroyoTailorAddress being true. Maybe the player can go visit Arroyo's tailor and buy a nice suit!
...And, to be honest, $askedAboutSuit is redundant if we use this. The option to ask about the tailor could simply check that !$player.gotArroyoTailorAddress. But I'm showing off how different scopes of variables work here.
AddTextSmall "Received address for Arroyo's tailor on Eochu Bres" highlight :
This adds a highlighted line of small text to the dialog window. This is usually used to emphasize some important action the player has performed. Note that this text is added AFTER the rule's text block; if you want to add normal-looking text before this special text is added, you either have to write a rule that first makes the special text then triggers another rule with a text block, or you can call the
AddText script command. (I don't usually use this except in very simple cases because formatting this text is more work).
FireAll PopulateOptions :
Because this rule has nothing in the
options block we have to call this because otherwise we're left hanging with no options, which causes an error. This will bring back the default cutCommLink option, and anything else PopulateOptions would show.
Hopefully that all makes sense. I'm slightly thwarted by the default table style here, alas. Happy to answer anything that's confusing.