Основы Ren’Py: система репутации / Хабр

Основы Ren’Py: система репутации / Хабр

In 2024, I embarked on developing an adventure game titled “Azrael, Herald of Death.” As an indie developer, I undertook the writing, game design, and coding myself. Now, I’d like to share my progress so you won’t have to start from scratch. In this note, I’m sharing the reputation system code and a guide on how to use it.


Disrespecting an elder leads to a negative reputation

Disrespecting an elder leads to a negative reputation

Note: I am not a professional programmer; this material is written for beginners in Ren’Py development who are not yet adept at Python to write their own reputation mechanics.

What Does It Offer?

Most visual novels are developed using the Ren’Py engine. It has its own syntax or uses Python for implementation. That’s what I loved about Ren’Py: it seems limited and straightforward, yet it’s a powerful tool due to Python integration.

If you’re new to game engines, it’s essential to recognize that Ren’py is primarily designed for creating visual novels, and writing anything else can be challenging. I chose it because my game initially started as a classical novel. Over time, it evolved to include economics, combat, city-building, puzzles, heroes, collections, battles, inventory, and a reputation system. I’d like to focus on the latter today.

Capabilities

Many visual novels incorporate character reputations. It’s a straightforward mechanic that meshes well with games focused on narrative and choices. My reputation system is user-friendly and provides the ability to:

  • Set a list of entities (characters, clans, factions…) for which reputations are managed;

  • Assign default reputation values for each entity;

  • Establish boundaries (minimum, maximum) for reputation values;

  • Increase or decrease reputation by a specified amount;

  • Add an event trigger to handle reputation changes (e.g., if the reputation with the boss drops below -5, the player gets fired).

Installation

Beneath is the Python code for the reputation class.

