Star Sector How to Continue Campaign
Only up to date for version 0.9.1. It is likely still broadly correct but not verified for the most up to date data yet. Please double check the Version History
Description [ ]
A quest is a mission that is offered during the campaign (not to be confused with Missions). It consists of one or more stages. For example, a quest to deliver goods to a specific market is a single stage. The quest in which the player acquires the Planetary Shield building, by comparison, requires the player travel to multiple points during multiple stages.
Creating a quest requires some knowledge of Java or Kotlin (what is this?) and cannot be done only by modifying json and csv files.
Below here, this page describes the pre-0.95.0 way of creating quests, which still works but there is a new possible approach. The Internal Affairs mod is an excellent example of the 0.95.0 approach to building quests. |
The bulk of a typical quest is contained in two parts; the BarEvent and the BaseIntelPlugin
. Additionally, an InteractionDialog
may be used to display dialogs outside of the bar.
Note: "quest" is a loose definition. A quest could be offered randomly when the player enters a system, with no BarEvent
and direct them to a specific location without using Intel either; just InteractionDialog
s. However, this page is going to cover the most common and expected type, which is offered at a bar and tracked using Intel.
We're going to build a quest where the player is told to travel to a random planet - a simple "go here" quest. It will only be completable once.
Structure [ ]
The three components above (BarEvent
, BaseIntelPlugin
, and possibly InteractionDialog
) are technically all that is needed to build a quest (plus a bit more code to get the game to recognize them). However, it makes it much easier to read and manage if we have a "coordinator" class as well.
-
BarEvent
defines what the player sees when they enter the bar, as well as the dialog where they can accept or reject the quest offer. -
BarEventCreator
is used by the game to create the BarEvent when needed. -
BaseModPlugin
is needed to actually add your BarEvent to the game. - A coordinator class tracks the player's progress, holds constant variables, and moves the player from one stage to another. It's best to make this a static class that holds no state; this will be explained later.
-
BaseIntelPlugin
displays the player's progress in the Intel screen and shows them what to do next. -
InteractionDialogPlugin
can be used to show a dialog outside of the bar. For example, we may want to display "The cargo is unloaded and the manager hands you your payment of 100,000 credits" when the player lands on a planet. We're not limited to displaying dialogs on planets, however. - And finally, a
BaseCampaignPlugin
can be useful to show InteractionDialogs.
BarEvent [ ]
The place where it all starts; at the bar. Quests don't need to start at the bar, but that is the common place and is where the example quest will start.
To create our bar event, we'll need to choose and implement the interface PortsideBarEvent
. There are a few ready-made implementations to chose from.
BaseBarEvent
- Bare-bones, abstract implementation of PortsideBarEvent
. Very flexible.
BaseBarEventWithPerson
- Abstract implementation of BaseBarEvent
that adds a person for the user to talk to with a customizable gender, job, faction, portrait, etc.
BaseGetCommodityBarEvent
- Abstract implementation for a quest where the user needs to bring some commodity somewhere. "A concerned-looking man is sitting at a corner table..." is an example of such a quest.
We're going to get our quest from a person at the bar, so we're going to take BaseBarEventWithPerson
and create a subclass of it called DemoBarEvent
. It can be placed anywhere.
Now, there are a few methods that we'll want to override.
public class DemoBarEvent extends BaseBarEventWithPerson { /** * True if this event may be selected to be offered to the player, * or false otherwise. */ public boolean shouldShowAtMarket ( MarketAPI market ) { // add any conditions you want return super . shouldShowAtMarket ( market ) && ! getMarket (). getFactionId (). equals ( "luddic_path" ); } /** * Set up the text that appears when the player goes to the bar * and the option for them to start the conversation. */ @Override public void addPromptAndOption ( InteractionDialogAPI dialog , Map < String , MemoryAPI > memoryMap ) { // Calling super does nothing in this case, but is good practice because a subclass should // implement all functionality of the superclass (and usually more) super . addPromptAndOption ( dialog , memoryMap ); regen ( dialog . getInteractionTarget (). getMarket ()); // Sets field variables and creates a random person // Display the text that will appear when the player first enters the bar and looks around dialog . getTextPanel (). addPara ( "A small crowd has gathered around a " + getManOrWoman () + " who looks to be giving " + "some sort of demonstration." ); // Display the option that lets the player choose to investigate our bar event dialog . getOptionPanel (). addOption ( "See what the demonstration is about" , this ); } /** * Called when the player chooses this event from the list of options shown when they enter the bar. */ @Override public void init ( InteractionDialogAPI dialog , Map < String , MemoryAPI > memoryMap ) { super . init ( dialog , memoryMap ); // Choose where the player has to travel to DemoQuestCoordinator . initQuest (); // If player starts our event, then backs out of it, `done` will be set to true. // If they then start the event again without leaving the bar, we should reset `done` to false. done = false ; // The boolean is for whether to show only minimal person information. True == minimal dialog . getVisualPanel (). showPersonInfo ( person , true ); // Launch into our event by triggering the "INIT" option, which will call `optionSelected()` this . optionSelected ( null , OptionId . INIT ); } /** * This method is called when the player has selected some option for our bar event. * * @param optionText the actual text that was displayed on the selected option * @param optionData the value used to uniquely identify the option */ @Override public void optionSelected ( String optionText , Object optionData ) { if ( optionData instanceof OptionId ) { // Clear shown options before we show new ones dialog . getOptionPanel (). clearOptions (); // Handle all possible options the player can choose switch (( OptionId ) optionData ) { case INIT : // The player has chosen to walk over to the crowd, so let's tell them what happens. dialog . getTextPanel (). addPara ( "You walk over and see that the " + getManOrWoman () + " is showing the crowd how to create quest mods for a video game." ); dialog . getTextPanel (). addPara ( "It seems that you can learn more by traveling to " + DemoQuestCoordinator . getDestinationPlanet (). getName ()); // And give them some options on what to do next dialog . getOptionPanel (). addOption ( "Take notes and decide to travel to learn more" , OptionId . TAKE_NOTES ); dialog . getOptionPanel (). addOption ( "Leave" , OptionId . LEAVE ); break ; case TAKE_NOTES : // Tell our coordinator class that the player just started the quest DemoQuestCoordinator . startQuest (); dialog . getTextPanel (). addPara ( "You take some notes. Quest mods sure seem like a lot of work..." ); dialog . getOptionPanel (). addOption ( "Leave" , OptionId . LEAVE ); break ; case LEAVE : // They've chosen to leave, so end our interaction. This will send them back to the bar. // If noContinue is false, then there will be an additional "Continue" option shown // before they are returned to the bar. We don't need that. noContinue = true ; done = true ; // Removes this event from the bar so it isn't offered again BarEventManager . getInstance (). notifyWasInteractedWith ( this ); break ; } } } enum OptionId { INIT , TAKE_NOTES , LEAVE } }
Now that we have created our quest offer at the bar, we need to actually tell Starsector to offer it at the bar.
BarEventCreator [ ]
The BarEventCreator
is a small class Starsector uses to, unsurprisingly, create bar events. The game keeps a list of these creators and periodically creates bar events for the player to find.
There is an abstract default implementation of BarEventCreator
that we will use; BaseBarEventCreator
. For missions that should be offered frequently and constantly, DeliveryBarEventCreator
is also an option.
This class can be implemented with minimal code.
public class DemoBarEventCreator extends BaseBarEventCreator { @Override public PortsideBarEvent createBarEvent () { return new DemoBarEvent (); } }
There are additional methods (viewable here) that can be overriden to customize how frequently and/or rarely the player should encounter the event.
BaseModPlugin [ ]
A BaseModPlugin
is what we use to tell Starsector what to load and when. It has hooks (methods) that are called by the game itself.
If this guide has been followed sequentially, we now have a BarEvent
and a BarEventCreator
, but the game doesn't yet know how to add these to a bar. We can use the onGameLoad
method to do so.
public class DemoBaseModPlugin extends BaseModPlugin { /** * Called when the player loads a saved game. * * @param newGame true if the save game was just created for the first time. * Note that there are a few `onGameLoad` methods that may be a better choice than using this parameter */ @Override public void onGameLoad ( boolean newGame ) { super . onGameLoad ( newGame ); BarEventManager barEventManager = BarEventManager . getInstance (); // If the prerequisites for the quest have been met (optional) and the game isn't already aware of the bar event, // add it to the BarEventManager so that it shows up in bars if ( DemoQuestCoordinator . shouldOfferQuest () && ! barEventManager . hasEventCreator ( DemoBarEventCreator . class )) { barEventManager . addEventCreator ( new DemoBarEventCreator ()); } } }
All that's left is to add this to mod_info.json
:
"modPlugin" : "your.class.package.DemoBaseModPlugin"
and with that, Starsector will add the quest to bars around the sector! Of course, the code won't compile yet, because we still need to add the coordinator class.
Coordinator [ ]
A coordinator class handles all of the quest's logic and tracks the player's progress.
Note: This class isn't strictly necessary, as the logic it contains could instead be distributed throughout the rest of the quest's classes, but having one central class makes the quest code easier to understand and follow.
This class should be stateless, meaning that it has no field variables (unless they are read-only). Being stateless has a few advantages.
- Ensures that the player's save game is the single source of truth for quest-related information.
- Stateless classes also allow methods to be reused and combined more confidently.
Despite being stateless, the coordinator is able to track the player's quest progress. It does so by keeping state in the player's save file, rather than in the coordinator class, using either Global.getSector().getMemory() or Global.getSector().getPersistentData(). These work equally well, although persistent data is simpler.
Coordinator classes will not be automatically saved in the player's save game.
Here is a simple implementation for our Demo quest:
/** * Coordinates and tracks the state of Demo quest. */ class DemoQuestCoordinator { /** * The tag that is applied to the planet the player must travel to. */ private static String TAG_DESTINATION_PLANET = "Demo_destination_planet" ; static SectorEntityToken getDestinationPlanet () { return Global . getSector (). getEntityById ( TAG_DESTINATION_PLANET ); } static boolean shouldOfferQuest () { return true ; // Set some conditions } /** * Called when player starts the bar event. */ static void initQuest () { chooseAndTagDestinationPlanet (); } /** * Player has accepted quest. */ static void startQuest () { Global . getSector (). getIntelManager (). addIntel ( new DemoIntel ()); } /** * Very dumb method that idempotently tags a random planet as the destination. */ private static void chooseAndTagDestinationPlanet () { if ( getDestinationPlanet () == null ) { StarSystemAPI randomSystem = Global . getSector (). getStarSystems () . get ( new Random (). nextInt ( Global . getSector (). getStarSystems (). size ())); PlanetAPI randomPlanet = randomSystem . getPlanets () . get ( new Random (). nextInt ( randomSystem . getPlanets (). size ())); randomPlanet . addTag ( TAG_DESTINATION_PLANET ); } } }
Intel [ ]
Starsector's version of a quest log is the Intel Manager.
Intel must implement the IntelInfoPlugin
, which is most easily done by creating a subclass of either BaseIntelPlugin
or BreadcrumbIntel
.
BaseIntelPlugin
implements basic intel logic, but nothing more. Choose this for the most flexibility.
BreadcrumbIntel
is a subclass of BaseIntelPlugin
that adds support for pointing the player to a destination, and optionally showing a source destination and an arrow between them. Has some potentially unwanted default behavior, such as using the "Exploration"
tag. Choose this to guide the player to some destination.
A piece of intel has a few different parts.
- Icon: The quest icon. Vanilla icons are 40x40 px pngs.
- Name: The current title of the quest, eg
"Delivery - Completed"
. "Name" is part ofBreadcrumbIntel
, but notBaseIntelPlugin
, which requires the title be added to the IntelInfo section manually. - Info: Text that appears underneath the name showing a quick objective or status, eg
" - 45,000 reward"
. - Small Description: A bit of a misnomer, this is the description panel on the right side of the Intel Manager.
- Intel Tags: The tag(s) that will appear in the Intel Manager and display this intel if selected.
Here is our fairly barebones implementation ofBreadcrumbIntel
public class DemoIntel extends BreadcrumbIntel { public DemoIntel ( SectorEntityToken foundAt , SectorEntityToken target ) { super ( foundAt , target ); } @Override public String getIcon () { return "graphics/icons/intel/player.png" ; } @Override public String getName () { return "Demo Quest" + ( DemoQuestCoordinator . isComplete () ? " - Completed" : "" ); } /** * The small list entry on the left side of the Intel Manager * * @param info the text area that shows the info * @param mode where the info is being shown */ @Override public void createIntelInfo ( TooltipMakerAPI info , ListInfoMode mode ) { // The call to super will add the quest name, so we just need to add the summary super . createIntelInfo ( info , mode ); info . addPara ( "Destination: %s" , // text to show. %s is highlighted. 3f , // padding on left side of text. Vanilla hardcodes these values so we will too super . getBulletColorForMode ( mode ), // color of text Misc . getHighlightColor (), // color of highlighted text DemoQuestCoordinator . getDestinationPlanet (). getName ()); // highlighted text // This will display in the intel manager like: // Demo Quest // Destination: Ancyra } @Override public void createSmallDescription ( TooltipMakerAPI info , float width , float height ) { info . addImage ( "graphics/illustrations/fly_away.jpg" , // path to sprite width , 128 , // height 10 f ); // left padding info . addPara ( "You learned a little about quest design at a bar on " + foundAt . getName () + " and are traveling to %s to learn more." , // text to show. %s is highlighted. 10 f , // padding on left side of text. Vanilla hardcodes these values so we will too Misc . getHighlightColor (), // color of highlighted text target . getName ()); // highlighted text // The super call adds the text from `getText()` (which we'll leave empty) // and then adds the number of days since the quest was acquired, which is // typically the bottom-most thing shown. Therefore, we'll make the call to // super as the last thing in this method. super . createSmallDescription ( info , width , height ); } /** * Return whatever tags your quest should have. You can also create your own tags. */ @Override public Set < String > getIntelTags ( SectorMapAPI map ) { return new HashSet <> ( Arrays . asList ( Tags . INTEL_EXPLORATION , Tags . INTEL_STORY )); } }
Interaction Dialogs [ ]
An interaction dialog is simply a dialog window with options. Bar events are displayed using interaction dialogs; in order to display text, we used dialog.getTextPanel().addPara(text)
. dialog
is a reference to the bar event's interaction dialog.
However, interaction dialogs can also be displayed on their own at any point, not just in bars, and we're going to leverage that to give the end of our demo quest a custom dialog that appears instead of the normal planet dialog.
There are two different interfaces/classes to choose from.
-
InteractionDialogPlugin
is theinterface
for all interaction dialogs, making it the most flexible. -
PaginatedOptions
is great for presenting many options, such as a list of all factions. It displays "Next" and "Previous" as options. Check out the source ofPaginatedOptionsExample
for a basic example.
We don't need pages of options, so we'll implement this using InteractionDialogPlugin
.
public class DemoEndDialog implements InteractionDialogPlugin { protected InteractionDialogAPI dialog ; /** * Called when the dialog is shown. * * @param dialog the actual UI element being shown */ @Override public void init ( InteractionDialogAPI dialog ) { // Save the dialog UI element so that we can write to it outside of this method this . dialog = dialog ; // Launch into our event by triggering the invisible "INIT" option, // which will call `optionSelected()` this . optionSelected ( null , DemoBarEvent . OptionId . INIT ); } /** * This method is called when the player has selected some option on the dialog. * * @param optionText the actual text that was displayed on the selected option * @param optionData the value used to uniquely identify the option */ @Override public void optionSelected ( String optionText , Object optionData ) { if ( optionData instanceof OptionId ) { // Clear shown options before we show new ones dialog . getOptionPanel (). clearOptions (); // Handle all possible options the player can choose switch (( OptionId ) optionData ) { // The invisible "init" option was selected by the init method. case INIT : dialog . getTextPanel (). addPara ( "As soon as your shuttle touches down, your mind is filled " + "with knowledge of how to create quest mods. How amazingly convenient." ); dialog . getTextPanel (). addPara ( "You resolve to go off and create your very own!" ); // The quest is completed as soon as the plot is resolved DemoQuestCoordinator . completeQuest (); dialog . getOptionPanel (). addOption ( "Leave" , OptionId . LEAVE ); break ; case LEAVE : dialog . dismiss (); break ; } } } enum OptionId { INIT , LEAVE } // The rest of the methods must exist, but can be ignored for our simple demo quest. @Override public void optionMousedOver ( String optionText , Object optionData ) { // Can add things like hints for what the option does here } @Override public void advance ( float amount ) { } @Override public void backFromEngagement ( EngagementResultAPI battleResult ) { } @Override public Object getContext () { return null ; } @Override public Map < String , MemoryAPI > getMemoryMap () { return null ; } }
We now have a dialog that nearly wraps up the quest; all that's left is to show it to the player. For that, we'll create our own Campaign Plugin.
Showing a Dialog [ ]
To finish up our quest, we need to show a dialog when the player interacts with the destination planet.
There are two main options for doing this; CampaignPlugin
and rules.csv
.
-
BaseCampaignPlugin
is the programmatic way that trades the complexity ofrules.csv
for the complexity of code. This method makes the quest mod arguably easier to understand, as there's less "magic"; any IDE will be able to see that an interaction dialog is being created in this class, whereas it's harder to determine how a dialog is being launched if it's done byrules.csv
. -
rules.csv
; ah,rules.csv
. Launching a dialog using this is the best choice for mod compatibility, as it will allow other modders to override your dialog launch trigger by the other mod usingrules.csv
.- To give an example, the Gates Awakened mod originally showed a dialog whenever the player interacted with a gate, triggered using a
CampaignPlugin
. However, Vayra's Sector had a special interaction that could occurr occasionally when the player interacted with a gate, triggered usingrules.csv
. Because theCampaignPlugin
was always overriding anything inrules.csv
, the Vayra's Sector interaction was never triggered as long as both mods were enabled. The fix was for Gates Awakened to trigger its dialog usingrules.csv
, and for Vayra's Sector to set the dialog trigger to a higher priority than the one in Gates Awakened.
- To give an example, the Gates Awakened mod originally showed a dialog whenever the player interacted with a gate, triggered using a
The game chooses which interaction dialog to use to handle a player interaction by:
- First looking at all
CampaignPlugin
s and seeing if any can handle the interaction. - If multiple
CampaignPlugin
s can handle the interaction, it chooses the one with the highest priority, as determined byPickPriority
. - If multiple plugins have the same priority, it chooses one somehow ("in an undefined way").
- If the plugin that is picked is
RuleBasedInteractionDialogPluginImpl
, then it will look at all possibilities inrules.csv
and pick the one with the highest score.
Campaign Plugin [ ]
TODO
Source: https://starsector.fandom.com/wiki/Modding_Quests
0 Response to "Star Sector How to Continue Campaign"
Publicar un comentario