GegoXaren

another_chance_ic_patch.rpy

Aug 17th, 2025
57
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.93 KB | None | 0 0
  1. # Unofficial patch for the Game "Another Chance" by Time Wizard Studios
  2. #
  3. # For the avoidance of doubt: you, the reader, are hereby authorized to use any
  4. # part of this patch in any way you choose, without limitation.
  5. #
  6. # This explicitly includes Time Wizard Studios, who may take any segments of
  7. # this patch, up to and including the whole, and integrate them into the base
  8. # game in any way they see fit, without credit or acknowledgement.
  9. #
  10. # (Mostly they probably shouldn't, because Patreon. But some of it isn't what
  11. # Patreon would care about.)
  12.  
  13. # =============================================================================
  14.  
  15. # Changelog:
  16. #
  17. #  Changes made by Gego 2025-08-17:
  18. #    - Fixed B.O.B quest
  19. #    - Fixed "Out on the Waves" quest
  20. #    - Fixed "Medium Thirsty Tree" quest
  21. #
  22. #
  23. #  v3.2:
  24. #    - Fixed the accidental removal of everyone _but_ Jo's scenes from the
  25. #      Gallery, introduced in 3.1.
  26. #
  27. #  v3.1:
  28. #    - Rewrote the larger part of the patch, based in part on a suggestion by
  29. #      ZLZK (of F95).
  30. #
  31. #    - Removed all use of the name 'Jo' (for now?), which could be confusing
  32. #      if you started with the patch applied:
  33. #      - Removed use of "Jo" by some characters which previous patch-versions
  34. #        explicitly retained.
  35. #      - Adjusted various fragments of text in the quest canonically titled
  36. #        "Jo's Day", with added editorial meddling.
  37. #      - Amended the scene-titles in the Gallery.
  38. #
  39. #    - Converted the fix for the obscure `maxine_eggs` comment to use the
  40. #      Ren'Py translation mechanism. Lightly tweaked the text for consistency.
  41. #
  42. #    - Used the translation mechanism to alter a few other strings.
  43. #
  44. #  v2: Fixed a bug which would crash the game whenever "???" spoke.
  45. #
  46. #  v1: Original version.
  47.  
  48. # =============================================================================
  49.  
  50. # Another Chance (henceforth usually "AC") v1.36 has some code that seems
  51. # to be part of a mechanism whereby the substituted name of a character can
  52. # be made different depending on who's talking about them.
  53. #
  54. # That would be particularly useful for us, if only it were functional.
  55. # Unfortunately, it's not: none of it's hooked up to anything, nor is there
  56. # anything shaped like its complement to hook it up to. It can't really be
  57. # made to work because Ren'Py 7.4.4 (which AC uses) falls _just_ short of
  58. # being able to hook into the necessary code.
  59. #
  60. # Fortunately, we can mostly work around that. Ren'Py doesn't quite provide
  61. # us with the data we need, but this _is_ Python, so we can monkey-patch in
  62. # what we want.
  63.  
  64. # =============================================================================
  65.  
  66. init 1 python hide:
  67.     # Virtually all uses of "Jo" or "[jo]" are in contexts associated with
  68.     # either the main character or Flora. (They're not all speech; many,
  69.     # perhaps most, are narration -- but fortunately, as in most VNs, the
  70.     # narrator is the MC.) Thus, the following line alone would provide 99.9%
  71.     # of the functionality of this patch.
  72.    
  73.     Character_jo.default_name = u"Mom"
  74.    
  75.     # As always, the remaining 0.1% requires 99.9% of the work.
  76.     #
  77.     # As of AC v1.36, there are only a handful of places in the entire game
  78.     # where another character (or non-character UI text) names Jo directly.
  79.     #
  80.     # First, there are some UI-only hardcoded uses of "Jo". The hardcoded
  81.     # scene-replay title text is perhaps easiest to fix:
  82.  
  83.     def fixup_replays():
  84.         import re
  85.         rx = re.compile(r"^Jo's ")
  86.  
  87.         global replays_by_order
  88.         replays_by_order = [
  89.             (a, rx.sub("Mom's ", title), c, d)
  90.             for (a, title, c, d) in replays_by_order
  91.         ]
  92.     fixup_replays()
  93.  
  94.     # Also, there's one quest whose title contains it:
  95.     Quest_jo_day.title = "Mom's Day"
  96.     # ... along with some hint-adjacent flavor text in that quest; but that can
  97.     # more easily be taken care of with the translation mechanism. (We do that
  98.     # below.)
  99.  
  100.     # =========================================================================
  101.  
  102.     # AC's BaseChar class sets an attribute `game._speaker` to the current
  103.     # speaker of a Say command. We can't quite use that, for reasons that will
  104.     # be explained along the way... but we can set up our own mechanism in
  105.     # parallel instead.
  106.  
  107.     # Specifically, we store the current speaker in the Ren'Py-store variable
  108.     # `_speaker_true`. (We could just use a variable in this hidden scope; but
  109.     # this way we can easily get at the value from temporary diagnostic code
  110.     # elsewhere, if needed.)
  111.  
  112.     def get_current_speaker():
  113.         # Note that, as is customary in Ren'Py, this may either be a character
  114.         # object or a bare string.
  115.         return getattr(renpy.store, "_speaker_true", None)
  116.  
  117.     # Set the current speaker in a particular scope, restoring it on exit.
  118.     #
  119.     # (This is part of the reason we can't use `_speaker`: it never gets unset.
  120.     # If we tried to access it from within a Menu-statement, it would still
  121.     # have its value from the last dialogue-utterance.)
  122.     from contextlib import contextmanager
  123.     @contextmanager
  124.     def speaker_set_to(speaker):
  125.         old_value = get_current_speaker()
  126.         try:
  127.             renpy.store._speaker_true = speaker
  128.             yield None
  129.         finally:
  130.             renpy.store._speaker_true = old_value
  131.  
  132.     # Monkey-patching utility decorator. Replaces the specified method on a
  133.     # class (or function on an object) with a new method provided. The old
  134.     # method will be provided to the new method as a keyword argument. (In
  135.     # theory we could override `super()`, but it's REALLY not worth the
  136.     # trouble.)
  137.     def replace_method(obj, method):
  138.         def do_wrap(decorated_func):
  139.             old_method = getattr(obj, method)
  140.             def replacement_method(*args, **kwargs):
  141.                 return decorated_func(*args, _old_method=old_method, **kwargs)
  142.             replacement_method.__name__ = old_method.__name__
  143.             setattr(obj, method, replacement_method)
  144.             return None
  145.         return do_wrap
  146.  
  147.     # Wrap BaseChar.__call__ so that the speaker is set...
  148.     @replace_method(BaseChar, "__call__")
  149.     def mark_speaker(self, *args, **kwargs):
  150.         _old_method = kwargs.pop("_old_method")
  151.         with speaker_set_to(self):
  152.             return _old_method(self, *args, **kwargs)
  153.  
  154.     # ... and replace Character_jo.__str__ entirely, so that it checks the
  155.     # current speaker to decide what to return.
  156.     @replace_method(Character_jo, "__str__")
  157.     def __str__(self, _old_method=None):
  158.         speaker = get_current_speaker()
  159.  
  160.         # These characters (including None, which represents the MC when
  161.         # speaking in menu text or narration) use the new default name, and
  162.         # need no further processing.
  163.         if speaker in {None, narrator, mc, flora}:
  164.             return self.default_name
  165.  
  166.         # A previous version of this patch returned u"Jo" as some characters'
  167.         # preferred form of reference:
  168.         #
  169.         #    if speaker in {jo, mrsl, nurse, spinach, "???"}:
  170.         #        return u"Jo"
  171.         #
  172.         # Unfortunately, this doesn't quite work: if someone installs this
  173.         # patch before they start, they'll be left wondering who the hell this
  174.         # Jo person is when Mrs. L or the nurse eventually namedrop her.
  175.         # Avoiding this would require explicitly injecting "Jo" somewhere where
  176.         # the player will be guaranteed to see it, and I haven't (yet?) found a
  177.         # non-awkward place to do that.
  178.         #
  179.         # At present, the other characters that refer to "[jo]" only do so when
  180.         # addressing the protagonist, so the default below still works.
  181.  
  182.         # Note that this will fail if "[jo]" ever heads a sentence. No attempt
  183.         # is made to detect that: it must be handled on a case-by-case basis.
  184.         # (TWS could handle it by using "[jo!c]" instead; but there's only one
  185.         # reason to do that, and they probably shouldn't.)
  186.         return u'your mom'
  187.  
  188.     # You'd think that would cover it. Unfortunately, that covers too much.
  189.     #
  190.     # It may seem intuitive that a Say-statement's evaluation would happen as
  191.     # part of ordinary screen layout. This is the opposite of the truth: screen
  192.     # layout is performed ** as part of Say-statement evaluation **.
  193.     #
  194.     # If `str(jo)` is evaluated during screen layout, its current value will
  195.     # depend on which character, if any, happens to be speaking at the time. If
  196.     # the MC is speaking with another character while the result of that
  197.     # evaluation is on-screen, it'll flip back and forth between "Mom" and
  198.     # "your mom" as the characters alternate utterances.
  199.     #
  200.     # This is not hypothetical. In v1.36, it happens in the "phone_app_call"
  201.     # screen when calling Jo. (Coincidentally, this screen is also the other
  202.     # reason we can't use `_speaker`: it's it's accessed during screen layout
  203.     # to position the MC's dialogue, while we need something that's `None` in
  204.     # that same phase!)
  205.     #
  206.     # There isn't any scope that we can reasonably intercept in which
  207.     # dialogue-text-substitution happens but screen-evaluation doesn't: the
  208.     # former happens in `ADVCharacter.__call__`, which also invokes the latter.
  209.     # However, there _is_ an interceptable scope in which screen-evaluation
  210.     # happens, but text-substitution does not.
  211.     #
  212.     # So we just intercept that too. That's probably as sharp as we can get.
  213.  
  214.     @replace_method(ADVCharacter, "do_display")
  215.     def mark_speaker(self, *args, **kwargs):
  216.         _old_method = kwargs.pop("_old_method")
  217.         with speaker_set_to(None):
  218.             return _old_method(self, *args, **kwargs)
  219.  
  220.     # That's not great, because it involves monkey-patching the framework
  221.     # instead of just the game. It should still work up to Ren'Py 8.1.3 (and
  222.     # it's not likely AC will ever update its Ren'Py version anyway), but it
  223.     # still leaves a foul taste in my mouth.
  224.     #
  225.     # If we knew for sure that "phone_app_call" was the only place where that
  226.     # could happen, we could instead just monkey-patch that one screen...
  227.    
  228.     def fixup_phone():
  229.         __phonescreen = renpy.display.screen.get_screen_variant("phone_app_call",None)
  230.         __phonescreen.function.children[1].children[0].positional[0] = \
  231.             u'contact.default_name'
  232.     # fixup_phone()
  233.    
  234.     # ...but this is fragile in the face of minor stylistic changes to the
  235.     # screen(s) in question, won't cover any uses in new screens while the game
  236.     # is in development, and _still_ involves mucking around in Ren'Py
  237.     # internals.
  238.  
  239. # With the generic part out of the way, we turn to local and specific
  240. # adjustments. The Ren'Py translation mechanism turns out to be perfect for
  241. # this, so we define a fake language we're translating into.
  242. define config.language = "ipatch"
  243. # However: beyond this point necessarily lie artistic interpretations and,
  244. # therefore, decisions. Abandon all pretense to objectivity, ye who enter here.
  245.  
  246. translate ipatch quest_flora_bonsai_bed_16ea41b6:
  247.     # flora bed flirty "Being your roommate has taught me everything I need to know about purifying unholy taint."
  248.     flora bed flirty "Being your sister has taught me everything I need to know about purifying unholy taint."
  249.  
  250. translate ipatch quest_flora_squid_trail_flora_e6f80e4d:
  251.     # mc "Maybe it slipped into the principal's office?"
  252.     mc "Maybe it slipped into [jo]'s office?"
  253.  
  254. translate ipatch quest_jo_day_amends_2bdfe697:
  255.     # mc "Because, let's be honest, you're the mother of every student in Newfall High."
  256.     mc "Well, give or take half a year. But better late than never!"
  257.     "Later than you know, [jo]. Later than you know."
  258.  
  259. # This could be more specific; it's been left vague for now to avoid the
  260. # possibility of contradictions with new material.
  261. translate ipatch quest_jo_day_picnic_3e4a8f94:
  262.      # "She kept trying to make out with me and called me by her husband's name..."
  263.      "She kept trying to make out with me and calling me the wrong name..."
  264.  
  265. translate ipatch strings:
  266.     # Guides and flavor text for the quest "jo_day".
  267.     old "Today is the big day. Jo's day!"
  268.     new "Today is the big day. Mom's day!"
  269.  
  270.     old "Tomorrow is the big day. Jo's day!"
  271.     new "Tomorrow is the big day. Mom's day!"
  272.  
  273.     old "Jo's day or my day? Our day!"
  274.     new "Mom's day or my day? Our day!"
  275.    
  276.     # fix B.O.B quest - Gego
  277.     old "Your landlady and roommate will swallow my wood chips!"
  278.     new "Your mother and sister will swallow my wood chips!"
  279.    
  280.     old "I'll spank your roommate with my paddle tail and make her gnaw my log!{space=-85}"
  281.     new "I'll spank your sister with my paddle tail and make her gnaw my log!{space=-85}"
  282.    
  283.     # Fix "Out on the Waves" quest - Gego
  284.     old "Call your annoying roommate."
  285.     new "Call your annoying sister."
  286.    
  287.     # Fix "Medium Thirsty Tree" quest - Gego
  288.     old "Converse with your roommate about, err... roommate stuff."
  289.     new "Converse with your sister about, err... sibling stuff."
  290.  
  291. # This bit is incredibly niche; it's intended to fix a very minor bug
  292. # introduced into the `quest_maxine_eggs_bathroom_encounter` conversation...
  293. # and also to clean up the associated phrasing a little bit, because several of
  294. # the relevant characters probably don't have a locker.
  295. translate ipatch quest_maxine_eggs_bathroom_encounter_fa04a789:
  296.     # lindsey eyeroll "Nope. Have you checked [not_best_girl]'s locker?"
  297.     $ not_best_girl = max([jo, flora, mrsl, isabelle, kate, jacklyn, nurse, maxine], key=lambda c: c.love)
  298.     if not_best_girl is jo:
  299.         $ _fa04a789_storage = "office"
  300.     elif not_best_girl is nurse:
  301.         $ _fa04a789_storage = "office"
  302.         $ not_best_girl = "the nurse"
  303.     elif not_best_girl is mrsl:
  304.         $ _fa04a789_storage = "desk"
  305.     else:
  306.         $ _fa04a789_storage = "locker"
  307.     lindsey eyeroll "Nope. Have you checked [not_best_girl]'s [_fa04a789_storage]?"
  308.  
Advertisement
Add Comment
Please, Sign In to add comment