reputation.rpy

      # Color list depending on reputation value
      define reputation_colors = {
        -5: "#ff566d",
        -4: "#ff96a4",
        -3: "#ff96a4",
        -2: "#ffc5ce",
        -1: "#ffc5ce",
        0: "#ffffff",
        1: "#c1ffd0",
        2: "#c1ffd0",
        3: "#83ffa2",
        4: "#83ffa2",
        5: "#2fff63"
      }
      # Default color if not specified in the list or an error occurs
      define reputation_default_color = "#ffffff"
      # Maximum and minimum reputation values
      define reputation_minimum = -100
      define reputation_maximum = 100
      # List of entities with whom you can have a reputation
      define reputation_subjects = {
        'character1': 0,
        'character2': 0
      }
      # Names of those with whom you can have a reputation
      define reputation_subject_names = {
        'character1': _("first character"),
        'character2': _("second character")
      }
      # Locales
      define rep_inc_str = _("Reputation with [subject_name] improved by [value].")
      define rep_dec_str = _("Reputation with [subject_name] worsened by [value].")

      init python:
        # Reputation class    
        class Reputation:
          """
          Reputation class with other characters.
          """

          __reputation = {}
          __colors = {}
          __events = []
          notify_on_change = True
          trigger_only_first_event = False

          # Initialize reputation class object
          def __init__(self, default_reputation=None):
            global reputation_colors
            global reputation_default_color
            global reputation_subjects

            if default_reputation is not None:
              self.__reputation = default_reputation
            else:
              self.__reputation = reputation_subjects

            for char, value in self.__reputation.items():
              if value in reputation_colors:
                self.__colors[char] = reputation_colors[value]
              else:
                self.__colors[char] = reputation_default_color

            self.__events = []

          # Retrieve reputation by character designation
          def get(self, subject):
            if subject in self.__reputation:
              return self.__reputation[subject]

            return None

          # Change reputation: who, and by how much
          def change(self, subject, delta, notify=None, mark_met=True):
            global reputation_colors
            global reputation_minimum
            global reputation_maximum

            if subject in self.__reputation:
              self.__reputation[subject] += delta
              if reputation_maximum is not None:
                self.__reputation[subject] = min(self.__reputation[subject], reputation_maximum)
              if reputation_minimum is not None:
                self.__reputation[subject] = max(self.__reputation[subject], reputation_minimum)
              self.__colors[subject] = reputation_colors[self.__reputation[subject]]

              if notify or (self.notify_on_change and notify is not False):
                global rep_inc_str
                global rep_dec_str
                global reputation_subject_names
                if delta >= 0:
                  notify_str = _(rep_inc_str).replace("[subject_name]", _(reputation_subject_names[subject])).replace("[value]", str(delta))
                else:
                  notify_str = _(rep_dec_str).replace("[subject_name]", _(reputation_subject_names[subject])).replace("[value]", str(-delta))
                renpy.notify(notify_str)

              if mark_met and ('char_meet' in globals()):
                globals()['char_meet'][subject] = True

              self._trigger_events(subject, delta=delta)

              return True

            return False

          # Increment reputation by 1
          def inc(self, subject):
            return self.change(subject, 1)

          # Decrease reputation by 1
          def dec(self, subject):
            return self.change(subject, -1)

          # Current reputation color
          @property
          def colors(self):
            return self.__colors

          # Current reputation values
          @property
          def values(self):
            return self.__reputation

          # Add event to handler
          def register_event(self, subject: str, value: int, _label="", _screen="", _function=None, compare_method='=', repeat=False, **kwargs):
            """
            Registers a reputation change event.

            :param str subject: The character whose reputation we are tracking
            :param int value: The reputation value at which to trigger the event
            :param str _label: Label to call when the event occurs
            :param str compare_method: "=" for exact equality of reputation with character subject and value
            :param bool repeat: Whether to trigger the event each time the reputation changes or only once
            :return: True, if the event was added to the handler
            :rtype: bool
            """
            new_event = {}
            new_event['subject'] = subject
            new_event['value'] = value
            new_event['label'] = _label
            new_event['screen'] = _screen
            new_event['function'] = _function
            new_event['compare_method'] = compare_method
            new_event['repeat'] = repeat
            new_event['count_triggered'] = 0
            new_event['kwargs'] = {}
            for k, v in kwargs.items():
              new_event['kwargs'][k] = v
            self.__events.append(new_event)
            return True

          # Check all possible event triggers upon reputation change
          def _trigger_events(self, subject, delta=None):
            for event in self.__events:
              if (event['subject'] == subject or event['subject'] == "" or event['subject'] is None) and (event['repeat'] or event['count_triggered'] == 0):
                if ((event['compare_method'] == "=" or event['compare_method'] == "==") and (self.__reputation[subject] == event['value'])) \
                    or ((event['compare_method'] == ">") and (self.__reputation[subject] > event['value'])) \
                    or ((event['compare_method'] == "<") and (self.__reputation[subject] < event['value'])) \
                    or ((event['compare_method'] == ">=") and (self.__reputation[subject] >= event['value'])) \
                    or ((event['compare_method'] == "<=") and (self.__reputation[subject] <= event['value'])) \
                    or ((event['compare_method'] == "!=") and (self.__reputation[subject] != event['value'])):
                  event['count_triggered'] += 1
                  globals()['reputation_subject'], globals()['reputation_value'], globals()['reputation_delta'] = subject, self.__reputation[subject], delta
                  if callable(event['function']):
                    event['function'](**event['kwargs'])
                  if event['screen']:
                    renpy.show_screen(event['screen'], **event['kwargs'])
                  if event['label']:
                    renpy.call(event['label'], from_current=False, **event['kwargs'])
                  if self.trigger_only_first_event:
                    return event['count_triggered']
            return False
      

To incorporate it into your Ren'Py project, simply download the reputation.rpy file and place it in any directory within your project. I personally stored it in the game's root directory.

Initialization

Create an instance of the reputation class in your script. It's preferable to perform this task before the actual game code. For instance, I created a file named init_game.rpy for such cases, executed first following the start label. Nevertheless, for the reputation system, initializing a Reputation class object in the init python block or directly declaring the appropriate variable and feeding it a dictionary in the format: reputation subject - starting value, will suffice.


  # Creating a reputation object
  # Defining a list of those with whom reputations will change along with initial values 
  define default_reputation = {
    'anton': -1,
    'stella': 1,
    'roman': 0
  }
  # Creating the reputation variable - a Reputation class instance
  # All reputation system operations will proceed through this variable
  default reputation = Reputation(default_reputation)
  

