Skip to content
Alchyr edited this page Oct 29, 2024 · 7 revisions

When making an event, there are two styles you can follow. You can copy the base game's events, which extend AbstractEvent or AbstractImageEvent, or you can use PhasedEvent from BaseMod, which adds another layer on top of the base game's event class. The results are about the same, the only difference is how they are set up.

As a warning, events, regardless of format, are more complicated than most cards or relics.

This tutorial will be explaining how to make an event using PhasedEvent.

More details can be found on BaseMod's wiki: https://github.com/daviscook477/BaseMod/wiki/Custom-Events

Setting up the Class

First, you'll need a place to put your event class. Generally an events package is used, but there are no rules on how or where you put your event classes. In the example image below, the ExampleEvent class is placed in an events package.

image

Next, you'll need to set up the class itself. Add extends PhasedEvent to the class declaration and make sure it's imported.

image

To hold some information about the event, like its ID, text, and image path, some constants will be defined.

public class ExampleEvent extends PhasedEvent {
    public static final String ID = makeID("ExampleEvent"); //The event's ID

    //The text that will be displayed in the event, loaded based on the ID. The text will be set up later in this tutorial.
    private static final EventStrings eventStrings = CardCrawlGame.languagePack.getEventString(ID);
    private static final String NAME = eventStrings.NAME;
    private static final String[] DESCRIPTIONS = eventStrings.DESCRIPTIONS;
    private static final String[] OPTIONS = eventStrings.OPTIONS;

    //For this example, an image from a basegame event is used.
    private static final String IMG = "images/events/weMeetAgain.jpg";
    //To use your own image, it would look more like
    //private static final String IMG = imagePath("events/ExampleEvent.jpg");
    //This would load yourmod/images/events/ExampleEvent.jpg

The last part for the basic setup of the class is the constructor, which will use some of the constants we just defined.

    public ExampleEvent() {
        super(ID, NAME, IMG);
    }

Your class should now look something like this:

image

The Event's Phases

Now that it's time to set up the actual event comes the main difference between PhasedEvent and the base game style of event. Slay the Spire's events normally work by overriding/calling methods to respond to button presses and swap between screens. PhasedEvent is set up entirely in the event's constructor, where you define "phases". Each phase corresponds to a single interaction, and the event works by transitioning between these phases.

For this tutorial, we will only be using TextPhase, which is the normal event screen that displays some text and has buttons to choose between, but PhasedEvent supports other options like a CombatPhase. You can find more complex examples on the BaseMod wiki page.

This example event will be very simple; the player will have the option to get a random relic at the cost of 70 gold, or to do nothing.

    public ExampleEvent() {
        super(ID, NAME, IMG);

        registerPhase("start", new TextPhase(DESCRIPTIONS[0]));
    }

Each phase is registered with a key and the phase itself. This key can be anything, just make it different from any other key used within the event. This key is used to transition between phases. Currently, this will make a text phase that shows DESCRIPTIONS[0] as its body text, but has no options. To add these options, addOption is called on the TextPhase object. Note the placement of parentheses throughout this tutorial, as the methods must be called on the correct objects.

    public ExampleEvent() {
        super(ID, NAME, IMG);

        registerPhase("start", new TextPhase(DESCRIPTIONS[0])
                .addOption(new TextPhase.OptionInfo(OPTIONS[0]).enabledCondition(() -> AbstractDungeon.player.gold >= 70, OPTIONS[1])));
    }

addOption has a variety of parameters it can be given. The simplest one just takes a String and what to do when the option is clicked. This one is more complicated, creating an OptionInfo object manually so that a condition can be added.

This adds an option that will have OPTIONS[0] as its text, and only be clickable if the player has at least 70 gold. If they don't have enough gold, OPTIONS[1] will be used for the text instead.

However, clicking it still doesn't do anything.

        registerPhase("start", new TextPhase(DESCRIPTIONS[0])
                .addOption(new TextPhase.OptionInfo(OPTIONS[0]).enabledCondition(() -> AbstractDungeon.player.gold >= 70, OPTIONS[1])
                        .setOptionResult((i)->{
                            AbstractRelic relic = AbstractDungeon.returnRandomScreenlessRelic(AbstractDungeon.returnRandomRelicTier());
                            AbstractDungeon.player.loseGold(70);
                            AbstractDungeon.getCurrRoom().spawnRelicAndObtain(this.drawX, this.drawY, relic);
                            AbstractEvent.logMetricObtainRelicAtCost(ID, "Obtained Relic", relic, 70); //Optional, adds information to run history
                            transitionKey("they took the relic"); //Move to the phase defined with this key
                        })));

This results in a completed first option, which will cost the player 70 gold for a random relic, and then transition to the "they took the relic" phase, which is not yet defined. Next, another option needs to be added for the player to decline. This one will be far simpler.