Callable Methods

values - outputs a dictionary containing the current reputation values with all subjects. It can, for instance, verify current reputation with a character and provide a different response. Besides the full values list, one may ask for a single character's reputation using the get(subject) method.


  # Here, get('anton') and values['anton'] are equivalent
  if reputation.get('anton') > 10:
    anton "You're like a brother to me! Come, give me a hug."
  elif reputation.values['anton'] < 0:
    anton "Sorry, but we don't know each other that well."
  

colors - a dictionary of colors associated with the current reputation value. In my game, I use various colors to display reputation values in the interface. This helps players grasp the quality of their relationship with each character. To leverage this feature, assign a color for each reputation value in the reputation_colors constant. If this feature is unnecessary, simply ignore it; the system will function regardless of the color dictionary being filled.


  text "[charname]: [reputation.value[key]]" color reputation.colors[key]
  

Screenshot from "Azrael, Herald of Death" showing colored reputation values

Screenshot from "Azrael, Herald of Death" showing colored reputation values

change(subject, value, notify=None, mark_met=True) - modifies the current reputation with character subject by the specified value. It can notify the player of this change and mark characters as "met".


  $ reputation.change('roman', 2)
  roman "Oh, you're stunning today! I respect that."
  

The reputation object possesses a notify_on_change property, defaulting to True. If enabled, upon reputation change, it triggers Ren'Py's standard notification to inform the player about this alteration.


Notification bar near the character reputation panel indicating reputation change

Notification bar near the character reputation panel indicating reputation change

To suppress notifications, set the notify_on_change parameter to False.


  reputation.notify_on_change = False
  

If the notify flag is specified within the change method, it supersedes the notify_on_change property for the current command. This is useful if changes are generally auto-notified, but specific instances require manual visibility or none at all.


  # One action caused reputation changes with two characters
  # Convenient to unify this in a single notification instead of sequential ones
  reputation.change('anton', -2, False)
  reputation.change('stella', 1, False)
  renpy.notify(_("Reputation improved with Stella by 1. Reputation decreased with Anton by 2."))
  

Phases for altering reputation are outlined in the rep_inc_str and rep_dec_str constants. Ren'Py autonomously generates localization if instructed. Names of those tracked are stored in the reputation_subject_names dictionary.


  # Names of those with whom reputation can be established
  define reputation_subject_names = {
    'anton': _("Anton"),
    'stella': _("Stella"),
    'roman': _("Roman")
  }
  

The change method can also take a mark_met flag defaulting to True. In Azrael, character reputation showcased only after the player initially encountered them. By default, the reputation class assumes that a reputation change command indicates meeting the character and marks them as "met". For this feature, declare a char_meet variable listing all reputation subjects. Ignoring this feature will not cause errors.


  default char_meet = {
    'anton': False,
    'stella': False,
    'roman': False
  }
  

inc(subject) - enhances reputation with subject by 1 and notifies the player if enabled.


  # Below commands are equivalent
  reputation.change('anton', 1)
  reputation.inc('anton')
  

dec(subject) - reduces reputation with subject by 1, analogous to how inc increases it.

register_event(subject, value, _label="", _screen="", _function=None, compare_method='=', repeat=False) - instructs the reputation system to undergo a specific action upon reputation alteration. For example, in Azrael, if the reputation with Ozymandias drops to -5, the player is defeated and exiled for 100 years. This mechanism grants rewards for reputation, allows for victories, or envision wild scenarios. The register_event method accepts these parameters:

  • subject - monitor reputation change with this entity;

  • value - value for reputation comparison upon alteration;

  • _label - the label that the call method invokes when the event's condition is met;

  • _screen - screen displayed by the show_screen method upon event condition fulfillment;

  • _function - any function (or callable object) invoked when the event triggers;

  • compare_method - method for comparing the character's current reputation to the value. Options: =, >, <, >=, <=, !=. Default is reputation equaling value;

  • repeat - if False, the event triggers once; if True, it triggers every time reputation with the given character changes, and conditions are met. Default is False.


Ozymandias is displeased with Azrael's actions, and his hologram interrupts gameplay in reaction to reputation change

Ozymandias is displeased with Azrael's actions, and his hologram interrupts gameplay in reaction to reputation change

Example usage:


  # Register defeat event when reputation with Roman drops
  reputation.register_event('roman', -10, _label="defeat")
  

  label defeat:
    "Anton delivers an epic punch to your jaw. Darkness envelops."
    "Defeat."
    $ MainMenu(confirm=False)()
  

Use None or an empty string ("") for the subject to trigger an event whenever any character's reputation changes. Example:


  # Trigger info_screen upon any reputation change with any character
  reputation.register_event('', -10, compare_method=">", _screen="info_screen", repeat=True)
  

The register_event method allows for any number of named parameters. Upon the occurrence of the designated event, these parameters are forwarded to the called label, function, or shown screen. For instance, this code calls a standard Yes/No dialog upon Stella's reputation dropping to -5 or below.


  reputation.register_event('stella', -5, compare_method="<=", _screen="confirm", repeat=True, \
                          message="Are you sure you want to initiate an apocalypse? Really sure?", \
                          yes_action=Jump("defeat"), no_action=Jump("start"))
  

In Azrael, a similar event occurs if Veles's reputation drops to -5

In Azrael, a similar event occurs if Veles's reputation drops to -5

This example passes three parameters to Ren'Py's standard confirm screen:

  • message - the dialog message;

  • yes_action - the screen action invoked if the player selects "Yes," in this case transitioning to the defeat label;

  • no_action - the screen action invoked if the player selects "No," transitioning to the start label.

Tip: When handling screens, remember to use Hide() to conceal windows after performing actions. This can be done by defining the event as a list: yes_action = [Hide(), your_action()].

Using register_event, you can, for instance, make a notification pop-up with every reputation change indicating with whom and by how much reputation altered, instead of the default Notify. The reputation class records the following parameters in globals:

  • reputation_subject - the subject whose reputation changed;

  • reputation_value - the current reputation value with reputation_subject;

  • reputation_delta - the change in reputation with reputation_subject.

These can be accessed from the called screen or label utilizing the globals() function:


  globals()['reputation_subject']
  

Do note that reputation_delta records the commanded amount rather than the actual reputation change. Why might they differ? While one might command a 2-point reputation increase expecting game execution, the reputation system enforces maximum and minimum values. They are specified by:


  # Max and min reputation values
  define reputation_minimum = -5
  define reputation_maximum = 5
  

If undesired, set these constants to None.

Here's an example syntax for invoking a function:


  reputation.register_event('stella', 5, _function=renpy.notify, message="Stella approves.")
  

While assigning a function to the _function parameter, ensure to only specify the function's name without parentheses or arguments. All arguments are specified separately, as above. Writing _function=renpy.notify() would assign the result of renpy.notify execution to _function, which, rather than being a function, won't be executed upon event trigger.

Note any potential conflicts when employing the reputation system's event triggering on change. If simultaneously registered events could potentially execute, the system processes them in registration order.

What consequences might arise? If two events are set to invoke two labels, the game will only execute one. Should this label result in a return, the second may proceed. However, under standard circumstances, the latter won't execute.

For multiple screens, a conflict is absent unless the screen responds to additional actions. For example, showcasing a tooltip that vanishes upon user action. Event or screen invocation may suppress exposure to this tooltip. Be mindful of event processing order, especially if some prompt labels while others present screens.

Conversely, a singular event triggering a screen, label, and function will first execute the function, then show the screen, and finally, invoke the label.


I'm thrilled if this reputation system benefits your Ren'Py game development. If you're interested in more game mechanics written in Python for Ren'Py, please like and bookmark this article.

The game for which I wrote this code will release this year. Meanwhile, consider adding it to your Steam wishlist to catch open testing and the free demo.

Beyond programming, my expertise is in game design, production, and narrative writing, crafting games for 17 years and teaching for 10 at HSE University's Business School in:

I educate on systems game design, narrative design, programming fundamentals, and elaborate on game development investments, production, and studio management.

Source