        registerPhase("start", new TextPhase(DESCRIPTIONS[0])
                .addOption(new TextPhase.OptionInfo(OPTIONS[0]).enabledCondition(() -> AbstractDungeon.player.gold >= 70, OPTIONS[1])
                        .setOptionResult((i)->{
                            AbstractRelic relic = AbstractDungeon.returnRandomScreenlessRelic(AbstractDungeon.returnRandomRelicTier());
                            AbstractDungeon.player.loseGold(70);
                            AbstractDungeon.getCurrRoom().spawnRelicAndObtain(this.drawX, this.drawY, relic);
                            AbstractEvent.logMetricObtainRelicAtCost(ID, "Obtained Relic", relic, 70); //Optional, adds information to run history
                            transitionKey("they took the relic");
                        }))
                .addOption(OPTIONS[2], (i)->transitionKey("they didn't take the relic")));

This option simply has OPTIONS[2] for its text and will transition to "they didn't take the relic" when clicked.

Next comes defining those two phases, where the player took the relic or skipped it. These will also be simpler, since they will only have one option (ending the event) and don't need to do anything complex.

        registerPhase("they took the relic", new TextPhase(DESCRIPTIONS[1]).addOption(OPTIONS[3], (i)->openMap()));
        registerPhase("they didn't take the relic", new TextPhase(DESCRIPTIONS[2]).addOption(OPTIONS[3], (i)->openMap()));

These display DESCRIPTIONS[1] or DESCRIPTIONS[2] and have a single option, which will open the map (which ends the event). Every event should end on a phase like this, that only has the option to open the map, as players are able to close the map and return to the last screen of an event. If this last screen has a button which does something, like give them a relic, this could be repeated.

The last step is to tell the event which phase to start with. This is done by calling transitionKey within the constructor.

public class ExampleEvent extends PhasedEvent {
    public static final String ID = makeID("ExampleEvent");
    private static final EventStrings eventStrings = CardCrawlGame.languagePack.getEventString(ID);
    private static final String NAME = eventStrings.NAME;
    private static final String[] DESCRIPTIONS = eventStrings.DESCRIPTIONS;
    private static final String[] OPTIONS = eventStrings.OPTIONS;
    private static final String IMG = "images/events/weMeetAgain.jpg";

    public ExampleEvent() {
        super(ID, NAME, IMG);

        registerPhase("start", new TextPhase(DESCRIPTIONS[0])
                .addOption(new TextPhase.OptionInfo(OPTIONS[0]).enabledCondition(() -> AbstractDungeon.player.gold >= 70, OPTIONS[1])
                        .setOptionResult((i)->{
                            AbstractRelic relic = AbstractDungeon.returnRandomScreenlessRelic(AbstractDungeon.returnRandomRelicTier());
                            AbstractDungeon.player.loseGold(70);
                            AbstractDungeon.getCurrRoom().spawnRelicAndObtain(this.drawX, this.drawY, relic);
                            AbstractEvent.logMetricObtainRelicAtCost(ID, "Obtained Relic", relic, 70); //Optional, adds information to run history
                            transitionKey("they took the relic");
                        }))
                .addOption(OPTIONS[2], (i)->transitionKey("they didn't take the relic")));

        registerPhase("they took the relic", new TextPhase(DESCRIPTIONS[1]).addOption(OPTIONS[3], (i)->openMap()));
        registerPhase("they didn't take the relic", new TextPhase(DESCRIPTIONS[2]).addOption(OPTIONS[3], (i)->openMap()));

        transitionKey("start");
    }
}

With this, the event's code is complete. All that's left is defining the event's text in the json file, and then registering the event.

Event Text

The event's text goes in the EventStrings.json file, in resources/yourmod/localization. First, make a copy of the example (or you can set up a second entry yourself) and change the ID (the first part, ${modID}:EventID) to match your event.

makeID("ExampleEvent") corresponds to ${modID}:ExampleEvent.

image

The event's name will be "Rude Merchant". The name is displayed at the top of an event.

For DESCRIPTIONS and OPTIONS, make sure you line them up correctly with what you used in the code. For this example event, there are 3 DESCRIPTIONS entries and 4 OPTIONS entries.

  "${modID}:ExampleEvent": {
    "NAME": "Rude Merchant",
    "DESCRIPTIONS": [
      "Do you want to buy a relic?",
      "Here's your relic. Now go away.",
      "Well, why are you here then?"
    ],
    "OPTIONS": [
      "[Buy] #y70 #yGold: #gObtain #ga #gRelic.",
      "[Locked] Requires: 70 Gold.",
      "[Decline]",
      "[Leave]"
    ]
  }

With the text defined, the event is complete.

Registering the Event

Events do not have their own dedicated hook for registration. Instead, they are just registered in the receivePostInitialize method, which should already exist in your main mod file. For a generic event, it's as simple as calling BaseMod.addEvent(ExampleEvent.ID, ExampleEvent.class);.

image

You can add conditions for the event to spawn during registration. See the BaseMod wiki for details.

The complexity of events can be much higher than this depending on what your goal is. You can see another more complex example here.

Clone this wiki locally