diff --git a/Monika After Story/game/definitions.rpy b/Monika After Story/game/definitions.rpy index 6152d6258a..7e00823bb6 100644 --- a/Monika After Story/game/definitions.rpy +++ b/Monika After Story/game/definitions.rpy @@ -147,7 +147,34 @@ python early: EV_ACT_POOL ] - + #### bitmask flags + # all bitmask flags apply until next restart or the flag is unset. + # NOTE: do NOT add a bitmask flag if you want to save its value. + # if you need saved data, add a new prop or use an existing one. + + EV_FLAG_HFM = 2 + # Hidden From Menus + # this flag marks an event as temporarily hidden from all menus + + EV_FLAG_HFRS = 4 + # Hidden From Random Selection + # this flag marks an event as temporarily hidden from all random-based + # selection + # Random-based selection consists of: + # - startup greetings + # - randomly selected farewells + # - random topics + + EV_FLAG_HFNAS = EV_FLAG_HFM | EV_FLAG_HFRS + # Hidden from Non-Active Selection + # combines hidden from menu and random select + + # TODO: when needed: + # Hidden From Check Events - ignored in Event.checkEvents + # NOTE: this is potentially dangerous, so maybe we dont need + # Hidden From Active Selection - like blacklisting queue/push actions + + #### End bitmask flags # custom event exceptions class EventException(Exception): @@ -236,7 +263,7 @@ python early: # show_in_idle - True if this Event can be shown during idle # False if not # (Default: False) - # flags - bitmask system that acts as unchanging flags. + # flags - bitmask system that acts as unchanging or temporary flags. # (Default: 0) class Event(object): @@ -273,6 +300,22 @@ python early: "flags" ) + # filterables + FLT = ( + "category", # 0 + "unlocked", # 1 + "random", # 2 + "pool", # 3 + "action", # 4 + "seen", # 5 + "excl_cat", # 6 + "moni_wants", # 7 + "sensitive", # 8 + "aff", # 9 + "flag_req", # 10 + "flag_ban", # 11 + ) + # other constants DIARY_LIMIT = 500 @@ -572,6 +615,27 @@ python early: and "monika wants this first" in self.rules ) + def allflags(self, flags): + """ + Checks if this event has ALL flags from flags + + IN: + flags - flags to check + + RETURNS: True if all flags from flags is in this event's flags + """ + return (flags & ~self.flags) == 0 + + def anyflags(self, flags): + """ + Checks if this event has ANY flag from flags + + IN: + flags - flags to check + + RETURNS: True if any flag from flags is in this event's flag + """ + return (self.flags & flags) != 0 def checkAffection(self, aff_level): """ @@ -591,7 +655,6 @@ python early: low, high = self.aff_range return store.mas_affection._betweenAff(low, aff_level, high) - def canRepeat(self): """ Checks if this event has the vars to enable repeat @@ -604,6 +667,14 @@ python early: and self.years is not None ) + def flag(self, flags): + """ + Adds flags from the given flags to this event's flags + + IN: + flags - flags to add to this event + """ + self.flags |= flags def prepareRepeat(self, force=False): """ @@ -697,6 +768,15 @@ python early: """ return mas_timePastSince(self.last_seen, time_passed, _now) + def unflag(self, flags): + """ + Removes given flags from this event's flags + + IN: + flags - flags to remove from this event + """ + self.flags &= ~flags + @staticmethod def getSortPrompt(ev): # @@ -991,31 +1071,42 @@ python early: moni_wants=None, sensitive=None, aff=None, - ): - # - # Filters the given event object accoridng to the given filters - # NOTE: NO SANITY CHECKS - # - # For variable explanations, please see the static method - # filterEvents - # - # RETURNS: - # True if this event passes the filter, False if not + flag_req=None, + flag_ban=None + ): + """ + Filters the given event object accoridng to the given filters + NOTE: NO SANITY CHECKS + + For variable explanations, please see the static method + filterEvents + + RETURNS: + True if this event passes the filter, False if not + """ # collections allow us to match all from collections import Counter + # NOTE: this is done in an order to minimize branching. + # now lets filter if unlocked is not None and event.unlocked != unlocked: return False + if aff is not None and not event.checkAffection(aff): + return False + if random is not None and event.random != random: return False if pool is not None and event.pool != pool: return False - if aff is not None and not event.checkAffection(aff): + if flag_ban is not None and event.anyflags(flag_ban): + return False + + if flag_req is not None and event.allflags(flag_req): return False if seen is not None and renpy.seen_label(event.eventlabel) != seen: @@ -1055,108 +1146,101 @@ python early: return True @staticmethod - def filterEvents( - events, -# full_copy=False, - category=None, - unlocked=None, - random=None, - pool=None, - action=None, - seen=None, - excl_cat=None, - moni_wants=None, - sensitive=None, - aff=None - ): - # - # Filters the given events dict according to the given filters. - # HOW TO USE: Use ** to pass in a dict of filters. they must match - # the names we use here. - # - # IN: - # events - the dict of events we want to filter - # full_copy - True means we create a new dict with deepcopies of - # the events. False will only copy references - # (Default: False) - # DEPRECATEDE - # - # FILTERING RULES: (recommend to use **kwargs) - # NOTE: None means we ignore that filtering rule - # category - Tuple of the following format: - # [0]: True means we use OR logic. False means AND logic. - # [1]: Tuple/list of strings that to match category. - # (Default: None) - # NOTE: If either element is None, we ignore this filteirng - # rule. - # unlocked - boolean value to match unlocked attribute. - # (Default: None) - # random - boolean value to match random attribute - # (Default: None) - # pool - boolean value to match pool attribute - # (Default: None) - # action - Tuple/list of strings/EV_ACTIONS to match action - # NOTE: OR logic is applied - # (Default: None) - # seen - boolean value to match renpy.seen_label - # (True means include seen, False means dont include seen) - # (Default: None) - # excl_cat - list of categories to exclude, if given an empty - # list it filters out events that have a non-None category - # (Default: None) - # moni_wants - boolean value to match if the event has the monika - # wants this first. - # (Default: None ) - # sensitive - boolean value to match if the event is sensitive - # or not - # NOTE: if None, we use inverse of _mas_sensitive_mode, only - # if sensitive mode is True. - # AKA: we only filter sensitve topics if sensitve mode is - # enabled. - # (Default: None) - # aff - affection level to match aff_range - # (Default: None) - # - # RETURNS: - # if full_copy is True, we return a completely separate copy of - # Events (in a new dict) with the given filters applied - # If full_copy is False, we return a copy of references of the - # Events (in a new dict) with the given filters applied - # if the given events is None, empty, or no filters are given, - # events is returned + def filterEvents(events, **flt_args): + """ + Filters the given events dict according to the given filters. + HOW TO USE: Use ** to pass in a dict of filters. they must match + the names we use here. + IN: + events - the dict of events we want to filter + **flt_args - see FILTERING RULES below for name=value rules + + FILTERING RULES: (recommend to use **kwargs) + NOTE: None means we ignore that filtering rule + category - Tuple of the following format: + [0]: True means we use OR logic. False means AND logic. + [1]: Tuple/list of strings that to match category. + (Default: None) + NOTE: If either element is None, we ignore this + filtering rule. + unlocked - boolean value to match unlocked attribute. + (Default: None) + random - boolean value to match random attribute + (Default: None) + pool - boolean value to match pool attribute + (Default: None) + action - Tuple/list of strings/EV_ACTIONS to match action + NOTE: OR logic is applied + (Default: None) + seen - boolean value to match renpy.seen_label + (True means include seen, False means dont include seen) + (Default: None) + excl_cat - list of categories to exclude, if given an empty + list it filters out events that have a non-None category + (Default: None) + moni_wants - boolean value to match if the event has the monika + wants this first. + (Default: None ) + sensitive - boolean value to match if the event is sensitive + or not + NOTE: if None, we use inverse of _mas_sensitive_mode, only + if sensitive mode is True. + AKA: we only filter sensitve topics if sensitve mode is + enabled. + (Default: None) + aff - affection level to match aff_range + (Default: None) + flag_req - flags that the event must match + (Default: None) + flag_ban - flags that the event must NOT have + (Default: None) + + RETURNS: copy of references of the Events in a new dict with + the given filters applied. + if the given events is None, empty, or no filters are given, + events is returned + """ # sanity check - if (not events or len(events) == 0 or ( - category is None - and unlocked is None - and random is None - and pool is None - and action is None - and seen is None - and excl_cat is None - and moni_wants is None - and sensitive is None - and aff is None)): + if ( + not events + or len(events) == 0 + or store.mas_utils.all_none(data=flt_args) + ): return events # copy check # if full_copy: # from copy import deepcopy - # setting up rules - if (category and ( - len(category) < 2 - or category[0] is None - or category[1] is None - or len(category[1]) == 0)): - category = None + # setup keys + cat_key = Event.FLT[0] + act_key = Event.FLT[4] + sns_key = Event.FLT[8] + + # validate filter rules + category = flt_args.get(cat_key) + if ( + category + and ( + len(category) < 2 + or category[0] is None + or category[1] is None + or len(category[1]) == 0 + ) + ): + flt_args[cat_key] = None + + action = flt_args.get(act_key) if action and len(action) == 0: - action = None + flt_args[act_key] = None + + sensitive = flt_args.get(sns_key) if sensitive is None: try: # i have no idea if this is reachable from here if persistent._mas_sensitive_mode: - sensitive = False + flt_args[sns_key] = False except: pass @@ -1165,11 +1249,7 @@ python early: # python 2 for k,v in events.iteritems(): # time to apply filtering rules - if Event._filterEvent(v,category=category, unlocked=unlocked, - random=random, pool=pool, action=action, seen=seen, - excl_cat=excl_cat,moni_wants=moni_wants, - sensitive=sensitive, aff=aff): - + if Event._filterEvent(v, **flt_args): filt_ev_dict[k] = v return filt_ev_dict @@ -3384,6 +3464,7 @@ init -991 python in mas_utils: import codecs import platform import time + import traceback #import tempfile from os.path import expanduser from renpy.log import LogFile @@ -3400,6 +3481,32 @@ init -991 python in mas_utils: "[": "[[" } + def all_none(data=None, lata=None): + """ + Checks if a dict and/or list is all None + + IN: + data - Dict of data. values are checked for None-ness + (Default: None) + lata - List of data. values are checked for None-ness + (Default: None) + + RETURNS: True if all data is None, False otherwise + """ + # check dicts + if data is not None: + for value in data.itervalues(): + if value is not None: + return False + + # now lists + if lata is not None: + for value in lata: + if value is not None: + return False + + return True + def clean_gui_text(text): """ @@ -3544,6 +3651,13 @@ init -991 python in mas_utils: mas_log.write(msg) + def writestack(): + """ + Prints current stack to log + """ + writelog("".join(traceback.format_stack())) + + def trydel(f_path, log=False): """ Attempts to delete something at the given path @@ -3891,6 +4005,8 @@ init -100 python in mas_utils: import math from cStringIO import StringIO as fastIO + __secInDay = 24 * 60 * 60 + __FLIMIT = 1000000 def tryparsefloat(value, default=0): @@ -4035,6 +4151,113 @@ init -100 python in mas_utils: ) + def secInDay(): + """ + RETURNS: number of seconds in a day + """ + return __secInDay + + + def time2sec(_time): + """ + Converts a time value to seconds + + IN: + time - datetime.time object to convert + + RETURNS: number of seconds + """ + return (_time.hour * 3600) + (_time.minute * 60) + _time.second + + + def fli_indk(lst, d): + """ + Find + List + Item + IN + Dictionary + Keys + + Finds index of an item in the list if it is a key in the given dict. + + IN: + lst - list to cehck + d - dictionary to check + + RETURNS: The index of the first item in the list that is a key in the + dict. There are no checks of if the item can be a valid key. + -1 is returned if no item in the list is a key in the dict. + """ + for idx, item in enumerate(lst): + if item in d: + return idx + + return -1 + + + def insert_sort(sort_list, item, key): + """ + Performs a round of insertion sort. + This does least to greatest sorting + + IN: + sort_list - list to insert + sort + item - item to sort and insert + key - function to call using the given item to retrieve sort key + + OUT: + sort_list - list with 1 additonal element, sorted + """ + index = len(sort_list) - 1 + while index >= 0 and key(sort_list[index]) > key(item): + index -= 1 + + sort_list.insert(index + 1, item) + + + def insert_sort_compare(sort_list, item, cmp_func): + """ + Performs a round of insertion sort using comparison function + + IN: + sort_list - list to insert + sort + item - item to sort and insert + cmp_func - function to compare items with. + first arg will be item in the list + second arg will always be the item being inserted + This should return True if the item is not in the correct place + False when the item is in the correct place + + OUT: + sort_list - list with 1 additional element, sorted + """ + index = len(sort_list) - 1 + while index >= 0 and cmp_func(sort_list[index], item): + index -= 1 + + sort_list.insert(index + 1, item) + + + def insert_sort_keyless(sort_list, item): + """ + Performs a round of insertion sort for natural comparison objects. + This does least to greatest sorting. + + IN: + sort_list - list to insert + sort + item - item to sort and insert + + OUT: + sort_list - list with 1 additional element, sorted + """ + index = len(sort_list) - 1 + while index >= 0 and sort_list[index] > item: + index -= 1 + + sort_list.insert(index + 1, item) + + def normalize_points(points, offsets, add=True): """ normalizes a list of points using the given offsets @@ -4065,6 +4288,172 @@ init -100 python in mas_utils: return normal_pts + def nz_count(value_list): + """ + NonZero Count + + Counts all non-zero values in the given list + + IN: + value_list - list to count nonzero values for + + RETURNS: number of nonzero values in list + """ + count = 0 + for value in value_list: + count += int(value != 0) + + return count + + + def ev_distribute(value_list, amt, nz=False): + """ + EVen Distribute + + Evenly distributes the given value to a given value list. + NOTE: only for ints + + IN: + value_list - list of numbers to distribute to + amt - amount to evenly distribute + nz - True will make distribution only apply to non-zero values, + False will distribute to all + (Default: False) + + OUT: + value_list - even distribution amount added to each appropriate + item in this list + + RETURNS: leftover amount + """ + # determine effective size + size = len(value_list) + if nz: + size -= nz_count(value_list) + + # deteremine distribution amount + d_amt = amt / size + + # now distribute + for index in range(len(value_list)): + if not nz or value_list[index] > 0: + value_list[index] += d_amt + + # leftovers + return amt % size + + + def fz_distribute(value_list): + """ + Flipped Zero Distribute + + Redistributes values in the given list such that: + 1. any index with a value larger than 0 is set to 0 + 2. any index with a value of 0 now has a nonzero value + 3. the nonzero is evenly divided among the appropriate indexes + + IN: + value_list - list of numbers to flip zero distribute + + OUT: + value_list - flip-zero distributed list of numbers + + RETURNS: any leftover amount + """ + # determine amt to distribute + amt = sum(value_list) + + # dont do anything if nothing to distribute + if amt < 1: + return 0 + + # determine distribution amount + size = len(value_list) - nz_count(value_list) + d_amt = amt / size + + # now apply the amount to zero and clear non-zero values + for index in range(len(value_list)): + if value_list[index] > 0: + value_list[index] = 0 + else: + value_list[index] = d_amt + + # and return leftovers + return amt % size + + + def ip_distribute(value_list, amt_list): + """ + In Place Distribute + + Distributes values from one list to the other list, based on index. + Mismatched list sizes are allowed. There is no concept of leftovers + here. + + IN: + value_list - list of numbers to distribute to + amt_list - list of amounts to distribute + + OUT: + value_list - each corresponding index in amt_list added to + corresponding index in value_list + """ + vindex = 0 + amtindex = 0 + while vindex < len(value_list) and amtindex < len(amt_list): + value_list[vindex] += amt_list[amtindex] + + + def lo_distribute(value_list, leftovers, reverse=False, nz=False): + """ + LeftOver Distribute + Applies leftovers to the given value list. + + If leftovers are larger than the value list, we do ev_distribute first + + IN: + value_list - list of numbers to distribute to + leftovers - amount of leftover to distribute + reverse - True will add in reverse order, false will not + (Default: False) + nz - True will only apply leftovers to non-zero values + False will not + (Default: False) + + OUT: + value_list - some items will have leftovers added to them + """ + # determine effective size + if nz: + size = nz_count(value_list) + else: + size = len(value_list) + + # apply ev distribute if leftovesr is too large + if leftovers >= size: + leftovers = ev_distribute(value_list, leftovers, nz=nz) + + # dont add leftovers if none leftover + if leftovers < 1: + return + + # determine direction + if reverse: + indexes = range(len(value_list)-1, -1, -1) + else: + indexes = range(len(value_list)) + + # apply leftovers + index = 0 + while leftovers > 0 and index < len(indexes): + real_index = indexes[index] + if not nz or value_list[real_index] > 0: + value_list[real_index] += 1 + leftovers -= 1 + + index += 1 + + def _EVgenY(_start, _end, current, for_start): """ Generates/decides if a given start/end datetime/date should have its diff --git a/Monika After Story/game/dev/dev_backgrounds.rpy b/Monika After Story/game/dev/dev_backgrounds.rpy new file mode 100644 index 0000000000..68a5c20a6e --- /dev/null +++ b/Monika After Story/game/dev/dev_backgrounds.rpy @@ -0,0 +1,397 @@ +# test module for backgrounds + +init 100 python: + + def mas_build_mbgfm( + mn_sr_size, + mn_sr_d, + sr_ss_size, + sr_ss_d, + ss_mn_size, + ss_mn_d, + ml_min, + ml_max, + pr_min, + pr_max, + mx_min, + mx_max + ): + """ + Generates a MASBackgroundFilterManager using sample number of slice + sizes. + + NOTE: verify is NOT called + + IN: + mn_sr_size - how many slices to use for the mn_sr chunk + mn_sr_d - passed to the is_day param + sr_ss_size - how many slices to use for the sr_ss chunk + sr_ss_d - passed to the is_day param + ss_mn_size - how many slices to use for the ss_mn chunk + ss_mn_d - passed to the is_day param + ml_min - minimum minlength time to use in seconds + NOTE: if larger than ml_max, ml_max takes precedence + ml_max - max minlength time to use in seconds + pr_min - min priority to use (must be 1-10) + NOTE: if larger than pr_max, pr_max takes precedence + pr_max - max priority to use (must be 1-10) + mx_min - minimum maxlength time to use in seconds + NOTE: if larger than mx_max, mx_max takes precdence + mx_max - minimum maxlength time to use in seconds + + RETURNS: MASBackgroundFilterManager object with the given settings + """ + # validate input + if mn_sr_size < 1: + mn_sr_size = 1 + if sr_ss_size < 1: + sr_ss_size = 1 + if ss_mn_size < 1: + ss_mn_size = 1 + if ml_min > ml_max: + ml_min = ml_max + if pr_min > pr_max: + pr_min = pr_max + if mx_min > mx_max: + mx_min = mx_max + + # generate slices + mn_sr_slices = _mas_build_fake_slices( + "mn_sr", + mn_sr_size, + ml_min, + ml_max, + pr_min, + pr_max, + mx_min, + mx_max + ) + sr_ss_slices = _mas_build_fake_slices( + "sr_ss", + sr_ss_size, + ml_min, + ml_max, + pr_min, + pr_max, + mx_min, + mx_max + ) + ss_mn_slices = _mas_build_fake_slices( + "ss_mn", + ss_mn_size, + ml_min, + ml_max, + pr_min, + pr_max, + mx_min, + mx_max + ) + + # now build the filter manager + chunks + return MASBackgroundFilterManager( + + # mn_sr + MASBackgroundFilterChunk( + bool(mn_sr_d), + None, + *mn_sr_slices + ), + + # sr_ss + MASBackgroundFilterChunk( + bool(sr_ss_d), + None, + *sr_ss_slices + ), + + # ss_mn + MASBackgroundFilterChunk( + bool(ss_mn_d), + None, + *ss_mn_slices + ) + ) + + + def _mas_build_fake_slices( + flt_pfx, + size, + ml_min, + ml_max, + pr_min, + pr_max, + mx_min, + mx_max + ): + """ + Builds fake slices with the given size + + NOTE: no sanity checks so dont screw up + + IN: + flt_pfx - prefix to use for each slice filter + size - number of slices to make + ml_min - min minlength time to use in seconds + ml_max - max minlength time ot use in seconds + pr_min - min priority to use + pr_max - max priority to use + mx_min - min maxlength time to use in seconds + mx_max - max maxlength time ot use in seconds + + RETURNS: list of created slices. + """ + flt_str = flt_pfx + "_{0}" + + # then the other slices + slices = [ + _mas_build_random_fake_slice( + flt_str.format(index), + ml_min, + ml_max, + pr_min, + pr_max, + mx_min, + mx_max + ) + for index in range(size) + ] + + # pick an unbounded + ub_index = random.randint(0, len(slices)-1) + slices[ub_index].maxlength = None + + return slices + + + def _mas_build_random_fake_slice( + flt, + ml_min, + ml_max, + pr_min, + pr_max, + mx_min, + mx_max + ): + """ + Builds a fake slice with the given filter name and randomized + minlength and pr based on the given values + + IN: + flt - filter name to use + See _mas_build_fake_slices for the other props + + RETURNS: MASBackgroundFilterSlice object + """ + return MASBackgroundFilterSlice( + flt, + random.randint(ml_min, ml_max), + maxlength=random.randint(mx_min, mx_max), + priority=random.randint(pr_min, pr_max), + cache=False + ) + + + def mas_qb_mbgfm(): + return mas_build_mbgfm( + 10, + False, + 20, + True, + 8, + False, + 300, + 60*20, + 1, + 10, + 60*30, + 60*40 + ) + + + def mas_qb_mbgfm_otm(): + return mas_build_mbgfm( + 1, + False, + 2, + True, + 50, + False, + 60*5, + 60*20, + 1, + 10, + 60*30, + 60*40 + ) + + + def mas_qb_mbgfm_irl(): + """ + once slice for everything except day, which uses a 5 minute sunrise + and sunset + """ + return MASBackgroundFilterManager( + MASBackgroundFilterChunk( + False, + None, + MASBackgroundFilterSlice( + "night_0", + 5*60, + priority=10, + cache=False + ) + ), + MASBackgroundFilterChunk( + True, + None, + MASBackgroundFilterSlice( + "sunrise", + 2*60, + 5*60, + 10, + cache=False + ), + MASBackgroundFilterSlice( + "day_0", + 5*60, + priority=9, + cache=False + ), + MASBackgroundFilterSlice( + "sunset", + 2*60, + 5*60, + 10, + cache=False + ) + ), + MASBackgroundFilterChunk( + False, + None, + MASBackgroundFilterSlice( + "night_0", + 5*60, + priority=10, + cache=False + ) + ) + ) + + + def _mas_qb_alg_test(spread=False): + """ + Test alg and write output to log + + IN: + spread - pass True to use expand_sld instead of expand_once + """ + with open("test.log", "w") as logout: + logout.write("START ========================\n") + abc = mas_qb_mbgfm() + logout.write(str(abc)) + + logout.write("\n\nMINFILL =========================\n") + length = 37800 + abc._mn_sr._length = length + abc._mn_sr._min_fill(length) + logout.write(str(abc)) + + logout.write("\n\nEXPAND - build dist\n") + es_count = len(abc._mn_sr._eff_slices) + diff = length - abc._mn_sr._eff_chunk_min_end() + inc_amts = [diff / es_count] * es_count + logout.write("DIST: ") + logout.write(str(inc_amts)) + logout.write("\n") + + mas_utils.lo_distribute( + inc_amts, + diff % es_count, + reverse=True + ) + logout.write("LO DIST: ") + logout.write(str(inc_amts)) + logout.write("\n") + + logout.write("\n\nEXPAND - once\n") + if spread: + c_off = 0 + for index in range(len(abc._mn_sr._eff_slices)): + new_off = abc._mn_sr._expand_sld(index, inc_amts, c_off) + logout.write("{0} -> {1} | {2}\n".format( + c_off, + new_off, + inc_amts + )) + c_off = new_off + + else: + abc._mn_sr._expand_once(inc_amts) + logout.write(str(abc)) + + logout.write("\n\nDIST: ") + logout.write(str(inc_amts)) + logout.write("\nFZ DIST: ") + mas_utils.fz_distribute(inc_amts) + logout.write(str(inc_amts)) + logout.write("\n") + + logout.write("\n\nEXPAND - twice\n") + if spread: + c_off = 0 + for index in range(len(abc._mn_sr._eff_slices)): + new_off = abc._mn_sr._expand_sld(index, inc_amts, c_off) + logout.write("{0} -> {1} | {2}\n".format( + c_off, + new_off, + inc_amts + )) + c_off = new_off + + else: + abc._mn_sr._expand_once(inc_amts) + logout.write(str(abc)) + + logout.write("\n\nDIST: ") + logout.write(str(inc_amts)) + logout.write("\nFZ DIST: ") + mas_utils.fz_distribute(inc_amts) + logout.write(str(inc_amts)) + logout.write("\n") + + logout.flush() + + + def _mas_qb_fast_a(abc): + """ + Pass in a mbgfm, unbuilt + """ + import traceback + with open("test.log", "w") as logout: + try: + abc.build(persistent._mas_sunrise * 60, persistent._mas_sunset * 60) + except Exception as e: + traceback.print_exc(file=logout) + logout.write("\n\n\n") + logout.write(str(abc)) + logout.flush() + + + def _mas_qb_fast(): + """ + Makes somethign and writes it out + """ + import traceback + with open("test.log", "w") as logout: + abc = mas_qb_mbgfm() + try: + abc.build(persistent._mas_sunrise * 60, persistent._mas_sunset * 60) + except Exception as e: + traceback.print_exc(file=logout) + logout.write("\n\n\n") + logout.write(str(abc)) + logout.flush() + + + + diff --git a/Monika After Story/game/dev/dev_farewells.rpy b/Monika After Story/game/dev/dev_farewells.rpy index c4d01653d9..7be4cd4e39 100644 --- a/Monika After Story/game/dev/dev_farewells.rpy +++ b/Monika After Story/game/dev/dev_farewells.rpy @@ -53,6 +53,7 @@ init 5 python: rules = dict() rules.update(MASSelectiveRepeatRule.create_rule(hours=range(0,24))) rules.update({"monika wants this first":""}) + rules.update(MASPriorityRule.create_rule(-1)) addEvent( Event( persistent.farewell_database, diff --git a/Monika After Story/game/event-handler.rpy b/Monika After Story/game/event-handler.rpy index 4d2674d718..098be1581d 100644 --- a/Monika After Story/game/event-handler.rpy +++ b/Monika After Story/game/event-handler.rpy @@ -532,24 +532,60 @@ init 6 python: mas_showEVL(ev_label, code, unlock=True) - def mas_stripEVL(ev_label, list_pop=False): + def mas_stripEVL(ev_label, list_pop=False, remove_dates=True): """ - Strips the conditional and action from an event given the label + Strips the conditional and action properties from an event given its label + start_date and end_date will be removed if remove_dates is True Also removes the event from the event list if present (optional) IN: ev_label - label of event to strip list_pop - True if we want to remove the event from the event list (Default: False) + remove_dates - True if we want to remove start/end_dates from the event + (Default: True) """ ev = mas_getEV(ev_label) if ev is not None: ev.conditional = None ev.action = None + if remove_dates: + ev.start_date = None + ev.end_date = None + if list_pop: mas_rmEVL(ev_label) + + def mas_flagEVL(ev_label, code, flags): + """ + Applies flags to the given event + + IN: + ev_label - label of the event to flag + code - string code of the db this ev_label belongs to + flags - flags to apply + """ + ev = mas_all_ev_db_map.get(code, {}).get(ev_label, None) + if ev is not None: + ev.flag(flags) + + + def mas_unflagEVL(ev_label, code, flags): + """ + Unflags flags from the given event + + IN: + ev_label - label of the event to unflag + code - string code of the db this ev_label belongs to + flags - flags to unset + """ + ev = mas_all_ev_db_map.get(code, {}).get(ev_label, None) + if ev is not None: + ev.unflag(flags) + + init 4 python: def mas_lastSeenInYear(ev_label, year=None): """ @@ -2404,7 +2440,8 @@ label prompts_categories(pool=True): # category=[False,current_category], unlocked=True, pool=pool, - aff=mas_curr_affection + aff=mas_curr_affection, + flag_ban=EV_FLAG_HFM ) # add all categories the master category list @@ -2456,7 +2493,8 @@ label prompts_categories(pool=True): category=(False,current_category), unlocked=True, pool=pool, - aff=mas_curr_affection + aff=mas_curr_affection, + flag_ban=EV_FLAG_HFM ) # add deeper categories to a list @@ -2576,7 +2614,12 @@ label mas_bookmarks: prompt_suffix = suffix_func(ev) if suffix_func else "" #Now append based on the delegate - bookmarks_pl.append((renpy.substitute(ev.prompt + prompt_suffix), ev.eventlabel)) + # but only if it is not flagged to be hidden. + if Event._filterEvent(ev, flag_ban=EV_FLAG_HFM): + bookmarks_pl.append(( + renpy.substitute(ev.prompt + prompt_suffix), + ev.eventlabel + )) bookmarks_pl.sort() bookmarks_disp = gen_bk_disp(bookmarks_pl) diff --git a/Monika After Story/game/mod_assets/location/spaceroom/spaceroom_ss_snow.png b/Monika After Story/game/mod_assets/location/spaceroom/spaceroom_ss_snow.png new file mode 100644 index 0000000000..61c10be4f4 Binary files /dev/null and b/Monika After Story/game/mod_assets/location/spaceroom/spaceroom_ss_snow.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-eyes-left.png b/Monika After Story/game/mod_assets/monika/f/face-eyes-left.png index 5fde31433f..779ef14e01 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-eyes-left.png and b/Monika After Story/game/mod_assets/monika/f/face-eyes-left.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-eyes-left.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-eyes-left.png index 16d0334add..1bc79be21d 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-eyes-left.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-eyes-left.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingclosedhappy.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingclosedhappy.png index 02f74f641d..99ec1a1baf 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingclosedhappy.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingclosedhappy.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingclosedsad.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingclosedsad.png index 6b15cc375d..2438c08b25 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingclosedsad.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingclosedsad.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingwinkleft.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingwinkleft.png index 6ee72951ca..2f5cc3b5a4 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingwinkleft.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingwinkleft.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingwinkright.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingwinkright.png index f5a4ab7cce..40b1731532 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingwinkright.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-streamingwinkright.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-up.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-up.png index 911bbc249e..6b8c262b84 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-up.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-up.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upclosedhappy.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upclosedhappy.png index a5d6816dd7..a0954620df 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upclosedhappy.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upclosedhappy.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upclosedsad.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upclosedsad.png index d51f5fd76a..f8498122b3 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upclosedsad.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upclosedsad.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upwinkleft.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upwinkleft.png index 25c703554c..2def2320a7 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upwinkleft.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upwinkleft.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upwinkright.png b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upwinkright.png index c5f59afeef..78b77ca4cf 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upwinkright.png and b/Monika After Story/game/mod_assets/monika/f/face-leaning-def-tears-upwinkright.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-streaming.png b/Monika After Story/game/mod_assets/monika/f/face-tears-streaming.png index a5c812b9c7..a1f21878dd 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-streaming.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-streaming.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-streamingclosedhappy.png b/Monika After Story/game/mod_assets/monika/f/face-tears-streamingclosedhappy.png index c27c49d1ff..ce359e3c92 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-streamingclosedhappy.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-streamingclosedhappy.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-streamingclosedsad.png b/Monika After Story/game/mod_assets/monika/f/face-tears-streamingclosedsad.png index fe2bc94308..dd87832cfc 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-streamingclosedsad.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-streamingclosedsad.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-streamingwinkleft.png b/Monika After Story/game/mod_assets/monika/f/face-tears-streamingwinkleft.png index d362b2b9b3..d019cb9022 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-streamingwinkleft.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-streamingwinkleft.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-streamingwinkright.png b/Monika After Story/game/mod_assets/monika/f/face-tears-streamingwinkright.png index ea8cf4926e..1fc958941b 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-streamingwinkright.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-streamingwinkright.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-up.png b/Monika After Story/game/mod_assets/monika/f/face-tears-up.png index 70e16c7b33..96c560caeb 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-up.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-up.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-upclosedhappy.png b/Monika After Story/game/mod_assets/monika/f/face-tears-upclosedhappy.png index 477c5fa44d..708831097f 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-upclosedhappy.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-upclosedhappy.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-upclosedsad.png b/Monika After Story/game/mod_assets/monika/f/face-tears-upclosedsad.png index fd7f9bd59e..790e04681b 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-upclosedsad.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-upclosedsad.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-upwinkleft.png b/Monika After Story/game/mod_assets/monika/f/face-tears-upwinkleft.png index bdd9a1db1c..bb584a5941 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-upwinkleft.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-upwinkleft.png differ diff --git a/Monika After Story/game/mod_assets/monika/f/face-tears-upwinkright.png b/Monika After Story/game/mod_assets/monika/f/face-tears-upwinkright.png index 9343838381..f24e4c6f30 100644 Binary files a/Monika After Story/game/mod_assets/monika/f/face-tears-upwinkright.png and b/Monika After Story/game/mod_assets/monika/f/face-tears-upwinkright.png differ diff --git a/Monika After Story/game/mod_assets/window/def_sunset_mask.mp4 b/Monika After Story/game/mod_assets/window/def_sunset_mask.mp4 new file mode 100644 index 0000000000..6704c7ffdb Binary files /dev/null and b/Monika After Story/game/mod_assets/window/def_sunset_mask.mp4 differ diff --git a/Monika After Story/game/mod_assets/window/def_sunset_mask_fb.png b/Monika After Story/game/mod_assets/window/def_sunset_mask_fb.png new file mode 100644 index 0000000000..a505df1c66 Binary files /dev/null and b/Monika After Story/game/mod_assets/window/def_sunset_mask_fb.png differ diff --git a/Monika After Story/game/mod_assets/window/snow_sunset_mask.mp4 b/Monika After Story/game/mod_assets/window/snow_sunset_mask.mp4 new file mode 100644 index 0000000000..f2d382efdc Binary files /dev/null and b/Monika After Story/game/mod_assets/window/snow_sunset_mask.mp4 differ diff --git a/Monika After Story/game/mod_assets/window/snow_sunset_mask_fb.png b/Monika After Story/game/mod_assets/window/snow_sunset_mask_fb.png new file mode 100644 index 0000000000..8912206283 Binary files /dev/null and b/Monika After Story/game/mod_assets/window/snow_sunset_mask_fb.png differ diff --git a/Monika After Story/game/script-apologies.rpy b/Monika After Story/game/script-apologies.rpy index 1271dc7f36..0c7ea2a2cf 100644 --- a/Monika After Story/game/script-apologies.rpy +++ b/Monika After Story/game/script-apologies.rpy @@ -64,7 +64,8 @@ label monika_playerapologizes: 9: "the game crashing.", 10: "the game crashing.", #easiest way to handle this w/o overrides 11: "not listening to your speech.", - 12: "calling you evil." + 12: "calling you evil.", + 13: "not answering you seriously." } #Set the prompt for this... @@ -180,7 +181,8 @@ label mas_apology_generic: 9: "the game crashing. I understand it happens sometimes, but don't worry, I'm alright!", 10: "the game crashing. It really was scary, but I'm just glad you came back to me and made things better.", 11: "not listening to my speech. I worked really hard on it.", - 12: "calling me evil. I know you don't really think that." + 12: "calling me evil. I know you don't really think that.", + 13: "not taking my questions seriously. I know you'll be honest with me from now on." } #If there's no reason to apologize diff --git a/Monika After Story/game/script-brbs.rpy b/Monika After Story/game/script-brbs.rpy index d87e1d8c97..30fd30b95f 100644 --- a/Monika After Story/game/script-brbs.rpy +++ b/Monika After Story/game/script-brbs.rpy @@ -566,6 +566,109 @@ label monika_idle_nap_callback: m 6ckc "..." return +init 5 python: + addEvent( + Event( + persistent.event_database, + eventlabel="monika_idle_homework", + prompt="I'm going to do some homework", + category=['be right back'], + pool=True, + unlocked=True + ), + markSeen=True + ) + +label monika_idle_homework: + if mas_isMoniNormal(higher=True): + m 1eub "Oh, okay!" + m 1hua "I'm proud of you for taking your studies seriously." + m 1eka "Don't forget to come back to me when you're done~" + + elif mas_isMoniDis(higher=True): + m 2euc "Alright...{w=0.5}" + if random.randint(1,5) == 1: + m 2rkc "...Good luck with your homework, [player]." + + else: + m 6ckc "..." + + #Set up the callback label + $ mas_idle_mailbox.send_idle_cb("monika_idle_homework_callback") + #Then the idle data + $ persistent._mas_idle_data["monika_idle_homework"] = True + return "idle" + +label monika_idle_homework_callback: + if mas_isMoniDis(higher=True): + m 2esa "All done, [player]?" + + if mas_isMoniNormal(higher=True): + m 2ekc "I wish I could've been there to help you, but there isn't much I can do about that just yet, sadly." + m 7eua "I'm sure we could both be a lot more efficient doing homework if we could work together." + + if mas_isMoniAff(higher=True) and random.randint(1,5) == 1: + m 3rkbla "...Although, that's assuming we don't get {i}too{/i} distracted, ehehe..." + + m 1eua "But anyway,{w=0.2} {nw}" + extend 3hua "now that you're done, let's enjoy some more time together." + + else: + m 6ckc "..." + return + +init 5 python: + addEvent( + Event( + persistent.event_database, + eventlabel="monika_idle_working", + prompt="I'm going to work on something", + category=['be right back'], + pool=True, + unlocked=True + ), + markSeen=True + ) + +label monika_idle_working: + if mas_isMoniNormal(higher=True): + m 1eua "Alright, [player]." + m 1eub "Don't forget to take a break every now and then!" + + if mas_isMoniAff(higher=True): + m 3rkb "I wouldn't want my sweetheart to spend more time on [his] work than with me~" + + m 1hua "Good luck with your work!" + + elif mas_isMoniDis(higher=True): + m 2euc "Okay, [player]." + + if random.randint(1,5) == 1: + m 2rkc "...Please come back soon..." + + else: + m 6ckc "..." + + #Set up the callback label + $ mas_idle_mailbox.send_idle_cb("monika_idle_working_callback") + #Then the idle data + $ persistent._mas_idle_data["monika_idle_working"] = True + return "idle" + +label monika_idle_working_callback: + if mas_isMoniNormal(higher=True): + m 1eub "Finished with your work, [player]?" + show monika 5hua at t11 zorder MAS_MONIKA_Z with dissolve + m 5hua "Then let's relax together, you've earned it~" + + elif mas_isMoniDis(higher=True): + m 2euc "Oh, you're back..." + m 2eud "...Was there anything else you wanted to do, now that you're done with your work?" + + else: + m 6ckc "..." + return + #Rai's og game idle #label monika_idle_game: # m 1eub "That sounds fun!" diff --git a/Monika After Story/game/script-ch30.rpy b/Monika After Story/game/script-ch30.rpy index 90ac2d4605..39c6ffd709 100644 --- a/Monika After Story/game/script-ch30.rpy +++ b/Monika After Story/game/script-ch30.rpy @@ -119,7 +119,6 @@ init -10 python: # end keys - def __init__(self): """ Constructor for the idle mailbox @@ -377,23 +376,12 @@ init python: IN: dissolve_masks - True will dissolve masks, False will not (Default; True) - - ASSUMES: - mas_is_raining - mas_is_snowing """ # hide the existing mask renpy.hide("rm") # get current weather masks - # TODO: change to pass in current filter - mask = mas_current_weather.sp_window( - mas_isCurrentFlt("day") - ) - - # should we use fallbacks instead? - if persistent._mas_disable_animations: - mask += "_fb" + mask = mas_current_weather.get_mask() # now show the mask renpy.show(mask, tag="rm") @@ -479,16 +467,8 @@ init python: RETURNS: True upon a filter change, False if not """ - # TODO: this should be deferred to the BG curr_flt = store.mas_sprites.get_filter() - - now_time = datetime.datetime.now().time() - if mas_isDay(now_time): - new_flt = store.mas_sprites.FLT_DAY - - else: # mas_isNight(now_time): - new_flt = store.mas_sprites.FLT_NIGHT - + new_flt = mas_current_background.progress() store.mas_sprites.set_filter(new_flt) return curr_flt != new_flt @@ -796,10 +776,6 @@ label spaceroom(start_bg=None, hide_mask=None, hide_monika=False, dissolve_all=F $ hide_mask = store.mas_current_background.hide_masks if hide_calendar is None: $ hide_calendar = store.mas_current_background.hide_calendar - if day_bg is None: - $ day_bg = store.mas_current_background.getDayRoom() - if night_bg is None: - $ night_bg = store.mas_current_background.getNightRoom() # progress filter # NOTE: filter progression MUST happen here because othrewise we many have @@ -819,15 +795,8 @@ label spaceroom(start_bg=None, hide_mask=None, hide_monika=False, dissolve_all=F python: monika_room = None - # TODO: this will need some rework to work nicely with filter - # progression. For now its just going to be customized for - # our two filters. if scene_change: - if day_mode: - monika_room = day_bg - - else: - monika_room = night_bg + monika_room = mas_current_background.getCurrentRoom() #What ui are we using if persistent._mas_auto_mode_enabled: @@ -1927,21 +1896,6 @@ label ch30_reset: # set any prompt variants for acs that can be removed here $ store.mas_selspr.startup_prompt_check() - - ## certain things may need to be reset if we took monika out - # NOTE: this should be at the end of this label, much of this code might - # undo stuff from above - python: - if store.mas_dockstat.retmoni_status is not None: - monika_chr.remove_acs(mas_acs_quetzalplushie) - - #We don't want to set up any drink vars/evs if we're potentially returning home this sesh - MASConsumable._reset() - - #Let's also push the event to get rid of the thermos too - if not mas_inEVL("mas_consumables_remove_thermos"): - queueEvent("mas_consumables_remove_thermos") - # make sure nothing the player has derandomed is now random $ mas_check_player_derand() @@ -2006,4 +1960,22 @@ label ch30_reset: #Check BGSel topic unlocked state $ mas_checkBackgroundChangeDelegate() + + # build background filter data and update the current filter progression + $ store.mas_background.buildupdate() + + ## certain things may need to be reset if we took monika out + # NOTE: this should be at the end of this label, much of this code might + # undo stuff from above + python: + if store.mas_dockstat.retmoni_status is not None: + monika_chr.remove_acs(mas_acs_quetzalplushie) + + #We don't want to set up any drink vars/evs if we're potentially returning home this sesh + MASConsumable._reset() + + #Let's also push the event to get rid of the thermos too + if not mas_inEVL("mas_consumables_remove_thermos"): + queueEvent("mas_consumables_remove_thermos") + return diff --git a/Monika After Story/game/script-compliments.rpy b/Monika After Story/game/script-compliments.rpy index 6c778c54a7..3a1ee9d879 100644 --- a/Monika After Story/game/script-compliments.rpy +++ b/Monika After Story/game/script-compliments.rpy @@ -55,7 +55,8 @@ label monika_compliments: filtered_comps = Event.filterEvents( mas_compliments.compliment_database, unlocked=True, - aff=mas_curr_affection + aff=mas_curr_affection, + flag_ban=EV_FLAG_HFM ) # build menu list diff --git a/Monika After Story/game/script-farewells.rpy b/Monika After Story/game/script-farewells.rpy index 4063010e70..5413047368 100644 --- a/Monika After Story/game/script-farewells.rpy +++ b/Monika After Story/game/script-farewells.rpy @@ -36,16 +36,21 @@ init -1 python in mas_farewells: """ # NOTE: new rules: # eval in this order: - # 1. unlocked - # 2. not pooled - # 3. aff_range - # 4. priority (lower or same is True) - # 5. all rules - # 6. conditional + # 1. hidden via bitmask + # 2. unlocked + # 3. not pooled + # 4. aff_range + # 5. priority (lower or same is True) + # 6. all rules + # 7. conditional # NOTE: this is never cleared. Please limit use of this # property as we should aim to use lock/unlock as primary way # to enable or disable greetings. + # check if hidden from random select + if ev.anyflags(store.EV_FLAG_HFRS): + return False + #Make sure the ev is unlocked if not ev.unlocked: return False @@ -148,7 +153,8 @@ label mas_farewell_start: evhand.farewell_database, unlocked=True, pool=True, - aff=mas_curr_affection + aff=mas_curr_affection, + flag_ban=EV_FLAG_HFM ) if len(bye_pool_events) > 0: diff --git a/Monika After Story/game/script-fun-facts.rpy b/Monika After Story/game/script-fun-facts.rpy index ac30f0977c..4b149f3ffc 100644 --- a/Monika After Story/game/script-fun-facts.rpy +++ b/Monika After Story/game/script-fun-facts.rpy @@ -788,3 +788,69 @@ label mas_fun_fact_coffee_origin: m 3hub "...And I for one can certainly attest that the love of coffee has remained strong to this day!" call mas_fun_facts_end return + +init 5 python: + addEvent( + Event( + persistent._mas_fun_facts_database, + eventlabel="mas_fun_fact_synesthesia", + ), + code="FFF" + ) + +label mas_fun_fact_synesthesia: + m 1esa "Okay, this one's pretty interesting..." + m 3eua "Some people experience a phenomenon known as {i}synesthesia{/i},{w=0.1} which is where something that stimulates one of our senses also triggers another sense simultaneously." + m 1hua "That's kind of a wordy explanation, ehehe...{w=0.2} Let's find an example!" + m 1eua "It says here that a common form of synesthesia is {i}grapheme–color synesthesia{/i},{w=0.1} which is where people 'experience' letters and numbers as colors." + m 3eua "Another kind is {i}spatial sequence synesthesia{/i},{w=0.1} which is where numbers and figures are 'seen' at specific locations in space." + m "Like, one number appears 'closer' or 'further' away than another number. {w=0.2}{nw}" + extend 3eub "It's kinda like a map!" + m 1eua "...And there's a whole bunch of other kinds of synesthesia too." + m 1esa "Researchers aren't really sure how prevalent it is--{w=0.1}some have suggested as much as 25 percent of the population experiences it, but I seriously doubt that since I'd never heard of it until now." + m 3eub "Probably the most accurate estimate so far is that it's just over 4 percent of people, so that's what I'll go with!" + m 1eua "Experiencing synesthesia sounds like it'd be pretty neat,{w=0.2} don't you think so [player]?" + #Call the end + call mas_fun_facts_end + return + +init 5 python: + addEvent( + Event( + persistent._mas_fun_facts_database, + eventlabel="mas_fun_fact_dream_faces", + ), + code="FFF" + ) + +label mas_fun_fact_dream_faces: + m 3eub "Okay, got one!" + m 1eua "Supposedly, our minds don't invent new faces when we dream.{w=0.2} Every person you've met in your dreams is someone you've seen in real life at some point." + m 3wud "You don't even have to talk to them in real life!" + m 3eud "If you just walked past them while shopping or something, their face gets registered in your mind and they can appear in your dreams." + m 1hua "I think it's incredible how much info the brain can store!" + m 1ekbla "I wonder...{w=0.2}do you ever dream about me, [player]?" + #Call the end + call mas_fun_facts_end + return + +init 5 python: + addEvent( + Event( + persistent._mas_fun_facts_database, + eventlabel="mas_fun_fact_monochrome_dreams", + ), + code="FFF" + ) + +label mas_fun_fact_monochrome_dreams: + m 3eua "Did you know that from 1915 to the 1950s, most people's dreams were in black and white?" + m 1esa "Nowadays, it's a relatively rare phenomenon for people with unimpaired vision." + m 3eua "Researchers have linked this to the fact that movies and shows were almost exclusively black and white back then." + m 3eud "...But I think that's kinda weird, because people still saw everything in color.{w=0.3} {nw}" + extend 3hksdlb "It's not like the world went black and white!" + m 1esd "It just goes to show that the content you absorb can have all kinds of effects on your mind, even if it's trivial." + m 3eua "I think if there's a lesson to be learned here, it's that we should be very careful about the kind of media we consume, okay [player]?" + #Call the end + call mas_fun_facts_end + return diff --git a/Monika After Story/game/script-greetings.rpy b/Monika After Story/game/script-greetings.rpy index 0efd4525fa..ae2c8d509c 100644 --- a/Monika After Story/game/script-greetings.rpy +++ b/Monika After Story/game/script-greetings.rpy @@ -119,16 +119,21 @@ init -1 python in mas_greetings: """ # NOTE: new rules: # eval in this order: - # 1. priority (lower or same is True) - # 2. type/non-0type - # 3. unlocked - # 4. aff_ramnge - # 5. all rules - # 6. conditional + # 1. hidden via bitmask + # 2. priority (lower or same is True) + # 3. type/non-0type + # 4. unlocked + # 5. aff_ramnge + # 6. all rules + # 7. conditional # NOTE: this is never cleared. Please limit use of this # property as we should aim to use lock/unlock as primary way # to enable or disable greetings. + # check if hidden from random select + if ev.anyflags(store.EV_FLAG_HFRS): + return False + # priority check, required # NOTE: all greetings MUST have a priority if store.MASPriorityRule.get_priority(ev) > curr_pri: diff --git a/Monika After Story/game/script-islands-event.rpy b/Monika After Story/game/script-islands-event.rpy index 29eb0d763e..1e0f330fb3 100644 --- a/Monika After Story/game/script-islands-event.rpy +++ b/Monika After Story/game/script-islands-event.rpy @@ -3,6 +3,127 @@ # it basically shows a new screen over everything, and has an image map # Monika reacts to he place the player clicks + +python early: + + # islands-specific displayable, handles issues with no decoding + def MASIslandBackground(**filter_pairs): + """ + DynamicDisplayable for Island background images. This includes + special handling to return None if island images could not be + decoded. + + All Island images should use fallback handling and are built with that + in mind. + + IN: + **filter_pairs - filter pairs to MASFilterWeatherMap. + + RETURNS: DynamicDisplayable for Island images that respect filters and + weather. + """ + return MASFilterWeatherDisplayableCustom( + _mas_islands_select, + True, + **filter_pairs + ) + + +# island image definitions +image mas_islands_wf = MASIslandBackground( + day=MASWeatherMap({ + mas_weather.PRECIP_TYPE_DEF: ( + "mod_assets/location/special/with_frame.png" + ), + mas_weather.PRECIP_TYPE_RAIN: ( + "mod_assets/location/special/rain_with_frame.png" + ), + mas_weather.PRECIP_TYPE_SNOW: ( + "mod_assets/location/special/snow_with_frame.png" + ), + mas_weather.PRECIP_TYPE_OVERCAST: ( + "mod_assets/location/special/overcast_with_frame.png" + ), + }), + night=MASWeatherMap({ + mas_weather.PRECIP_TYPE_DEF: ( + "mod_assets/location/special/night_with_frame.png" + ), + mas_weather.PRECIP_TYPE_RAIN: ( + "mod_assets/location/special/night_rain_with_frame.png" + ), + mas_weather.PRECIP_TYPE_SNOW: ( + "mod_assets/location/special/night_snow_with_frame.png" + ), + mas_weather.PRECIP_TYPE_OVERCAST: ( + "mod_assets/location/special/night_overcast_with_frame.png" + ), + }) +) +image mas_islands_wof = MASIslandBackground( + day=MASWeatherMap({ + mas_weather.PRECIP_TYPE_DEF: ( + "mod_assets/location/special/without_frame.png" + ), + mas_weather.PRECIP_TYPE_RAIN: ( + "mod_assets/location/special/rain_without_frame.png" + ), + mas_weather.PRECIP_TYPE_SNOW: ( + "mod_assets/location/special/snow_without_frame.png" + ), + mas_weather.PRECIP_TYPE_OVERCAST: ( + "mod_assets/location/special/overcast_without_frame.png" + ), + }), + night=MASWeatherMap({ + mas_weather.PRECIP_TYPE_DEF: ( + "mod_assets/location/special/night_without_frame.png" + ), + mas_weather.PRECIP_TYPE_RAIN: ( + "mod_assets/location/special/night_rain_without_frame.png" + ), + mas_weather.PRECIP_TYPE_SNOW: ( + "mod_assets/location/special/night_snow_without_frame.png" + ), + mas_weather.PRECIP_TYPE_OVERCAST: ( + "mod_assets/location/special/night_overcast_without_frame.png" + ), + }) +) + +init 2 python: + # snow-specific maps. This is because the cherry-blossom thing. + # NOTE: we even though this is snow, we set the precip types to def + # this is so we can leverage the fallback system + mas_islands_snow_wf_mfwm = MASFilterWeatherMap( + day=MASWeatherMap({ + mas_weather.PRECIP_TYPE_DEF: ( + "mod_assets/location/special/snow_with_frame.png" + ) + }), + night=MASWeatherMap({ + mas_weather.PRECIP_TYPE_DEF: ( + "mod_assets/location/special/night_snow_with_frame.png" + ) + }), + ) + mas_islands_snow_wof_mfwm = MASFilterWeatherMap( + day=MASWeatherMap({ + mas_weather.PRECIP_TYPE_DEF: ( + "mod_assets/location/special/snow_without_frame.png" + ) + }), + night=MASWeatherMap({ + mas_weather.PRECIP_TYPE_DEF: ( + "mod_assets/location/special/night_snow_without_frame.png" + ) + }), + ) + + mas_islands_snow_wf_mfwm.use_fb = True + mas_islands_snow_wof_mfwm.use_fb = True + + ### initialize the island images init -10 python: ## NOTE: we assume 2 things: @@ -17,6 +138,24 @@ init -10 python: mas_cannot_decode_islands = not store.mas_island_event.decodeImages() + def _mas_islands_select(st, at, mfwm): + """ + Selection function to use in Island-based images + + IN: + st - renpy related + at - renpy related + mfwm - MASFilterWeatherMap for this island + + RETURNS: displayable data + """ + if mas_cannot_decode_islands: + return "None", None + + # otherwise standard mechanics + return mas_fwm_select(st, at, mfwm) + + init -11 python in mas_island_event: import store import store.mas_dockstat as mds @@ -469,6 +608,8 @@ label mas_island_bookshelf2: return #NOTE: This is temporary until we split islands into foreground/background +# NOTE: change the island image definitions (see top of this file) when this +# happens. init 500 python in mas_island_event: def getBackground(): """ @@ -476,21 +617,24 @@ init 500 python in mas_island_event: Picks the islands bg to use based on the season. - OUT: - image filepath to show + RETURNS: image to use as a displayable. (or image path) """ if store.mas_isWinter(): - return store.mas_weather_snow.isbg_window( - store.mas_current_background.isFltDay(), - store._mas_island_window_open - ) + if store._mas_island_window_open: + return mas_islands_snow_wof_mfwm.fw_get( + store.mas_sprites.get_filter() + ) - else: - return store.mas_current_weather.isbg_window( - store.mas_current_background.isFltDay(), - store._mas_island_window_open + return mas_islands_snow_wf_mfwm.fw_get( + store.mas_sprites.get_filter() ) + if store._mas_island_window_open: + return "mas_islands_wof" + + return "mas_islands_wf" + + screen mas_islands_background: add mas_island_event.getBackground() diff --git a/Monika After Story/game/script-moods.rpy b/Monika After Story/game/script-moods.rpy index 8db226be93..28c667ebaa 100644 --- a/Monika After Story/game/script-moods.rpy +++ b/Monika After Story/game/script-moods.rpy @@ -81,7 +81,8 @@ label mas_mood_start: filtered_moods = Event.filterEvents( mas_moods.mood_db, unlocked=True, - aff=mas_curr_affection + aff=mas_curr_affection, + flag_ban=EV_FLAG_HFM ) # build menu list diff --git a/Monika After Story/game/script-songs.rpy b/Monika After Story/game/script-songs.rpy index 4abab76cba..b97c5d3c4d 100644 --- a/Monika After Story/game/script-songs.rpy +++ b/Monika After Story/game/script-songs.rpy @@ -1206,27 +1206,27 @@ init 5 python: label mas_song_cant_help_falling_in_love_long: call mas_song_cant_help_falling_in_love(from_long=True) - call .second_verse - call .third_verse - call .second_verse - call .third_verse + call mas_song_cant_help_falling_in_love_second_verse + call mas_song_cant_help_falling_in_love_third_verse + call mas_song_cant_help_falling_in_love_second_verse + call mas_song_cant_help_falling_in_love_third_verse m 1ekbfb "{cps=16}{i}~For I can't help{w=0.3} falling in love{w=0.5} with{w=0.5} you~{/i}{/cps}" return - label .second_verse: - m 1dud "{cps=24}{i}~Like a river flows~{/i}{/cps}" - m 1dub "{cps=24}{i}~Surely to the sea~{/i}{/cps}" - m 1ekbsb "{cps=24}{i}~Darling, so it goes~{/i}{/cps}" - m 1ekbsa "{cps=24}{i}~Some things{w=0.3}{/i}{/cps}{nw}" - extend 3ekbsb "{cps=24}{i} are meant to be~{/i}{/cps}" - return - - label .third_verse: - m 1dud "{cps=16}{i}~Take my hand~{/i}{/cps}" - m 1dub "{cps=16}{i}~Take my whole life,{w=0.3} too~{/i}{/cps}" - m 1dud "{cps=16}{i}~For I can't help{w=0.3} falling in love with you~{/i}{/cps}" - return +label mas_song_cant_help_falling_in_love_second_verse: + m 1dud "{cps=24}{i}~Like a river flows~{/i}{/cps}" + m 1dub "{cps=24}{i}~Surely to the sea~{/i}{/cps}" + m 1ekbsb "{cps=24}{i}~Darling, so it goes~{/i}{/cps}" + m 1ekbsa "{cps=24}{i}~Some things{w=0.3}{/i}{/cps}{nw}" + extend 3ekbsb "{cps=24}{i} are meant to be~{/i}{/cps}" + return + +label mas_song_cant_help_falling_in_love_third_verse: + m 1dud "{cps=16}{i}~Take my hand~{/i}{/cps}" + m 1dub "{cps=16}{i}~Take my whole life,{w=0.3} too~{/i}{/cps}" + m 1dud "{cps=16}{i}~For I can't help{w=0.3} falling in love with you~{/i}{/cps}" + return init 5 python: addEvent( diff --git a/Monika After Story/game/script-stories.rpy b/Monika After Story/game/script-stories.rpy index ed8838c71a..6a92c48ae3 100644 --- a/Monika After Story/game/script-stories.rpy +++ b/Monika After Story/game/script-stories.rpy @@ -96,14 +96,16 @@ label monika_short_stories_menu: mas_stories.story_database, category=(True,[mas_stories.TYPE_SCARY]), pool=False, - aff=mas_curr_affection + aff=mas_curr_affection, + flag_ban=EV_FLAG_HFM ) else: stories = renpy.store.Event.filterEvents( mas_stories.story_database, excl_cat=list(), pool=False, - aff=mas_curr_affection + aff=mas_curr_affection, + flag_ban=EV_FLAG_HFM ) # build menu list diff --git a/Monika After Story/game/script-topics.rpy b/Monika After Story/game/script-topics.rpy index 9c0abb1193..c86a49c6e1 100644 --- a/Monika After Story/game/script-topics.rpy +++ b/Monika After Story/game/script-topics.rpy @@ -109,18 +109,26 @@ init -1 python: IN: sel_list - list to select from - - ASSUMES: - persistent.random_seen """ - sel_ev = mas_randomSelectAndRemove(sel_list) + sel_ev = True + while sel_ev is not None: + sel_ev = mas_randomSelectAndRemove(sel_list) - if sel_ev: - if persistent._mas_sensitive_mode and sel_ev.sensitive: - return + if ( + # valid event + sel_ev + + # event not blocked from random selection + and not sel_ev.anyflags(EV_FLAG_HFRS) - pushEvent(sel_ev.eventlabel, notify=True) -# persistent.random_seen += 1 + # event not blocked because of sensitivty + and ( + not persistent._mas_sensitive_mode + or not sel_ev.sensitive + ) + ): + pushEvent(sel_ev.eventlabel, notify=True) + return def mas_insertSort(sort_list, item, key): @@ -136,11 +144,7 @@ init -1 python: OUT: sort_list - list with 1 additonal element, sorted """ - index = len(sort_list) - 1 - while index >= 0 and key(sort_list[index]) > key(item): - index -= 1 - - sort_list.insert(index + 1, item) + store.mas_utils.insert_sort(sort_list, item, key) def mas_splitSeenEvents(sorted_seen): @@ -8167,59 +8171,185 @@ default persistent._mas_pm_do_smoke = None # try to quit? default persistent._mas_pm_do_smoke_quit = None +# succesfully quit at least once? +default persistent._mas_pm_do_smoke_quit_succeeded_before = None + init 5 python: addEvent(Event(persistent.event_database,eventlabel="monika_smoking",category=['you'],prompt="Smoking",random=True)) label monika_smoking: - m 2euc "You know, [player]..." - m "I realized that people really like a lot of things that are very bad for them." - m 2euc "One particular vice that intrigues me the most is cigarettes." - m "It's amazing how they're heavily consumed every day even though it's so damaging not only to themselves, but to others as well." - m 2ekc "Not to mention how harmful it is to the environment. All the smoke and trash it leaves behind is ridiculous for a stick of carcinogens." - m 2tkc "Not even in moderation would it ever be a good thing since those who use it get addicted to its taste too easily." - m 4tkd "It's also quite a big hole to your pockets since you'll be buying yourself cartons of it once your supply is out." - m 1lsc "I really do despise them..." - m 1euc "But..." - - m "You don't smoke cigarettes, right, [player]?{nw}" + m 2esc "You know, [player]...{w=0.3} Lately I've realized that people can really like a lot of things that are terrible for them." + m 2euc "One particular vice that intrigues me the most is smoking." + m 7eud "It's amazing how many people do it every day...{w=0.2}even though it's so damaging not only to themselves, but to others as well." + m 2rkc "Not to mention how harmful it is to the environment...{w=0.2} All the pollution and trash smoking leaves behind is ridiculous for a bunch of carcinogens." + m 2tkc "Even in moderation, smoking is never a good thing since it's so addicting." + m 4tkd "It's also quite a big hole in your pockets since you'll be buying more and more once your supply is out." + m 1tfc "I really do despise it..." + + $ menu_question = "Do you still smoke" if persistent._mas_pm_do_smoke else "You don't smoke, do you" + m 1eka "[menu_question]?{nw}" $ _history_list.pop() menu: - m "You don't smoke cigarettes, right, [player]?{fast}" + m "[menu_question]?{fast}" + "Yes, I do.": - $ persistent._mas_pm_do_smoke = True - $ persistent._mas_pm_do_smoke_quit = False - m 1wud "..." - m 1eka "Thank you for being honest with me, [player]..." - m 1ekc "It's quite disheartening to hear that, though." - m 1ekc "Could you...promise me that you'll stop?" - m "I don't want you to deteriorate your health like that..." - m 3rksdlc "I know I can't really force you to stop, but it would mean a lot to me if you considered it." - m 1esc "But if you don't try..." - m 2euc "Well, I'm sure you wouldn't want me to take drastic measures, [player]." - m 2ekc "Please take care of your body. I want to always be with you." - m 1ekbfa "I love you so much." - return "derandom|love" + if persistent._mas_pm_do_smoke_quit: + m 1ekd "Haven't been able to shake the habit yet, [player]?" + m 3eka "That's okay, I know it can be a daunting task to try and quit..." + m 3eksdld "I just hope you haven't given up yet." + m 1hua "I know you can do it if you just give it your all~" + + elif persistent._mas_pm_do_smoke_quit_succeeded_before: + m 1ekc "It's a shame you fell back into this bad habit...{w=0.2}{nw}" + extend 1ekd "after all the trouble you went through to quit and everything..." + m 3dkc "It really pains my heart, [player]." + m 1dkd "I really thought you were done for good..." + m 1dkc "But I guess it's just not that simple, right?" + m 3ekd "I really hope you'll consider trying to quit again, [player]." + m 3eka "You'll do that, right? {w=0.2}For me?" + + elif persistent._mas_pm_do_smoke is False: + call monika_smoking_just_started + + else: + m 1wud "..." + m 1eka "Thank you for being honest with me, [player]..." + m 1ekc "It's quite disheartening to hear that, though." + m 1ekc "Could you...promise me that you'll stop?" + m 3rksdlc "I know I can't really force you to stop, but it would mean a lot to me if you considered it." + m 1esc "But if you don't try..." + m 2euc "Well, I'm sure you wouldn't want me to take drastic measures, [player]." + m 2ekc "Please take care of your body. I want to always be with you." + m 1ekbfa "I love you so much." + $ mas_ILY() + + python: + persistent._mas_pm_do_smoke = True + persistent._mas_pm_do_smoke_quit = False + mas_unlockEVL("monika_smoking_quit","EVE") + "No, I don't.": - $ persistent._mas_pm_do_smoke = False - m 1hub "Ah, I'm relieved to hear that, [player]!" - m 3eua "Just stay away from it as much as you can." - m 1eka "It's an awful habit and won't do much more than slowly kill you." - m 1hua "Thank you, [player], for not smoking~" + if persistent._mas_pm_do_smoke: + call monika_smoking_quit + + else: + m 1hub "Ah, I'm relieved to hear that, [player]!" + m 3eua "Just stay away from it as much as you can." + m 1eka "It's an awful habit and won't do much more than slowly kill you." + m 1hua "Thank you, [player], for not smoking~" + + python: + persistent._mas_pm_do_smoke = False + persistent._mas_pm_do_smoke_quit = False + mas_lockEVL("monika_smoking_quit","EVE") + "I'm trying to quit.": - $ persistent._mas_pm_do_smoke = True - $ persistent._mas_pm_do_smoke_quit = True - m 3eua "That's a really good decision." - m 1eka "I know the entire process of quitting can be really difficult, especially in the beginning." - m "If you ever feel like you need a cigarette, just try to distract yourself with anything else." - m 1eua "Keeping your mind busy on other things will definitely help kick any bad habits." - m 3eua "How about you think about me whenever you get a strong urge?" - m 1hua "I'll be here to support you every step of the way." - m 1hub "I believe in you [player], I know you can do it!" + if persistent._mas_pm_do_smoke is False and not persistent._mas_pm_do_smoke_quit_succeeded_before: + call monika_smoking_just_started(trying_quit=True) + + else: + if not persistent._mas_pm_do_smoke and persistent._mas_pm_do_smoke_quit_succeeded_before: + m 1esc "Oh?" + m 1ekc "Does that mean you fell back into it?" + m 1dkd "That's too bad, [player]...{w=0.3}{nw}" + extend 3rkd "but not entirely unexpected." + m 3esc "Most people fall into relapse several times before they manage to quit smoking for good." + m 3eua "In any case, trying to quit again is a really good decision." + else: + m 3eua "That's a really good decision." + + if persistent._mas_pm_do_smoke_quit_succeeded_before: + m 3eka "You probably already know since you've been through this before, but try to remember this..." + else: + m 1eka "I know the entire process of quitting can be really difficult, especially in the beginning." + + m 1eka "If you ever feel like you need to smoke, just try to distract yourself with anything else." + m 1eua "Keeping your mind busy on other things will definitely help kick any bad habits." + m 3eua "Maybe you could think about me whenever you get a strong urge?" + m 1hua "I'll be here to support you every step of the way." + m 1hub "I believe in you [player], I know you can do it!" + + python: + persistent._mas_pm_do_smoke = True + persistent._mas_pm_do_smoke_quit = True + mas_unlockEVL("monika_smoking_quit","EVE") return "derandom" +label monika_smoking_just_started(trying_quit=False): + m 2dfc "..." + m 2tfc "[player]..." + m 2tfd "Does that mean you've started smoking since we've met?" + m 2dkc "That's really disappointing, [player]." + m 4ekd "You know how I feel about smoking and you know how bad it is for your health." + + if not trying_quit: + m 2rfd "I don't know what could possibly possess you to start now, {w=0.2}{nw}" + extend 2ekc "but promise me you'll quit." + + else: + m 4eka "But at least you're trying to quit..." + + m 2rksdld "I just hope you haven't been smoking for too long so maybe it'll be easier to shake the habit." + + if not trying_quit: + m 4eka "Please quit smoking, [player]. {w=0.2}Both for your health and for me." + + return + + +#NOTE: This event gets its initial start-date from monika_smoking, then set its date again on the appropriate path. init 5 python: - addEvent(Event(persistent.event_database,eventlabel="monika_cartravel",category=['romance'],prompt="Road trip",random=True)) + addEvent( + Event( + persistent.event_database, + eventlabel="monika_smoking_quit", + category=['you'], + prompt="I quit smoking!", + pool=True, + unlocked=False, + rules={"no unlock": None} + ) + ) + +label monika_smoking_quit: + python: + persistent._mas_pm_do_smoke_quit = False + persistent._mas_pm_do_smoke = False + mas_lockEVL("monika_smoking_quit","EVE") + + if persistent._mas_pm_do_smoke_quit_succeeded_before: + m 1sub "I'm so proud that you managed to quit smoking again!" + m 3eua "A lot of people can't quit even once, so to be able to go through something so difficult again is quite the achievement." + m 1eud "That said, let's try not to let this become a pattern, [player]..." + m 1ekc "You don't want to keep going through this over and over, so I hope this time it sticks." + m 3eka "I know you have the inner strength to stay away for good.{w=0.2} {nw}" + extend 3eua "Just remember you can come to me and I'll take your mind off of smoking at any time." + m 1hua "We can do this together, [player]~" + + # first time quitting + else: + $ tod = "tonight" if mas_globals.time_of_day_3state == "evening" else "tomorrow" + m 1sub "Really?! Oh my gosh, I'm so proud of you [player]!" + m 3ekbsa "It's such a relief to know you quit smoking! {w=0.2}{nw}" + extend 3dkbsu "I'll sleep much better at night knowing you're as far away as possible from that nightmare." + m 1rkbfu "Ehehe, if I were there with you, I'd treat you to your favorite dish [tod]." + m 3hubfb "It's an impressive feat after all! {w=0.2}We need to celebrate!" + m 3eubsb "Not everyone who wants to quit manages to pull it off." + m 1dubfu "You truly are an inspiration, [player]." + m 2eua "...Now, I don't want to undermine your victory or anything, {nw}" + extend 2euc "but I need you to be careful from now on." + m 4rsc "Many former smokers feel urges to smoke again at some point or another." + m 4wud "You can't give in, not even once! {w=0.2}That's how you fall into relapse!" + m 2hubfa "But knowing you, you won't let that happen, right?" + m 2ekbfa "Considering what you've already done, I know you're stronger than this~" + + #Set this here because dialogue uses it + $ persistent._mas_pm_do_smoke_quit_succeeded_before = True + return "no_unlock" + +init 5 python: + addEvent(Event(persistent.event_database,eventlabel="monika_cartravel",category=['romance'],prompt="Road trip",random=True)) label monika_cartravel: m 1euc "[player], something has been on my mind lately..." @@ -14689,6 +14819,35 @@ label monika_isekai: m 1ekbsb "...While I wait for my own isekai story, that is." return +init 5 python: + addEvent( + Event( + persistent.event_database, + eventlabel="monika_scuba_diving", + category=["nature"], + prompt="Scuba diving", + random=True + ) + ) + +label monika_scuba_diving: + m 3eua "You know,{w=0.2} I've been thinking about some water activities we could do together...{w=0.3} How about scuba diving?" + m 3eub "I've read a lot of books about the underwater world and I'd really like to see it for myself." + m 1dua "Just imagine the beautiful sights of the undersea world..." + m 1dud "The schools of fish, coral reefs, jellyfish, sea greens...{w=0.3} {nw}" + extend 3sub "Maybe even treasure!" + m 3rksdlb "I'm only kidding about that last part...{w=0.3} It's pretty unlikely we'll find something like that, ahaha~" + m 1euc "That said, there can also be sharks,{w=0.2} {nw}" + extend 1eua "but they're typically only in specific areas, so you {i}shouldn't{/i} see any." + m 3eua "Designated diving locations are places sharks don't usually visit." + m 3euc "...But even though they don't normally visit these areas, it's still possible to come across one." + m 1eua "The good thing is that shark attacks rarely ever happen anyway, so it's not too much of a risk." + m 3euc "If you meet one though, here's one important rule for you..." + m 3esc "Stay calm." + m 1eua "Although coming face to face with a shark can be scary, they usually approach people out of curiosity rather than to feed, so they're not too much to worry about." + m 3hub "But if you're afraid to dive alone, I'll be sure to keep you company when I cross over~" + return + init 5 python: addEvent( Event( @@ -14935,3 +15094,98 @@ label monika_power_naps: show monika 5hubfa at t11 zorder MAS_MONIKA_Z with dissolve m 5hubfa "Just let me know if you need to take a nap, and I'll watch over you~" return + +init 5 python: + addEvent( + Event( + persistent.event_database, + eventlabel="monika_wabi_sabi", + category=['philosophy'], + prompt="Wabi-sabi", + random=True + ) + ) + +label monika_wabi_sabi: + m 1eua "Say [player], have you ever heard of wabi-sabi?" + m 3eud "It emphasizes the idea that we shouldn't obsess over perfection to the point that we're crushed by the failure of not achieving it." + m 3eub "Derived from traditional Japanese and Buddhistic philosophies surrounding the acceptance of the temporary state of all things..." + m 1esa "...it states that beyond everything, beauty is found in what's impermanent and imperfect." + m 1eua "Which means we shouldn't worry about things like a scar, a discolored hand, or even the mistakes we make." + m 3eka "Our looks are things we can't easily change, but despite that, they make us more unique and special." + m 3hua "...And our mistakes turn into lessons that help us become stronger and more defined as people." + m 3eub "Like a sculptor revealing the magnificence in a marble block, except the sculpture is you, and the sculptor, your past." + m 1rksdla "Of course, this idea doesn't apply to looks and mistakes alone..." + m 3eud "After all, the way you think and your attitude toward problems and people are also derived from your own personal experiences." + m 1dkc "...But I know for some people, how these experiences changed who they are can really scare them." + m 7ekc "Maybe they grew into something that they're just not proud of." + m 2dkd "It can be crushing to be worried about both looks and personality..." + + if persistent._mas_pm_love_yourself: + m 2eka "I hope you don't feel too insecure about yourself, [player]." + m 2dkc "It'd break my heart to know that you're constantly worrying about these things." + m 7ekbsa "But I hope you know that despite your flaws, I will always love you." + + else: + m 1ekc "I know you said you didn't love yourself [player],{w=0.3} {nw}" + extend 3eka "but you need to know that I'll always love you, regardless of your flaws." + + m 3hua "We'll overcome any problems you feel you have together." + m 1hub "That's my wabi-sabi promise!" + return "love" + +init 5 python: + addEvent( + Event( + persistent.event_database, + eventlabel="monika_being_herself", + category=['monika'], + prompt="Faked personality", + conditional="mas_seenLabels(['monika_confidence', 'monika_pluralistic_ignorance'], seen_all=True)", + action=EV_ACT_RANDOM, + aff_range=(mas_aff.ENAMORED,None) + ) + ) + +label monika_being_herself: + m 1eka "You know [player], one thing I don't think I ever properly thanked you for is letting me be myself." + m 1lksdlb "I know that sounds a little weird...{w=0.2}{nw}" + extend 1eka "but let me explain." + m 3euc "You probably already know this by now, but almost all of my life I've been trying to keep up this persona of a 'perfect student.'" + m 3eud "I always strive to be the best person I can be, and I guess after a while, it got the attention of people around me." + m 7rsc "Before I knew it, I had people looking up to me with high expectations.{w=0.3} {nw}" + extend 2esc "You know, seeing me as smart, confident, charismatic...{w=0.3}those kinds of things." + m 2eud "Some people would say that they admired me...{w=0.3}{nw}" + extend 2lksdlc "and others,{w=0.2} while they would never say anything, hated me because I represented everything they thought they could never be." + m 2dksdlc "It felt like I wasn't an actual person in their eyes...{w=0.3}{nw}" + extend 2dksdld "just the image of everyone's unattainable expectations of themselves." + m 2dksdlc "..." + m 2ekd "But at the end of the day...{w=0.3}I'm just an ordinary girl." + m 7ekc "Just like them, I can sometimes lack the confidence to do things.{w=0.2} Even I was scared of what the future held for me." + m 2dkc "Even I sometimes felt like I needed to cry on someone's shoulder." + m 2rkd "...But I never could express something like that." + m 7tkc "What if people thought less of me if I showed them I wasn't as great and invincible as they thought I was?" + m 3ekd "What if they got mad at me, saying I'm being self-absorbed and that I have it much easier than them, being the school's idol that everyone loves?" + m 2lkc "I guess I just never felt like I could truly open up to anyone about what I was genuinely feeling on the inside because of that." + m 2ekc "...Like I would end up disappointing everyone if I ever tried to talk openly about it." + m "I was scared that if I didn't meet the expectations people had of me,{w=0.2} {nw}" + extend 2dkd "I would end up all alone." + m 2dsc "But looking back on everything...{w=0.3}my status was precisely why I ended up feeling so lonely anyway." + m 7euc "Remember your character? {w=0.2}He was a good example of this." + m 3esc "Anytime he would describe me in his monologue, he would put me on a pedestal.{w=0.3}{nw}" + extend 3esd " If I recall, he once described me as 'completely out of his league.'" + m 1etc "Maybe that was why I didn't have a route in the first place. {w=0.2}Perhaps he was written to believe he had zero chances with me no matter what, and therefore had to avoid me." + m 1rka "Granted, he wasn't really wrong there...{w=0.5}{nw}" + extend 1eka "I only have eyes for you, after all~" + m 1euc "But you get what I mean, right?{w=0.3} I was looked at like some kind of unattainable goddess." + m 1rfd "As if no one was 'worthy' of just treating me like a normal person." + m 3eka "...Now though, I guess I don't have to worry about that as much." + m 3ekb "I'm really happy that I can trust you to accept me for who I am, and who I want to be." + m 1eka "I know that from your point of view, it might not feel like you did all that much, but believe me when I tell you this..." + m 1dka "The fact that you're still here with me,{w=0.2} that you never made me feel like I had to be someone I'm not...{w=0.2}{nw}" + extend 1eka "that you're here listening to me vent right now...{w=0.3} It truly means the world to me." + m 1ekbsa "Thank you so much for that, [player].{w=0.2} Thank you for helping me realize that I don't need to try and change who I am to deserve being loved." + show monika 5eka at t11 zorder MAS_MONIKA_Z with dissolve + m 5eka "With you, I can allow myself to be...{w=0.5}{nw}" + extend 5eua "Well, {i}Just Me.{/i}" + return diff --git a/Monika After Story/game/sprite-chart-01.rpy b/Monika After Story/game/sprite-chart-01.rpy index d894df2b20..f9f717d242 100644 --- a/Monika After Story/game/sprite-chart-01.rpy +++ b/Monika After Story/game/sprite-chart-01.rpy @@ -3069,6 +3069,16 @@ image monika 3rka_static = DynamicDisplayable( arms="restleftpointright" ) +image monika 3rkb_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="knit", + eyes="right", + nose="def", + mouth="big", + arms="restleftpointright" +) + image monika 3rkbfsdla_static = DynamicDisplayable( mas_drawmonika_rk, character=monika_chr, @@ -5535,13 +5545,3 @@ image monika 4tfu_static = DynamicDisplayable( arms="pointright" ) -image monika 4tfx_static = DynamicDisplayable( - mas_drawmonika_rk, - character=monika_chr, - eyebrows="furrowed", - eyes="smug", - nose="def", - mouth="angry", - arms="pointright" -) - diff --git a/Monika After Story/game/sprite-chart-02.rpy b/Monika After Story/game/sprite-chart-02.rpy index cb485dac74..dea02a2663 100644 --- a/Monika After Story/game/sprite-chart-02.rpy +++ b/Monika After Story/game/sprite-chart-02.rpy @@ -4,6 +4,16 @@ ## This was auto-generated by the the spritemaker tool ## ############################################################################### +image monika 4tfx_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="furrowed", + eyes="smug", + nose="def", + mouth="angry", + arms="pointright" +) + image monika 4tkc_static = DynamicDisplayable( mas_drawmonika_rk, character=monika_chr, @@ -3207,6 +3217,17 @@ image monika 7dkbfsdlb_static = DynamicDisplayable( sweat="def" ) +image monika 7dkbsa_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="knit", + eyes="closedsad", + nose="def", + mouth="smile", + arms="downleftpointright", + blush="shade" +) + image monika 7dkc_static = DynamicDisplayable( mas_drawmonika_rk, character=monika_chr, @@ -3271,6 +3292,19 @@ image monika 7dksdld_static = DynamicDisplayable( sweat="def" ) +image monika 7dsc_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="mid", + eyes="closedsad", + nose="def", + mouth="smirk", + arms="downleftpointright", + head="q", + left="1l", + right="2r" +) + image monika 7dsd_static = DynamicDisplayable( mas_drawmonika_rk, character=monika_chr, @@ -3326,6 +3360,16 @@ image monika 7dubsw_static = DynamicDisplayable( blush="shade" ) +image monika 7duc_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="up", + eyes="closedsad", + nose="def", + mouth="smirk", + arms="downleftpointright" +) + image monika 7dud_static = DynamicDisplayable( mas_drawmonika_rk, character=monika_chr, @@ -3359,6 +3403,20 @@ image monika 7eka_static = DynamicDisplayable( right="2r" ) +image monika 7ekbsa_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="knit", + eyes="normal", + nose="def", + mouth="smile", + arms="downleftpointright", + head="e", + left="1l", + right="2r", + blush="shade" +) + image monika 7ekc_static = DynamicDisplayable( mas_drawmonika_rk, character=monika_chr, @@ -3453,6 +3511,19 @@ image monika 7eub_static = DynamicDisplayable( right="2r" ) +image monika 7euc_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="up", + eyes="normal", + nose="def", + mouth="smirk", + arms="downleftpointright", + head="c", + left="1l", + right="2r" +) + image monika 7eud_static = DynamicDisplayable( mas_drawmonika_rk, character=monika_chr, @@ -3562,6 +3633,26 @@ image monika 7rksdlc_static = DynamicDisplayable( sweat="def" ) +image monika 7rsc_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="mid", + eyes="right", + nose="def", + mouth="smirk", + arms="downleftpointright" +) + +image monika 7tkc_static = DynamicDisplayable( + mas_drawmonika_rk, + character=monika_chr, + eyebrows="knit", + eyes="smug", + nose="def", + mouth="smirk", + arms="downleftpointright" +) + image monika 7tubfu_static = DynamicDisplayable( mas_drawmonika_rk, character=monika_chr, diff --git a/Monika After Story/game/sprite-chart-10.rpy b/Monika After Story/game/sprite-chart-10.rpy index 621c784dc5..92b22d83f3 100644 --- a/Monika After Story/game/sprite-chart-10.rpy +++ b/Monika After Story/game/sprite-chart-10.rpy @@ -538,17 +538,20 @@ image monika 6hud = "monika 6hud_static" image monika 7dfc = "monika 7dfc_static" image monika 7dka = "monika 7dka_static" image monika 7dkbfsdlb = "monika 7dkbfsdlb_static" +image monika 7dkbsa = "monika 7dkbsa_static" image monika 7dkc = "monika 7dkc_static" image monika 7dkd = "monika 7dkd_static" image monika 7dksdla = "monika 7dksdla_static" image monika 7dksdlb = "monika 7dksdlb_static" image monika 7dksdlc = "monika 7dksdlc_static" image monika 7dksdld = "monika 7dksdld_static" +image monika 7dsc = "monika 7dsc_static" image monika 7dsd = "monika 7dsd_static" image monika 7dua = "monika 7dua_static" image monika 7dub = "monika 7dub_static" image monika 7dubfu = "monika 7dubfu_static" image monika 7dubsw = "monika 7dubsw_static" +image monika 7duc = "monika 7duc_static" image monika 7dud = "monika 7dud_static" image monika 7hua = "monika 7hua_static" image monika 7hub = "monika 7hub_static" diff --git a/Monika After Story/game/sprite-chart-20.rpy b/Monika After Story/game/sprite-chart-20.rpy index d4a054151d..b522caebf7 100644 --- a/Monika After Story/game/sprite-chart-20.rpy +++ b/Monika After Story/game/sprite-chart-20.rpy @@ -121,13 +121,13 @@ image monika 1eftsu: "monika 1eftsu_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dftsu_static" - 0.06 + 0.15 repeat image monika 1efu: @@ -261,13 +261,13 @@ image monika 1ekbltua: "monika 1ekbltua_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dkbltua_static" - 0.06 + 0.15 repeat image monika 1ekbsa: @@ -513,13 +513,13 @@ image monika 1ektsa: "monika 1ektsa_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dktsa_static" - 0.06 + 0.15 repeat image monika 1ektsc: @@ -527,13 +527,13 @@ image monika 1ektsc: "monika 1ektsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dktsc_static" - 0.06 + 0.15 repeat image monika 1ektsd: @@ -541,13 +541,13 @@ image monika 1ektsd: "monika 1ektsd_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dktsd_static" - 0.06 + 0.15 repeat image monika 1ektua: @@ -555,13 +555,13 @@ image monika 1ektua: "monika 1ektua_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dktua_static" - 0.06 + 0.15 repeat image monika 1esa: @@ -833,13 +833,13 @@ image monika 1lftsc: "monika 1lftsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dftsc_static" - 0.06 + 0.15 repeat image monika 1lfu: @@ -1043,13 +1043,13 @@ image monika 1lktsc: "monika 1lktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dktsc_static" - 0.06 + 0.15 repeat image monika 1lsbsa: @@ -1477,13 +1477,13 @@ image monika 1rktsc: "monika 1rktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dktsc_static" - 0.06 + 0.15 repeat image monika 1rsbsd: @@ -1771,13 +1771,13 @@ image monika 1subftsb: "monika 1subftsb_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dubftsb_static" - 0.06 + 0.15 repeat image monika 1sublo: @@ -1869,13 +1869,13 @@ image monika 1sutsa: "monika 1sutsa_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dutsa_static" - 0.06 + 0.15 repeat image monika 1tfb: @@ -2415,13 +2415,13 @@ image monika 1wktsd: "monika 1wktsd_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 1dktsd_static" - 0.06 + 0.15 repeat image monika 1wua: @@ -2667,13 +2667,13 @@ image monika 2eftsu: "monika 2eftsu_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dftsu_static" - 0.06 + 0.15 repeat image monika 2efu: @@ -3073,13 +3073,13 @@ image monika 2ektsc: "monika 2ektsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dktsc_static" - 0.06 + 0.15 repeat image monika 2ektuc: @@ -3087,13 +3087,13 @@ image monika 2ektuc: "monika 2ektuc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dktuc_static" - 0.06 + 0.15 repeat image monika 2eku: @@ -3381,13 +3381,13 @@ image monika 2lftsc: "monika 2lftsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dftsc_static" - 0.06 + 0.15 repeat image monika 2lfu: @@ -3577,13 +3577,13 @@ image monika 2lktsc: "monika 2lktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dktsc_static" - 0.06 + 0.15 repeat image monika 2lsbsa: @@ -4109,13 +4109,13 @@ image monika 2rktsc: "monika 2rktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dktsc_static" - 0.06 + 0.15 repeat image monika 2rktsd: @@ -4123,13 +4123,13 @@ image monika 2rktsd: "monika 2rktsd_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dktsd_static" - 0.06 + 0.15 repeat image monika 2rsbsa: @@ -4221,13 +4221,13 @@ image monika 2subftsb: "monika 2subftsb_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dubftsb_static" - 0.06 + 0.15 repeat image monika 2sutsa: @@ -4235,13 +4235,13 @@ image monika 2sutsa: "monika 2sutsa_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dutsa_static" - 0.06 + 0.15 repeat image monika 2tfb: @@ -4683,13 +4683,13 @@ image monika 2wftsd: "monika 2wftsd_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dftsd_static" - 0.06 + 0.15 repeat image monika 2wftsw: @@ -4697,13 +4697,13 @@ image monika 2wftsw: "monika 2wftsw_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dftsw_static" - 0.06 + 0.15 repeat image monika 2wfw: @@ -4809,13 +4809,13 @@ image monika 2wktsc: "monika 2wktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dktsc_static" - 0.06 + 0.15 repeat image monika 2wktsd: @@ -4823,13 +4823,13 @@ image monika 2wktsd: "monika 2wktsd_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 2dktsd_static" - 0.06 + 0.15 repeat image monika 2wub: @@ -5047,13 +5047,13 @@ image monika 3eftsu: "monika 3eftsu_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 3dftsu_static" - 0.06 + 0.15 repeat image monika 3efu: @@ -5369,13 +5369,13 @@ image monika 3ektsc: "monika 3ektsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 3dktsc_static" - 0.06 + 0.15 repeat image monika 3esa: @@ -5627,13 +5627,13 @@ image monika 3lftsc: "monika 3lftsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 3dftsc_static" - 0.06 + 0.15 repeat image monika 3lfu: @@ -5823,13 +5823,13 @@ image monika 3lktsc: "monika 3lktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 3dktsc_static" - 0.06 + 0.15 repeat image monika 3lsbsa: @@ -6028,6 +6028,20 @@ image monika 3rka: 0.06 repeat +image monika 3rkb: + block: + "monika 3rkb_static" + block: + choice: + 3 + choice: + 5 + choice: + 7 + "monika 3dkb_static" + 0.06 + repeat + image monika 3rkbfsdla: block: "monika 3rkbfsdla_static" @@ -6173,13 +6187,13 @@ image monika 3rktsc: "monika 3rktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 3dktsc_static" - 0.06 + 0.15 repeat image monika 3rsa: @@ -6411,13 +6425,13 @@ image monika 3subftsb: "monika 3subftsb_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 3dubftsb_static" - 0.06 + 0.15 repeat image monika 3subsb: @@ -6467,13 +6481,13 @@ image monika 3sutsa: "monika 3sutsa_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 3dutsa_static" - 0.06 + 0.15 repeat image monika 3tfb: @@ -6966,17 +6980,3 @@ image monika 3wubsw: 0.06 repeat -image monika 3wud: - block: - "monika 3wud_static" - block: - choice: - 3 - choice: - 5 - choice: - 7 - "monika 3dud_static" - 0.06 - repeat - diff --git a/Monika After Story/game/sprite-chart-21.rpy b/Monika After Story/game/sprite-chart-21.rpy index bff917eaa7..2b6be38156 100644 --- a/Monika After Story/game/sprite-chart-21.rpy +++ b/Monika After Story/game/sprite-chart-21.rpy @@ -4,6 +4,20 @@ ## This was auto-generated by the the spritemaker tool ## ############################################################################### +image monika 3wud: + block: + "monika 3wud_static" + block: + choice: + 3 + choice: + 5 + choice: + 7 + "monika 3dud_static" + 0.06 + repeat + image monika 3wuo: block: "monika 3wuo_static" @@ -121,13 +135,13 @@ image monika 4eftsu: "monika 4eftsu_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 4dftsu_static" - 0.06 + 0.15 repeat image monika 4efu: @@ -331,13 +345,13 @@ image monika 4ektsc: "monika 4ektsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 4dktsc_static" - 0.06 + 0.15 repeat image monika 4esa: @@ -513,13 +527,13 @@ image monika 4lftsc: "monika 4lftsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 4dftsc_static" - 0.06 + 0.15 repeat image monika 4lfu: @@ -695,13 +709,13 @@ image monika 4lktsc: "monika 4lktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 4dktsc_static" - 0.06 + 0.15 repeat image monika 4lsbsa: @@ -1017,13 +1031,13 @@ image monika 4rktsc: "monika 4rktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 4dktsc_static" - 0.06 + 0.15 repeat image monika 4rsc: @@ -1101,13 +1115,13 @@ image monika 4subftsb: "monika 4subftsb_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 4dubftsb_static" - 0.06 + 0.15 repeat image monika 4sutsa: @@ -1115,13 +1129,13 @@ image monika 4sutsa: "monika 4sutsa_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 4dutsa_static" - 0.06 + 0.15 repeat image monika 4tfb: @@ -1395,13 +1409,13 @@ image monika 4wktsw: "monika 4wktsw_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 4dktsw_static" - 0.06 + 0.15 repeat image monika 4wua: @@ -2393,13 +2407,13 @@ image monika 6eftsc: "monika 6eftsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dftsc_static" - 0.06 + 0.15 repeat image monika 6eka: @@ -2645,13 +2659,13 @@ image monika 6ektsa: "monika 6ektsa_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dktsa_static" - 0.06 + 0.15 repeat image monika 6ektsc: @@ -2659,13 +2673,13 @@ image monika 6ektsc: "monika 6ektsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dktsc_static" - 0.06 + 0.15 repeat image monika 6ektuc: @@ -2673,13 +2687,13 @@ image monika 6ektuc: "monika 6ektuc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dktuc_static" - 0.06 + 0.15 repeat image monika 6ektud: @@ -2687,13 +2701,13 @@ image monika 6ektud: "monika 6ektud_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dktud_static" - 0.06 + 0.15 repeat image monika 6esa: @@ -2771,13 +2785,13 @@ image monika 6lftsc: "monika 6lftsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dftsc_static" - 0.06 + 0.15 repeat image monika 6lkc: @@ -2827,13 +2841,13 @@ image monika 6lktsc: "monika 6lktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dktsc_static" - 0.06 + 0.15 repeat image monika 6lsc: @@ -2967,13 +2981,13 @@ image monika 6rktsc: "monika 6rktsc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dktsc_static" - 0.06 + 0.15 repeat image monika 6rktuc: @@ -2981,13 +2995,13 @@ image monika 6rktuc: "monika 6rktuc_static" block: choice: - 3 + 9 choice: - 5 + 11 choice: - 7 + 12 "monika 6dktuc_static" - 0.06 + 0.15 repeat image monika 6ska: @@ -3326,6 +3340,20 @@ image monika 7eka: 0.06 repeat +image monika 7ekbsa: + block: + "monika 7ekbsa_static" + block: + choice: + 3 + choice: + 5 + choice: + 7 + "monika 7dkbsa_static" + 0.06 + repeat + image monika 7ekc: block: "monika 7ekc_static" @@ -3424,6 +3452,20 @@ image monika 7eub: 0.06 repeat +image monika 7euc: + block: + "monika 7euc_static" + block: + choice: + 3 + choice: + 5 + choice: + 7 + "monika 7duc_static" + 0.06 + repeat + image monika 7eud: block: "monika 7eud_static" @@ -3494,6 +3536,34 @@ image monika 7rksdlc: 0.06 repeat +image monika 7rsc: + block: + "monika 7rsc_static" + block: + choice: + 3 + choice: + 5 + choice: + 7 + "monika 7dsc_static" + 0.06 + repeat + +image monika 7tkc: + block: + "monika 7tkc_static" + block: + choice: + 3 + choice: + 5 + choice: + 7 + "monika 7dkc_static" + 0.06 + repeat + image monika 7tubfu: block: "monika 7tubfu_static" diff --git a/Monika After Story/game/sprite-chart-matrix.rpy b/Monika After Story/game/sprite-chart-matrix.rpy index 917a281c3c..16f58fe587 100644 --- a/Monika After Story/game/sprite-chart-matrix.rpy +++ b/Monika After Story/game/sprite-chart-matrix.rpy @@ -7,6 +7,39 @@ python early: # uncomment this if you want syntax highlighting support on vim #init -1 python: + class MASFilterException(Exception): + """ + General filter exceptions + """ + + def __init__(self, msg): + """ + Constructor + + IN: + msg - messgae to show + """ + self.msg = msg + + def __str__(self): + return self.msg + + + class MASInvalidFilterException(MASFilterException): + """ + Use when an invalid filter is being used + """ + + def __init__(self, bad_flt): + """ + Constructor + + IN: + bad_flt - the invalid filter being used + """ + self.msg = "'{0}' is not a valid filter".format(bad_flt) + + class MASFilterable(renpy.Displayable): """ Special displayable that adjusts its image based on filter. @@ -249,9 +282,7 @@ python early: for flt in flt_pairs: if flt in store.mas_sprites.FILTERS: # condition - args.append( - "store.mas_sprites.get_filter() == '{0}'".format(flt) - ) + args.append("mas_isCurrentFlt('{0}')".format(flt)) # image args.append(flt_pairs[flt]) @@ -265,9 +296,7 @@ python early: # only use the filtesr we have not already added if flt not in flt_pairs: # condition - args.append( - "store.mas_sprites.get_filter() == '{0}'".format(flt) - ) + args.append("mas_isCurrentFlt('{0}')".format(flt)) # image args.append(store.mas_sprites._gen_im(flt, def_img)) @@ -280,8 +309,23 @@ python early: return ConditionSwitch(*args) - # TODO: variation of FilterSwitch with just Day/Night options - # This would allow for generic day sprite and generic night sprite + def MASDayNightFilterSwitch(day_img, night_img): + """ + Builds a filter switch that changes image based on if the current flt + is a day/night filter. + + This does NOT apply any filters. + + IN: + day_img - image to return if day flt is in use + night_img - image to return if night flt is in use + + RETURNS: ConditionSwitch that works with day/night filters. + """ + return ConditionSwitch( + "store.mas_current_background.isFltDay()", day_img, + "store.mas_current_background.isFltNight()", night_img + ) def MASFilteredSprite(flt, img): @@ -297,6 +341,367 @@ python early: return renpy.easy.displayable(store.mas_sprites._gen_im(flt, img)) + def MASFallbackFilterDisplayable(**filter_pairs): + """ + Generates a dynamic displayable for filters that applies fallback + mechanics. If you don't need fallback mechanics, use the filter + switches. + + IN: + **filter_pairs - filter=val args to use. invalid filters are + ignored. + + RETURNS: Dynamic displayable that handles fallback filters + """ + return DynamicDisplayable( + mas_fbf_select, + MASFilterMapFallback(**filter_pairs) + ) + + + def MASFilterWeatherDisplayable(use_fb, **filter_pairs): + """ + Generates a dynamic displayable that maps filters to weathers for + arbitrary objects. + + This supports fallback-based value getting. For example: + Assuming there are 3 precip types (pt - 1, 2, 3) + and there are 4 filters (flt - A, B, C, D) + Fallback is denoted by fb + Precip type 1 is the DEFAULT precip type. + + Let's say the configuration for this MASFilterWeatherDisp is: + + flt A - pt 1, 2, 3 + flt B - pt 1, 3 - fb: A + flt C - pt 2 - fb: B + + flt B is a fallback for flt D, but flt D is NOT defined in this + MASFilterWeatherDisp. + + This is what would happen for combinations of filter, precip_type, + and use_fb settings: + + Current flt: A - Current pt: 2 - use_fb: any + In this case, flt A is defined and has a value for precip type 2. + The image at flt A, pt 2 is selected. + + Current flt: B - Current pt: 3 - use_fb: any + In this case, flt B is defined and has a value for pt 3. The image + at flt B, pt 3 is selected. + + Current flt: B - Current pt: 2 - use_fb: True + In this case, flt B does not have a precip_type of 2. Since we are + using fallback mode and flt A is a fallback of B, the image at + flt A, pt 2 is selected. + + Current flt: B - Current pt: 2 - use_fb: False + This is the same case as above except we are NOT using fallback + mode. In this case, the image at flt B, pt 1 is selected since it + is the default precip type. + + Current flt: C - Current pt: 3 - use_fb: True + In this case, flt C does not have a pt of 3. Since we are using + fallback mode and flt B is a fallback of C, the image at flt B, + pt 3 is selected. + + Current flt: C - Current pt: 3 - use_fb: False + This case would NEVER happen because an exception would be raised + on startup. If use_fb is False, a default precip type must be + defined for all filters. + + Current flt: D - Current pt: 3 - use_fb: True + In this case, flt D is not defined in this MASFilterWeatherDisp. + Since we are using fallback mode and flt B is a fallback of flt D, + the image at flt B, pt 3 is selected. + + Current flt: D - Current pt: 3 - use_fb: False + In thise, flt D is not defined. Even though we are NOT using + fallback mode, since flt B is a fallback of flt D, the image at + flt B, pt 3 is selected. + + Current flt: D - Current pt: 2 - use_fb: True + In this case, flt D is not defined, but flt B does NOT have an + image for pt 2. Since we are using fallback mode, flt B is a + fallback of flt D, and flt A is a fallback of flt B, the image at + flt A, pt 2 is selected. + + Current flt: D - Current pt: 2 - use_fb: False + In thise case, flt D is not defined. Since we are NOT using + fallback mode and flt B does NOT have an image for pt 2, the image + at flt B, pt 1 is selected as it is the default precip type. + + In terms of filter_pairs, if fallback-based getting is used, then + only the base filters need a PRECIP_TYPE_DEF to be valid for all + filter+weather type combinations. If normal getting is used, then + every filter will need PRECIP_TYPE_DEF to be set or else images may + not exist for a filter. This is checked on startup at init level 2. + + IN: + use_fb - set to True to use filter fallback-based value getting. + This will use the filter fallback mapping to retrieve values + for a precip_type if a selected filter does not have a value + for a precip_type. See above for an example. + False will use standard value getting. + **filter_pairs - fitler pairs to pass directly to + MASFilterWeatherMap + + RETURNS: DynamicDisplayable that respects Filters and weather. + """ + return MASFilterWeatherDisplayableCustom( + mas_fwm_select, + use_fb, + **filter_pairs + ) + + + def MASFilterWeatherDisplayableCustom(dyn_func, use_fb, **filter_pairs): + """ + Version of MASFilterWeatherDisplayable that accepts a custom function + to use instead of the default mas_fwm_select. + + See MASFilterWeatherDisplayable for explanations of this kind of disp. + + NOTE: in general, you should use MASFilterWeatherDisplayable. + """ + # build new MASFilterWeatherMap + new_map = MASFilterWeatherMap(**filter_pairs) + new_map.use_fb = use_fb + + # add to DB and set ID + new_id = store.mas_sprites.FW_ID + 1 + store.mas_sprites.FW_DB[new_id] = new_map + store.mas_sprites.FW_ID += 1 + + # return DynDisp + return DynamicDisplayable(dyn_func, new_map) + + +init -2 python: + + def mas_fwm_select(st, at, mfwm): + """ + Selects an image based on current filter and weather. + + IN: + st - renpy related + at - renpy related + mfwm - MASFilterWeatherMap to select image wtih + + RETURNS: dynamic disp output + """ + return ( + mfwm.fw_get( + store.mas_sprites.get_filter(), + store.mas_current_weather + ), + None + ) + + + def mas_fbf_select(st, at, mfmfb): + """ + Selects an image based on current filter, respecting fallback + mechanics. + + IN: + st - renpy related + at - renpy related + mfmfb - MASFilterMapFallback object to select image with + + RETURNS: dynamic disp output + """ + return mfmfb.get(store.mas_sprites.get_filter()), None + + +init 1 python in mas_sprites: + + def _verify_fwm_db(): + """ + Verifies that data in the FW_DB is correct. + MASFilterWeatherMaps are valid if: + 1. if the MFWM is fallback-based: + a) All filters provided include a fallback filter with + PRECIP_TYPE_DEF set. + 2. If the MFWM is standard: + a) All filters contain a PRECIP_TYPE_DEF set. + + Raises all errors. + """ + for mfwm_id, mfwm in FW_DB.iteritems(): + _verify_mfwm(mfwm_id, mfwm) + + + def _verify_mfwm(mfwm_id, mfwm): + """ + Verifies a MASFilterWeatherMap object. + + Raises all errors. + + IN: + mfwm_id - ID of the MASFilterWeatherMap object + mfwm - MASFilterWeatherMap object to verify + """ + if mwfm.use_fb: + # fallback-based + + # contains all flts that have a valid default fallback somewhere. + flt_defs = {} + + for flt in mfwm.flts(): + if not _mfwm_find_fb_def(mfwm, flt, flt_defs): + raise Exception( + ( + "MASFilterWeatherMap does not have default " + "precip type set in the fallback chain for " + "filter '{0}'. ID: {1}" + ).format( + flt, + mfwm_id + ) + ) + + else: + # standard + for flt in mfwm.flts(): + wmap = mfwm._raw_get(flt) + if wmap._raw_get(store.mas_weather.PRECIP_TYPE_DEF) is None: + raise Exception( + ( + "MASFilterWeatherMap does not have a default " + "precip type set for filter '{0}'. ID: {1}" + ).format( + flt, + mfwm_id + ) + ) + + + def _mfwm_find_fb_def(mfwm, flt, flt_defs): + """ + Finds fallbacks from a starting flt that are covered with a default + precip type. + + IN: + mfwm - MASFilterWeatherMap object we are checking + flt - filter we are checking for fallbacks + flt_defs - dict containing keys of filters that already have known + defaults in their fallback chains. + + OUT: + flt_defs - additional filters with known defaults are added to this + dict as we go through the fallback chain of the given flt. + + RETURNS: True if we found a non-None default precip type. False if not + """ + # check if filter has already been checked. + if flt in flt_defs: + return True + + # otherwise begin fallback chains + memo = {} + ord_memo = [] + curr_flt = _find_next_fb(flt, memo, ord_memo) + while not mfwm.has_def(curr_flt): + nxt_flt = _find_nxt_fb(curr_flt, memo, ord_memo) + + # if filter has not changed, we are done searching. + if nxt_flt == curr_flt: + # we should have returned True somewhere if we found a default. + return False + + if nxt_flt in flt_defs: + # this chain of fallbacks has already been resolved to a + # a default + flt_defs.update(memo) + return True + + curr_flt = nxt_flt + + # if we got here, then we found a default at the current flt. + # save the results and return True + flt_defs.update(memo) + return True + + + def _find_circ_fb(flt, memo): + """ + Tries to find circular fallbacks. + Assumes that the current flt has not been placed into memo yet. + + IN: + flt - flt we are checking + memo - dict of all flts we traversed here + + OUT: + memo - if False is returned, all keys in this memo are deemed to + be non-circular fallbacks. + + RETURNS: True if circular fallback is found, False otherwise + """ + # if we find this in the memo, we have a circular dependcy + if flt in memo: + return True + + # otherwise mark that we found this flt + memo[flt] = True + + # get next flt and check if we are done + next_flt = _rslv_flt(flt) + if next_flt == flt: + FLT_BASE[flt] = True + return False + + # recursively check flts + return _find_circ_fb(next_flt, memo) + + + def _find_next_fb(flt, memo, ordered_memo): + """ + Finds next filter and stores in memo and ordered memo + + IN: + flt - filter to find next filter for + + OUT: + memo - dict to add the next filter as a key if not None + ordered memo - list to append the next filter if not None + + RETURNS: the next filter, or None if no next filter. + """ + nxt_flt = _rslv_flt(flt) + if nxt_flt != flt: + memo[nxt_flt] = True + ordered_memo.append(nxt_flt) + + return nxt_flt + + + def _verify_flt_fb(): + """ + Verifies that there are no circular fallbacks in the filter + fallback dict. + + Raises an error if circular fallbacks are found + """ + non_cd = {} + + for flt in FLT_FB: + memo = {} + if _find_circ_fb(flt, memo): + raise Exception("filter '{0}' has a circular fallback".format( + flt + )) + + # otherwise good filter fallbac train + non_cd.update(memo) + + + # do verifications + _verify_flt_fb() + _verify_fwm_db() + + init -1 python in mas_sprites: __ignore_filters = True @@ -307,6 +712,14 @@ init -99 python in mas_sprites: import store import store.mas_utils as mas_utils + FW_ID = 1 + + FW_DB = {} + # internal collection of all MASFitlerWeatherDisplayable flt objects. + # (basically a dict of MASFilterWeatherMap objects) + # this is primarily used for verification later. + # IDs are generic integers. + # Filtering Framework # TODO: consider making the filter dict use Curryables so custom filters # can use non-Matrixcolor-based logic @@ -315,13 +728,29 @@ init -99 python in mas_sprites: # filter enums FLT_DAY = "day" FLT_NIGHT = "night" + FLT_SUNSET = "sunset" # filter dict FILTERS = { FLT_DAY: store.im.matrix.identity(), FLT_NIGHT: store.im.matrix.tint(0.59, 0.49, 0.55), + FLT_SUNSET: store.im.matrix.tint(0.93, 0.82, 0.78), + } + + # filter fallback dict + # key: filter + # value: filter that should be considered "base" filter + FLT_FB = { + FLT_SUNSET: FLT_DAY } + # contains all base filters. These are filtesr without a fallback. + # this is populated during filter fallback verification and is availabe + # for use AFTER init level 1. + # key: filter + # value: Ignored. + FLT_BASE = {} + # should be false until init -1 __ignore_filters = False @@ -330,7 +759,7 @@ init -99 python in mas_sprites: __flt_global = FLT_DAY - def add_filter(flt_enum, imx): + def add_filter(flt_enum, imx, base=None): """ Adds a filter to the global filters You can also use this to override built-in filters. @@ -338,10 +767,21 @@ init -99 python in mas_sprites: NOTE: if you plan to use this, please use it before init level -1 Filters beyond this level will be ignored. + NOn-pythonable filter names are ignored + IN: flt_enum - enum key to use as a filter. imx - image matrix to use as filter + base - filter to use as a backup for this filter. Any images + that are unable to be shown for flt_enum will be revert to + the base filter. + This should also be a FLT_ENUM. + This is checked to make sure it is a valid, preexisting enum, + so if chaining multiple bases, add them in order. + If None, no base is given for the flt. + (Default: None) """ + # check init if __ignore_filters: mas_utils.writelog( "[Warning!]: Cannot add filter '{0}' after init -1\n".format( @@ -350,6 +790,26 @@ init -99 python in mas_sprites: ) return + # check name arg able + if not _test_filter(flt_enum): + return + + # check base if given + if base is not None: + if base not in FILTERS: + mas_utils.writelog( + ( + "[Warning!]: Cannot add filter '{0}' with base '{1}', " + "base flt not exist\n" + ).format(flt_enum, base) + ) + return + + if not _test_filter(base): + return + + FLT_FB[flt_enum] = base + FILTERS[flt_enum] = imx @@ -369,6 +829,30 @@ init -99 python in mas_sprites: return __flt_global + def is_filter(flt): + """ + Checks if the given filter is a valid filter + + IN: + flt - filter enum to check + + RETURNS: True if valid filter, False if not + """ + return flt in FILTERS + + + def _rslv_flt(flt): + """ + Gets base filter for a flt. + + IN: + flt - flt to get base filter for + + RETURNS: base flt for flt, or the flt itself if no base + """ + return FLT_FB.get(flt, flt) + + def set_filter(flt_enum): """ Sets the current filter if it is valid. @@ -382,6 +866,32 @@ init -99 python in mas_sprites: __flt_global = flt_enum + def _test_filter(flt_enum): + """ + Checks if this filter enum can be a filter enum. + + Logs to mas log if there are errors + + IN: + flt_enum - filter enum to test + + RETURNS: True if passed test, False if not + """ + fake_context = {flt_enum: True} + try: + eval(flt_enum, fake_context) + return True + except: + mas_utils.writelog( + ( + "[Warning!]: Cannot add filter '{0}'. Name is not " + "python syntax friendly\n" + ).format(flt_enum) + ) + + return False + + init -98 python: # global filter-based functions @@ -2079,12 +2589,19 @@ init -4 python in mas_sprites: return rk_list -init -2 python: +init -10 python: class MASFilterMap(object): - """ + """SEALED The FilterMap connects filters to values + DO NOT EXTEND THIS CLASS. if you need similar functionality, just + make a wrapper class. There are functions in this class that will + cause crashes if used in unexpected contexts. + + NOTE: you can make filtermaps with non-string values, just dont + use the hash/eq/ne operators. + PROPERTIES: map - dict containg filter to string map key: filter constant @@ -2092,13 +2609,24 @@ init -2 python: """ import store.mas_sprites_json as msj - def __init__(self, default=None, **filter_pairs): + def __init__(self, + default=None, + cache=True, + verify=True, + **filter_pairs + ): """ Constructor IN: default - default code to apply to all filters (Default: None) + cache - True will cache the MFM, False will not + (Default: True) + verify - True will verify the filters, False will not. + NOTE: if passing False, use the verify function to + verify flts. + (Default: True) **filter_pairs - filter=val args to use. invalid filters are ignored. See FILTERS dict. Example: @@ -2106,7 +2634,12 @@ init -2 python: night="0" """ self.map = MASFilterMap.clean_flt_pairs(default, filter_pairs) - store.mas_sprites.MFM_CACHE[hash(self)] = self + + if verify: + self.verify() + + if cache: + store.mas_sprites.MFM_CACHE[hash(self)] = self def __eq__(self, other): """ @@ -2384,6 +2917,133 @@ init -2 python: return vals + def verify(self): + """ + Verifies all filters in this filter map. Raises exceptions if + bad filtesr are found. + """ + for flt in self.map: + if not store.mas_sprites.is_filter(flt): + raise MASInvalidFilterException(flt) + + + class MASFilterMapSimple(object): + """ + MASFilterMap for simple implementations, aka filter - value pairs + without type checks. + + Classes that need MASFilterMap should just extend this one as a base. + + This will NOT cache filter maps. + + PROPERTIES: + None + """ + + def __init__(self, **filter_pairs): + """ + Constructor + + Passes values directly to the internal MFM + + IN: + **filter_pairs - filter=val args to use. invalid filters + are ignored. + """ + self.__mfm = MASFilterMap( + default=None, + cache=False, + **filter_pairs + ) + + def flts(self): + """ + Gets all filter names in this filter map + + RETURNS: list of all filter names in this map + """ + return self.__mfm.map.keys() + + def get(self, flt, defval=None): + """ + See MASFilterMap.get + """ + return self.__mfm.get(flt, defval) + + def _mfm(self): + """ + Returns the intenral MASFilterMap. Only use if you know what you + are doing. + + RETURNS: MASFilterMap + """ + return self.__mfm + + + class MASFilterMapFallback(MASFilterMapSimple): + """ + MASFilterMap that respects fallback mechanics. + + Classes that need fallback behavior should just extend this one as a + base. + + This will NOT cache filter maps. + + PROPERTIES: + None + """ + + def __init__(self, **filter_pairs): + """ + Constructor + + IN: + **filter_pairs - filter=val args to use. invalid filters are + ignored. + """ + super(MASFilterMapFallback, self).__init__(**filter_pairs) + + def get(self, flt, defval=None): + """ + Gets value from map based on filter. This follows fallback + mechanics until a non-None value is found. + + IN: + flt - filter to lookup + defval - default value to return if no non-None value is + found after exhausting all fallbacks. + (Default: None) + + REUTRNS: value for a given filter + """ + value = self._raw_get(flt) + cur_flt = flt + while value is None: + nxt_flt = store.mas_sprites._rslv_flt(cur_flt) + + if nxt_flt == cur_flt: + # if flt doesnt change, we have reached teh bottom + return defval + + value = self._raw_get(nxt_flt) + cur_flt = nxt_flt + + return value + + def _raw_get(self, flt): + """ + Gets value from map based on filter + + IN: + flt - filter to lookup + + RETURNS: value for the given filter + """ + return super(MASFilterMapFallback, self).get(flt) + + +init -2 python: + def mas_drawmonika_rk( st, diff --git a/Monika After Story/game/sprite-chart.rpy b/Monika After Story/game/sprite-chart.rpy index 863d04a47a..03c032ffbc 100644 --- a/Monika After Story/game/sprite-chart.rpy +++ b/Monika After Story/game/sprite-chart.rpy @@ -1806,356 +1806,671 @@ init -5 python in mas_sprites: # retrieved from a Dress Up Renpy Cookbook # https://lemmasoft.renai.us/forums/viewtopic.php?f=51&t=30643 -init -3 python: -# import renpy.store as store -# import renpy.exports as renpy # we need this so Ren'Py properly handles rollback with classes -# from operator import attrgetter # we need this for sorting items - import math - from collections import namedtuple - # Monika character base - class MASMonika(renpy.store.object): - import store.mas_sprites as mas_sprites +init -10 python: - # CONSTANTS - PRE_ACS = 0 # PRE ACCESSORY (before body) - MID_ACS = 1 # MID ACCESSORY (between face and front arms) - PST_ACS = 2 # post accessory (after front arms) - BBH_ACS = 3 # betweeen Body and Back Hair accessory - BFH_ACS = 4 # between Body and Front Hair accessory - AFH_ACS = 5 # between face and front hair accessory - BBA_ACS = 6 # between body and back arms - MAB_ACS = 7 # between middle arms and boobs - BSE_ACS = 8 # between base and clothes - ASE_ACS = 9 # between base arms and clothes - BAT_ACS = 10 # between base arms and table - MAT_ACS = 11 # between middle arms and table + class MASHighlightMap(object): + """SEALED + Maps arbitrary keys to objects - # valid rec layers - # NOTE: this MUST be in the same order as save_state/load_State - # NOTE: do not remove layers. Because of load state, we can only - # ignore layers if needed, but not remove. BEtter to just replace - # a layer than remove it. - REC_LAYERS = ( - PRE_ACS, - BBH_ACS, - BFH_ACS, - AFH_ACS, - MID_ACS, - PST_ACS, - BBA_ACS, - MAB_ACS, - BSE_ACS, - ASE_ACS, - BAT_ACS, - MAT_ACS, - ) + DO NOT EXTEND THIS CLASS. If you need similar functionality, make a + wrapper around this. This class contains functions that may crash + when used in an unexpected context. + + NOTE: values dont have to be MASFilterMAP objects, but certain + functions will fail if not. - # split layers - SPL_LAYERS = ( - BSE_ACS, - ASE_ACS, - ) + NOTE: this can iterated over to retrieve all objects in here + EXCEPT for the default. - def __init__(self): + PROPERTIES: + None. Use provided functions to manipulate the map. + """ + __KEY_ALL = "*" + + def __init__(self, keys, default=None): """ Constructor - """ - self.name="Monika" - self.haircut="default" - self.haircolor="default" - self.skin_hue=0 # monika probably doesn't have different skin color - self.lipstick="default" # i guess no lipstick - self.clothes = mas_clothes_def # default clothes is school outfit - self.hair = mas_hair_def # default hair is the usual whtie ribbon - #self.table = mas_table_def # default table + IN: + keys - iterable of keys that we are allowed to use. + NOTE: the default catch all key of "*" (KEY_ALL) does NOt + need to be in here. + default - value to use as the default/catch all object. This + is assigned to the KEY_ALL key. + """ + self.__map = { self.__KEY_ALL: default } - # list of lean blacklisted accessory names currently equipped - self.lean_acs_blacklist = [] + # remove keyall + key_list = list(keys) + if self.__KEY_ALL in key_list: + key_list.remove(self.__KEY_ALL) - # accesories to be rendereed before anything - self.acs_pre = [] + # remove duplicate + self.__valid_keys = tuple(set(key_list)) - # accessories to be rendered after back hair, before body - self.acs_bbh = [] + def __iter__(self): + """ + Iterator object (generator) + """ + for key in self.__valid_keys: + item = self.get(key) + if item is not None: + yield item - # accessories to be rendered after base body, before body clothes - self.acs_bse = [] + def _add_key(self, new_key, new_value=None): + """ + Adds a key to the valid keys list. Also adds a value if desired + NOTE: this is not intended to be done wildly. Please do not + make a habit of adding keys after construction. + NOTE: do not use this to add values. if the given key already + exists, the value is ignored. - # accessories to be rendered after body, before back arms - self.acs_bba = [] + IN: + new_key - new key to add + new_value - new value to associate with this key + (Default: None) + """ + if new_key not in self.__valid_keys and new_key != self.__KEY_ALL: + key_list = list(self.__valid_keys) + key_list.append(new_key) + self.__valid_keys = tuple(key_list) + self.add(new_key, new_value) - # accessories to be rendered after base arms, before arm clothes - self.acs_ase = [] + def add(self, key, value): + """ + Adds value to map. + NOTE: type is enforced here. If the given item is None + it is ignored. + NOTE: this will NOT set the default even if KEY_ALL is passed in - # accessories to be rendered after back arms, before table - self.acs_bat = [] + IN: + key - key to store item to + value - value to add + if None is passed, this is equivalent to clear + """ + if value is None: + self.clear(key) + return - # accessories to be rendered after table, before middle arms - self.acs_mat = [] + if key == self.__KEY_ALL or key not in self.__valid_keys: + return - # accessories to be rendered after middle arms before boobs - self.acs_mab = [] + # otherwise valid to add + self.__map[key] = value - # accessories to be rendered after boobs, before front hair - self.acs_bfh = [] + def apply(self, mapping): + """ + Applies the given dict mapping to this MASHighlightMap. + NOTE: will not add invalid keys. - # accessories to be rendered after fornt hair, before face - self.acs_afh = [] + IN: + mapping - dict of the following format: + key: valid key for this map + value: value to add + """ + for key in mapping: + self.add(key, mapping[key]) - # accessories to be rendered after face, before front arms - self.acs_mid = [] + def clear(self, key): + """ + Clears value with the given key. + NOTE: will NOT clear the default even if KEY_ALL is passed in - # accessories to be rendered last - self.acs_pst = [] + IN: + key - key to clear with + """ + if key != self.__KEY_ALL and key in self.__map: + self.__map.pop(key) - self.hair_hue=0 # hair color? + @staticmethod + def clear_hl_mapping(hl_mpm_data): + """ + Clears hl mapping in the given hl data object. AKA: Sets the + hl mapping portion of a pre-MHM MPM to {}. - # setup acs dict - self.acs = { - self.PRE_ACS: self.acs_pre, - self.MID_ACS: self.acs_mid, - self.PST_ACS: self.acs_pst, - self.BBH_ACS: self.acs_bbh, - self.BFH_ACS: self.acs_bfh, - self.AFH_ACS: self.acs_afh, - self.BBA_ACS: self.acs_bba, - self.MAB_ACS: self.acs_mab, - self.BSE_ACS: self.acs_bse, - self.ASE_ACS: self.acs_ase, - self.BAT_ACS: self.acs_bat, - self.MAT_ACS: self.acs_mat, - } + NOTE: this should only be used with the MASPoseMap._transform + function with MASAccessory - # use this dict to map acs IDs with which acs list they are in. - # this will increase speed of removal and checking. - self.acs_list_map = {} + IN: + hl_mpm_data - hl data set in a MASPoseMap. - # LOCK VARS - # True if we should block any changes to hair - self.lock_hair = False + RETURNS: hl data to set in a MASPoseMap. + """ + if hl_mpm_data is None: + return None + return (hl_mpm_data[0], {}) - # True if we should block any chnages to clothes - self.lock_clothes = False + @staticmethod + def convert_mpm(hl_keys, mpm): + """ + Converts hl mappings in a MASPoseMap to MASHighlightMAp objects - # True if we should block any changes to cas - self.lock_acs = False + IN: + hl_keys - highlight keys to use + mpm - MASPoseMap object to convert + """ + if mpm is None: + return - # set to True to allow ACS overriding - self._override_rec_layer = False + # first, build modify pargs map + pargs = {} + for param in MASPoseMap.P_PARAM_NAMES: + hl_data = mpm.get(MASPoseMap.pn2lp(param), None) + if hl_data is None: + pargs[param] = None + else: + pargs[param] = MASHighlightMap.create_from_mapping( + hl_keys, + hl_data[0], + hl_data[1] + ) - # the current table/chair combo we - # NOTE: this is associated with monika because we could definitely - # have multiple table/chairs in a MASBackground. - # NOTE: replacing this is untested, but will be required because - # of highlights - self.tablechair = MASTableChair("def", "def") + # then modify the mpm + mpm._modify(**pargs) - def __get_acs(self, acs_type): + @staticmethod + def create_from_mapping(hl_keys, hl_def, hl_mapping): """ - Returns the accessory list associated with the given type + Creates a MASHighlightMap using keys/default/mapping IN: - acs_type - the accessory type to get + hl_keys - list of keys to use + hl_def - default highlight to use. Can be None + hl_mapping - mapping to use. - RETURNS: - accessory list, or None if the given acs_type is not valid + RETURNS: created MASHighlightMap """ - return self.acs.get(acs_type, None) - - def _determine_poses(self, lean, arms): + mhm = MASHighlightMap(hl_keys, default=hl_def) + mhm.apply(hl_mapping) + return mhm + + def fltget(self, key, flt, defval=None): """ - determines the lean/pose/hair/baked data for monika based on - the requested lean and arms + Combines getting from here and getting the resulting MASFilterMap + object. IN: - lean - requested lean - arms - requested arms + key - key to get from this map + flt - filter to get from associated MASFilterMap, if found + defval - default value to return if no flt value could be + found. + (Default: None) - RETURNS: tuple of the following format: - [0] - lean to use - [1] - leanpose to use - [2] - arms to use - [3] - hair to use - [4] - base arms to use - [5] - pose arms to use + RETURNS: value in the MASFilterMap associated with the given + flt, using the MASFilterMap associated with the given key. + or defval if no valid MASfilterMap or value found. """ - # first check black list - if store.mas_sprites.should_disable_lean(lean, arms, self): - # set lean to None if its on the blacklist - # NOTE: this function checks pose_maps - lean = None - arms = "steepling" + mfm = self.get(key) + if mfm is None: + return defval - # fallback adjustments: - if self.hair.pose_map.is_fallback(): - arms, lean = self.hair.get_fallback(arms, lean) + return mfm.get(flt, defval=defval) - if self.clothes.pose_map.is_fallback(): - arms, lean = self.clothes.get_fallback(arms, lean) + @staticmethod + def fromJSON(json_obj, msg_log, ind_lvl, hl_keys): + """ + Builds hl data from JSON data - # get the mapped hair for the current clothes - if self.clothes.has_hair_map(): - hair = store.mas_sprites.HAIR_MAP.get( - self.clothes.get_hair(self.hair.name), - mas_hair_def - ) + IN: + json_obj - JSON object to parse + ind_lvl - indentation level + NOTE: this function handles loading/success log so + do NOT increment indent when passing in + hl_keys - expected keys of this highlight map - else: - hair = self.hair + OUT: + msg_log - list to add messagse to - # combined pose with lean for efficient - if lean is not None: - leanpose = lean + "|" + arms - else: - leanpose = arms + RETURNS: hl_data, ready to passed split and passed into + create_from_mapping. Tuple: + [0] - default MASFilterMap object + [1] - dict: + key: hl_key + value: MASFilterMap object + or None if no data, False if failure in parsing occured + """ + # first log loading + msg_log.append(( + store.mas_sprites_json.MSG_INFO_T, + ind_lvl, + store.mas_sprites_json.MHM_LOADING + )) - # MASPoseArms rules: - # 1. If the pose_arms property in clothes is None, then we assume - # that the clothes follows the base pose rules. - # 2. If the pose_arms property exists, and the - # corresponding pose in that map is None, then we assume that - # the clothes does NOT have layers for this pose. - # select MASPoseArms for baes and outfit + # parse the data + hl_data = MASHighlightMap._fromJSON_hl_data( + json_obj, + msg_log, + ind_lvl + 1, + hl_keys + ) - # NOTE: we can always assume that base arms exist - # NOTE: but we will default steepling justin case - base_arms = [ - store.mas_sprites.base_arms.get(arm_id) - for arm_id in store.mas_sprites.base_mpm.get(leanpose, [7]) - ] - if self.clothes.pose_arms is None: - pose_arms = base_arms + # check fai/succ + if hl_data is False: + # loggin should take care of this already + return False - else: - pose_arms = self.clothes.pose_arms.getflp(leanpose) + # log success + msg_log.append(( + store.mas_sprites_json.MSG_INFO_T, + ind_lvl, + store.mas_sprites_json.MHM_SUCCESS + )) - return (lean, leanpose, arms, hair, base_arms, pose_arms) + return hl_data - def _same_state_acs(self, a1, a2): + def get(self, key): """ - Compares given acs lists as acs objects - - NOTE: order does not matter + Gets value wth the given key. IN: - a1 - list of acs objects to compare - a2 - list of acs objects to compare + key - key of item to get - RETURNS: True if the same, False if not + RETURNS: MASFilterMap object, or None if not found """ - # quick chec - if len(a1) != len(a2): - return False - - # make a list of names for comparison - a2_names = [acs.name for acs in a2] - - # now do comparison - same_count = 0 - for a1_acs in a1: - if a1_acs.name in a2_names: - same_count += 1 - else: - return False + if key in self.__map: + return self.__map[key] - return len(a2_names) == same_count + # otherwise return default + return self.getdef() - def _same_state_acs_prims(self, a1, a2): + def getdef(self): """ - Compares given acs lists as primitive data. - - NOTE: order does not matter - - IN: - a1 - list of acs names to compare - a2 - list of acs names to compare + Gets the default value - RETURNS: True if the same, False if not + RETURNS: MASFilterMap object, or NOne if not found """ - # quick check - if len(a1) != len(a2): - return False + return self.__map.get(self.__KEY_ALL, None) - same_count = 0 - for a1_name in a1: - if a1_name in a2: - same_count += 1 - else: - return False + def keys(self): + """ + gets keys in this map - return len(a2) == same_count + RETURNS: tuple of keys + """ + return self.__valid_keys - def _same_state(self, data): + @staticmethod + def o_fltget(mhm, key, flt, defval=None): """ - Compares the given state as objects + Similar to fltget, but on a MASHighlightMap object. + NOTE: does None checks of mhm and flt. IN: - data - previous object state + mhm - MASHighlightMap object to run fltget on + key - key to get MASFilterMap from mhm + flt - filter to get from associated MASFilterMap + defval - default value to return if no flt value could be found + (Default: None) - RETURNS: True if the same, False if not + RETURNS: See fltget """ - # object data is sprite objects, but we compare names - - # get current monikas state - curr_state = self.save_state(True, True, True, False) - - # first compare size - if len(data) != len(curr_state): - return False - - # clothes - if data[0].name != curr_state[0].name: - return False - - # hair - if data[1].name != curr_state[1].name: - return False - - # acs lists - for index in range(2, len(data)): - if not self._same_state_acs(data[index], curr_state[index]): - return False + if mhm is None or flt is None: + return defval - return True + return mhm.fltget(key, flt, defval=defval) - def _same_state_prims(self, data): + def setdefault(self, value): """ - Compares the given state as primitives + Sets the default value IN: - data - previous primitive state - - RETURNS: True if the same, False if not + value - value to use as default """ - # primtiive data is stored as names - - # get current monika's state - curr_state = self.save_state(True, True, True, True) - - # first compare state size - if len(data) != len(curr_state): - return False - - # clothes - if data[0] != curr_state[0]: - return False + if value is None or isinstance(value, MASFilterMap): + self.__map[self.__KEY_ALL] = value - # hair - if data[1] != curr_state[1]: - return False - # acs lists - for index in range(2, len(data)): - if not self._same_state_acs_prims(data[index], curr_state[index]): - return False +init -3 python: +# import renpy.store as store +# import renpy.exports as renpy # we need this so Ren'Py properly handles rollback with classes +# from operator import attrgetter # we need this for sorting items + import math + from collections import namedtuple - return True + # Monika character base + class MASMonika(renpy.store.object): + import store.mas_sprites as mas_sprites - def _load(self, - _clothes_name, - _hair_name, + # CONSTANTS + PRE_ACS = 0 # PRE ACCESSORY (before body) + MID_ACS = 1 # MID ACCESSORY (between face and front arms) + PST_ACS = 2 # post accessory (after front arms) + BBH_ACS = 3 # betweeen Body and Back Hair accessory + BFH_ACS = 4 # between Body and Front Hair accessory + AFH_ACS = 5 # between face and front hair accessory + BBA_ACS = 6 # between body and back arms + MAB_ACS = 7 # between middle arms and boobs + BSE_ACS = 8 # between base and clothes + ASE_ACS = 9 # between base arms and clothes + BAT_ACS = 10 # between base arms and table + MAT_ACS = 11 # between middle arms and table + + # valid rec layers + # NOTE: this MUST be in the same order as save_state/load_State + # NOTE: do not remove layers. Because of load state, we can only + # ignore layers if needed, but not remove. BEtter to just replace + # a layer than remove it. + REC_LAYERS = ( + PRE_ACS, + BBH_ACS, + BFH_ACS, + AFH_ACS, + MID_ACS, + PST_ACS, + BBA_ACS, + MAB_ACS, + BSE_ACS, + ASE_ACS, + BAT_ACS, + MAT_ACS, + ) + + # split layers + SPL_LAYERS = ( + BSE_ACS, + ASE_ACS, + ) + + def __init__(self): + """ + Constructor + """ + self.name="Monika" + self.haircut="default" + self.haircolor="default" + self.skin_hue=0 # monika probably doesn't have different skin color + self.lipstick="default" # i guess no lipstick + + self.clothes = mas_clothes_def # default clothes is school outfit + self.hair = mas_hair_def # default hair is the usual whtie ribbon + #self.table = mas_table_def # default table + + # list of lean blacklisted accessory names currently equipped + self.lean_acs_blacklist = [] + + # accesories to be rendereed before anything + self.acs_pre = [] + + # accessories to be rendered after back hair, before body + self.acs_bbh = [] + + # accessories to be rendered after base body, before body clothes + self.acs_bse = [] + + # accessories to be rendered after body, before back arms + self.acs_bba = [] + + # accessories to be rendered after base arms, before arm clothes + self.acs_ase = [] + + # accessories to be rendered after back arms, before table + self.acs_bat = [] + + # accessories to be rendered after table, before middle arms + self.acs_mat = [] + + # accessories to be rendered after middle arms before boobs + self.acs_mab = [] + + # accessories to be rendered after boobs, before front hair + self.acs_bfh = [] + + # accessories to be rendered after fornt hair, before face + self.acs_afh = [] + + # accessories to be rendered after face, before front arms + self.acs_mid = [] + + # accessories to be rendered last + self.acs_pst = [] + + self.hair_hue=0 # hair color? + + # setup acs dict + self.acs = { + self.PRE_ACS: self.acs_pre, + self.MID_ACS: self.acs_mid, + self.PST_ACS: self.acs_pst, + self.BBH_ACS: self.acs_bbh, + self.BFH_ACS: self.acs_bfh, + self.AFH_ACS: self.acs_afh, + self.BBA_ACS: self.acs_bba, + self.MAB_ACS: self.acs_mab, + self.BSE_ACS: self.acs_bse, + self.ASE_ACS: self.acs_ase, + self.BAT_ACS: self.acs_bat, + self.MAT_ACS: self.acs_mat, + } + + # use this dict to map acs IDs with which acs list they are in. + # this will increase speed of removal and checking. + self.acs_list_map = {} + + # LOCK VARS + # True if we should block any changes to hair + self.lock_hair = False + + # True if we should block any chnages to clothes + self.lock_clothes = False + + # True if we should block any changes to cas + self.lock_acs = False + + # set to True to allow ACS overriding + self._override_rec_layer = False + + # the current table/chair combo we + # NOTE: this is associated with monika because we could definitely + # have multiple table/chairs in a MASBackground. + # NOTE: replacing this is untested, but will be required because + # of highlights + self.tablechair = MASTableChair("def", "def") + + def __get_acs(self, acs_type): + """ + Returns the accessory list associated with the given type + + IN: + acs_type - the accessory type to get + + RETURNS: + accessory list, or None if the given acs_type is not valid + """ + return self.acs.get(acs_type, None) + + def _determine_poses(self, lean, arms): + """ + determines the lean/pose/hair/baked data for monika based on + the requested lean and arms + + IN: + lean - requested lean + arms - requested arms + + RETURNS: tuple of the following format: + [0] - lean to use + [1] - leanpose to use + [2] - arms to use + [3] - hair to use + [4] - base arms to use + [5] - pose arms to use + """ + # first check black list + if store.mas_sprites.should_disable_lean(lean, arms, self): + # set lean to None if its on the blacklist + # NOTE: this function checks pose_maps + lean = None + arms = "steepling" + + # fallback adjustments: + if self.hair.pose_map.is_fallback(): + arms, lean = self.hair.get_fallback(arms, lean) + + if self.clothes.pose_map.is_fallback(): + arms, lean = self.clothes.get_fallback(arms, lean) + + # get the mapped hair for the current clothes + if self.clothes.has_hair_map(): + hair = store.mas_sprites.HAIR_MAP.get( + self.clothes.get_hair(self.hair.name), + mas_hair_def + ) + + else: + hair = self.hair + + # combined pose with lean for efficient + if lean is not None: + leanpose = lean + "|" + arms + else: + leanpose = arms + + # MASPoseArms rules: + # 1. If the pose_arms property in clothes is None, then we assume + # that the clothes follows the base pose rules. + # 2. If the pose_arms property exists, and the + # corresponding pose in that map is None, then we assume that + # the clothes does NOT have layers for this pose. + # select MASPoseArms for baes and outfit + + # NOTE: we can always assume that base arms exist + # NOTE: but we will default steepling justin case + base_arms = [ + store.mas_sprites.base_arms.get(arm_id) + for arm_id in store.mas_sprites.base_mpm.get(leanpose, [7]) + ] + if self.clothes.pose_arms is None: + pose_arms = base_arms + + else: + pose_arms = self.clothes.pose_arms.getflp(leanpose) + + return (lean, leanpose, arms, hair, base_arms, pose_arms) + + def _same_state_acs(self, a1, a2): + """ + Compares given acs lists as acs objects + + NOTE: order does not matter + + IN: + a1 - list of acs objects to compare + a2 - list of acs objects to compare + + RETURNS: True if the same, False if not + """ + # quick chec + if len(a1) != len(a2): + return False + + # make a list of names for comparison + a2_names = [acs.name for acs in a2] + + # now do comparison + same_count = 0 + for a1_acs in a1: + if a1_acs.name in a2_names: + same_count += 1 + else: + return False + + return len(a2_names) == same_count + + def _same_state_acs_prims(self, a1, a2): + """ + Compares given acs lists as primitive data. + + NOTE: order does not matter + + IN: + a1 - list of acs names to compare + a2 - list of acs names to compare + + RETURNS: True if the same, False if not + """ + # quick check + if len(a1) != len(a2): + return False + + same_count = 0 + for a1_name in a1: + if a1_name in a2: + same_count += 1 + else: + return False + + return len(a2) == same_count + + def _same_state(self, data): + """ + Compares the given state as objects + + IN: + data - previous object state + + RETURNS: True if the same, False if not + """ + # object data is sprite objects, but we compare names + + # get current monikas state + curr_state = self.save_state(True, True, True, False) + + # first compare size + if len(data) != len(curr_state): + return False + + # clothes + if data[0].name != curr_state[0].name: + return False + + # hair + if data[1].name != curr_state[1].name: + return False + + # acs lists + for index in range(2, len(data)): + if not self._same_state_acs(data[index], curr_state[index]): + return False + + return True + + def _same_state_prims(self, data): + """ + Compares the given state as primitives + + IN: + data - previous primitive state + + RETURNS: True if the same, False if not + """ + # primtiive data is stored as names + + # get current monika's state + curr_state = self.save_state(True, True, True, True) + + # first compare state size + if len(data) != len(curr_state): + return False + + # clothes + if data[0] != curr_state[0]: + return False + + # hair + if data[1] != curr_state[1]: + return False + + # acs lists + for index in range(2, len(data)): + if not self._same_state_acs_prims(data[index], curr_state[index]): + return False + + return True + + def _load(self, + _clothes_name, + _hair_name, _acs_pre_names, _acs_bbh_names, _acs_bse_names, @@ -2697,1647 +3012,1340 @@ init -3 python: IN: exprop - extended property to check - RETURNS: True if wearing clothes with the exprop, False if not - """ - return self.clothes.hasprop(exprop) - - - def is_wearing_hair_with_exprop(self, exprop): - """ - Checks if we are currently wearing hair with the given exprop - - IN: - exprop - extend property to check - - RETURNS: True if wearing hair with the exprop, False if not - """ - return self.hair.hasprop(exprop) - - - def is_wearing_ribbon(self): - """ - Checks if we are currently wearing a ribbon or ribbon-like ACS - - RETURNS: True if wearing ACS with ribbon type or ACS with - ribbon-like ex prop - """ - return ( - self.is_wearing_acs_type("ribbon") - or self.is_wearing_acs_with_exprop("ribbon-like") - ) - - def load(self, startup=False): - """ - Loads hair/clothes/accessories from persistent. - - IN: - startup - True if loading on start, False if not - When True, we dont respesct locking - (Default: False) - """ - self._load( - store.persistent._mas_monika_clothes, - store.persistent._mas_monika_hair, - store.persistent._mas_acs_pre_list, - store.persistent._mas_acs_bbh_list, - store.persistent._mas_acs_bse_list, - store.persistent._mas_acs_bba_list, - store.persistent._mas_acs_ase_list, - store.persistent._mas_acs_mab_list, - store.persistent._mas_acs_bfh_list, - store.persistent._mas_acs_afh_list, - store.persistent._mas_acs_mid_list, - store.persistent._mas_acs_pst_list, - store.persistent._mas_acs_bat_list, - store.persistent._mas_acs_mat_list, - startup=startup - ) - - # TODO: consider adding startup to this - def load_state(self, _data, as_prims=False): - """ - Loads clothes/hair/acs from a tuple data format that was saved - using the save_state function. - - IN: - _data - data to load from. tuple of the following format: - [0]: clothes data - [1]: hair data - [2]: pre acs data - [3]: bbh acs data - [4]: bfh acs data - [5]: afh acs data - [6]: mid acs data - [7]: pst acs data - [8]: bba acs data - [9]: mab acs data - [10]: bse acs data - [11]: ase acs data - [12]: bat acs data - [13]: mat acs data - as_prims - True if this data was saved as primitive data types, - false if as objects - (Default: False) - """ - if as_prims: - # for prims, we can just call an existing function - self._load(*_data) - return - - # otherwise, we need to set things ourselves - # clothes and hair - self.change_outfit(_data[0], _data[1]) - - # acs - for index in range(len(self.REC_LAYERS)): - self._load_acs_obj(_data[index+2], self.REC_LAYERS[index]) - - def reset_all(self, by_user=None): - """ - Resets all of monika - - IN: - by_user - True if this action was mandated by user, False if - not. If None, we do NOT set force vars. - (Default: None) - """ - self.reset_clothes(by_user) - self.reset_hair(by_user) - self.remove_all_acs() - - def remove_acs(self, accessory): - """ - Removes the given accessory. this uses the map to determine where - the accessory is located. - - IN: - accessory - accessory to remove - """ - self.remove_acs_in( - accessory, - self.acs_list_map.get(accessory.name, None) - ) - - - def remove_acs_exprop(self, exprop): - """ - Removes all ACS of given exprop. - - IN: - exprop - exprop to check for - """ - for acs_name in self.acs_list_map.keys(): - _acs = store.mas_sprites.ACS_MAP.get(acs_name, None) - if _acs and _acs.hasprop(exprop): - self.remove_acs_in(_acs, self.acs_list_map[acs_name]) - - - def remove_acs_mux(self, mux_types): - """ - Removes all ACS with a mux type in the given list. - - IN: - mux_types - list of acs_types to remove from acs - """ - for acs_name in self.acs_list_map.keys(): - _acs = store.mas_sprites.ACS_MAP.get(acs_name, None) - if _acs and _acs.acs_type in mux_types: - self.remove_acs_in(_acs, self.acs_list_map[acs_name]) - - - def remove_acs_in(self, accessory, acs_type): - """ - Removes the given accessory from the given accessory list type - - IN: - accessory - accessory to remove - acs_type - ACS type - """ - if self.lock_acs: - return - - acs_list = self.__get_acs(acs_type) - temp_space = { - "acs_list": acs_list, - } - - if acs_list is not None and accessory in acs_list: - - # run pre exit point code - store.mas_sprites.acs_rm_exit_pre_change( - temp_space, - self, - accessory, - acs_type - ) - - # abort removal if we were told to abort - if temp_space.get("abort", False): - return - - # run programming point - accessory.exit(self) - - # run post exit code - store.mas_sprites.acs_rm_exit_pst_change( - temp_space, - self, - accessory, - acs_type - ) - - # cleanup blacklist - if accessory.name in self.lean_acs_blacklist: - self.lean_acs_blacklist.remove(accessory.name) - - # cleanup mapping - if accessory.name in self.acs_list_map: - self.acs_list_map.pop(accessory.name) - - # now remove - acs_list.remove(accessory) - - def remove_all_acs(self): - """ - Removes all accessories from all accessory lists - """ - for rec_layer in self.REC_LAYERS: - self.remove_all_acs_in(rec_layer) - - def remove_all_acs_in(self, acs_type): - """ - Removes all accessories from the given accessory type - - IN: - acs_type - ACS type to remove all - """ - if self.lock_acs: - return - - if acs_type in self.acs: - # need to clear blacklisted - for acs in self.acs[acs_type]: - # run programming point - acs.exit(self) - - # cleanup blacklist - if acs.name in self.lean_acs_blacklist: - self.lean_acs_blacklist.remove(acs.name) - - # remove from mapping - if acs.name in self.acs_list_map: - self.acs_list_map.pop(acs.name) - - self.acs[acs_type] = list() - - - def reset_clothes(self, by_user=None): - """ - Resets clothing to default - - IN: - by_user - True if this action was mandated by user, False if - not. If None, then we do NOT set force clothed vars - (Default: None) + RETURNS: True if wearing clothes with the exprop, False if not """ - self.change_clothes(mas_clothes_def, by_user) + return self.clothes.hasprop(exprop) - def reset_hair(self, by_user=None): + def is_wearing_hair_with_exprop(self, exprop): """ - Resets hair to default + Checks if we are currently wearing hair with the given exprop IN: - by_user - True if this action was mandated by user, False if - not. If None, then we do NOT set forced hair vars - (Default: None) + exprop - extend property to check + + RETURNS: True if wearing hair with the exprop, False if not """ - self.change_hair(mas_hair_def, by_user) + return self.hair.hasprop(exprop) - def reset_outfit(self, by_user=None): + def is_wearing_ribbon(self): """ - Resetse clothing and hair to default + Checks if we are currently wearing a ribbon or ribbon-like ACS - IN: - by_user - True if this action was mandated by user, False if - not. If None, then we do NOT set forced vars - (Default: None) + RETURNS: True if wearing ACS with ribbon type or ACS with + ribbon-like ex prop """ - self.reset_clothes(by_user) - self.reset_hair(by_user) + return ( + self.is_wearing_acs_type("ribbon") + or self.is_wearing_acs_with_exprop("ribbon-like") + ) - def restore(self, _data, as_prims=False): + def load(self, startup=False): """ - Restores monika to a previous state. This will reset outfit and - clear ACS before loading. + Loads hair/clothes/accessories from persistent. IN: - _data - see load_state - as_prims - see load_state + startup - True if loading on start, False if not + When True, we dont respesct locking + (Default: False) """ - self.reset_outfit() - self.remove_all_acs() - self.load_state(_data, as_prims=as_prims) + self._load( + store.persistent._mas_monika_clothes, + store.persistent._mas_monika_hair, + store.persistent._mas_acs_pre_list, + store.persistent._mas_acs_bbh_list, + store.persistent._mas_acs_bse_list, + store.persistent._mas_acs_bba_list, + store.persistent._mas_acs_ase_list, + store.persistent._mas_acs_mab_list, + store.persistent._mas_acs_bfh_list, + store.persistent._mas_acs_afh_list, + store.persistent._mas_acs_mid_list, + store.persistent._mas_acs_pst_list, + store.persistent._mas_acs_bat_list, + store.persistent._mas_acs_mat_list, + startup=startup + ) - def save(self, force_hair=False, force_clothes=False, force_acs=False): + # TODO: consider adding startup to this + def load_state(self, _data, as_prims=False): """ - Saves hair/clothes/acs to persistent + Loads clothes/hair/acs from a tuple data format that was saved + using the save_state function. IN: - force_hair - True means we force hair saving even if - stay_on_start is False - (Default: False) - force_clothes - True means we force clothes saving even if - stay_on_start is False - (Default: False) - force_acs - True means we force acs saving even if - stay_on_start is False + _data - data to load from. tuple of the following format: + [0]: clothes data + [1]: hair data + [2]: pre acs data + [3]: bbh acs data + [4]: bfh acs data + [5]: afh acs data + [6]: mid acs data + [7]: pst acs data + [8]: bba acs data + [9]: mab acs data + [10]: bse acs data + [11]: ase acs data + [12]: bat acs data + [13]: mat acs data + as_prims - True if this data was saved as primitive data types, + false if as objects (Default: False) """ - # hair and clothes - if force_hair or self.hair.stay_on_start: - store.persistent._mas_monika_hair = self.hair.name + if as_prims: + # for prims, we can just call an existing function + self._load(*_data) + return - if force_clothes or self.clothes.stay_on_start: - store.persistent._mas_monika_clothes = self.clothes.name + # otherwise, we need to set things ourselves + # clothes and hair + self.change_outfit(_data[0], _data[1]) # acs - store.persistent._mas_acs_pre_list = self._save_acs( - self.PRE_ACS, - force_acs - ) - store.persistent._mas_acs_bbh_list = self._save_acs( - self.BBH_ACS, - force_acs - ) - store.persistent._mas_acs_bse_list = self._save_acs( - self.BSE_ACS, - force_acs - ) - store.persistent._mas_acs_bba_list = self._save_acs( - self.BBA_ACS, - force_acs - ) - store.persistent._mas_acs_ase_list = self._save_acs( - self.ASE_ACS, - force_acs - ) - store.persistent._mas_acs_mab_list = self._save_acs( - self.MAB_ACS, - force_acs - ) - store.persistent._mas_acs_bfh_list = self._save_acs( - self.BFH_ACS, - force_acs - ) - store.persistent._mas_acs_afh_list = self._save_acs( - self.AFH_ACS, - force_acs - ) - store.persistent._mas_acs_mid_list = self._save_acs( - self.MID_ACS, - force_acs - ) - store.persistent._mas_acs_pst_list = self._save_acs( - self.PST_ACS, - force_acs - ) - + for index in range(len(self.REC_LAYERS)): + self._load_acs_obj(_data[index+2], self.REC_LAYERS[index]) - def same_state(self, data, as_prims=False): + def reset_all(self, by_user=None): """ - compares if the given state is the same as current monika + Resets all of monika IN: - data - data to compare - as_prims - True if prims, False if not - - RETURNS: True if same state, False if not + by_user - True if this action was mandated by user, False if + not. If None, we do NOT set force vars. + (Default: None) """ - if as_prims: - return self._same_state_prims(data) - - return self._same_state(data) + self.reset_clothes(by_user) + self.reset_hair(by_user) + self.remove_all_acs() - def save_state(self, - force_hair=False, - force_clothes=False, - force_acs=False, - as_prims=False - ): + def remove_acs(self, accessory): """ - Saves hair/clothes/acs to a tuple data format that can be loaded - later using the load_state function. + Removes the given accessory. this uses the map to determine where + the accessory is located. IN: - force_hair - True means force hair saving even if stay_on_start - is False. If False and stay_on_start is False, the default - hair will be returned. - (Default: False) - force_clothes - True meanas force clothes saving even if - stay_on_start is False. If False and stay_on_start is - False, the default clothes will be returned. - (Default: False) - force_acs - True means force acs saving even if stay_on_start - is False. At minimum, this will be an empty list. - (Default: False) - as_prims - True means to save the data as primitive types - for persistent saving. False will save the data as - objects. - (Default: False) - - RETURNS tuple of the following format: - [0]: clothes data (Default: mas_clothes_def) - [1]: hair data (Default: mas_hair_def) - [2]: pre acs data (Default: []) - [3]: bbh acs data (Default: []) - [4]: bfh acs data (Default: []) - [5]: afh acs data (Default: []) - [6]: mid acs data (Default: []) - [7]: pst acs data (Default: []) - [8]: bba acs data (Default: []) - [9]: mab acs data (Default: []) - [10]: bse acs data (Default: []) - [11]: ase acs data (Default: []) - [12]: bat acs data (Default: []) - [13]: mat acs data (Default: []) + accessory - accessory to remove """ - # determine which clothes to save - if force_clothes or self.clothes.stay_on_start: - cloth_data = self.clothes - else: - cloth_data = mas_clothes_def - - # determine which hair to save - if force_hair or self.hair.stay_on_start: - hair_data = self.hair - else: - hair_data = mas_hair_def + self.remove_acs_in( + accessory, + self.acs_list_map.get(accessory.name, None) + ) - state_data = [] - # save clothes and hair - if as_prims: - state_data.extend((cloth_data.name, hair_data.name)) - else: - state_data.extend((cloth_data, hair_data)) + def remove_acs_exprop(self, exprop): + """ + Removes all ACS of given exprop. - # now acs - for rec_layer in self.REC_LAYERS: - if as_prims: - state_data.append(self._save_acs(rec_layer, force_acs)) - else: - state_data.append(self._save_acs_obj(rec_layer, force_acs)) + IN: + exprop - exprop to check for + """ + for acs_name in self.acs_list_map.keys(): + _acs = store.mas_sprites.ACS_MAP.get(acs_name, None) + if _acs and _acs.hasprop(exprop): + self.remove_acs_in(_acs, self.acs_list_map[acs_name]) - # finally return results - return tuple(state_data) - def wear_acs(self, acs): + def remove_acs_mux(self, mux_types): """ - Wears the given accessory in that accessory's recommended - spot, as defined by the accessory. + Removes all ACS with a mux type in the given list. IN: - acs - accessory to wear + mux_types - list of acs_types to remove from acs """ - self.wear_acs_in(acs, acs.get_rec_layer()) + for acs_name in self.acs_list_map.keys(): + _acs = store.mas_sprites.ACS_MAP.get(acs_name, None) + if _acs and _acs.acs_type in mux_types: + self.remove_acs_in(_acs, self.acs_list_map[acs_name]) - def wear_acs_in(self, accessory, acs_type): - """ - Wears the given accessory - NOTE: will not allow mismatching layers, unless overrides are - enabled. + def remove_acs_in(self, accessory, acs_type): + """ + Removes the given accessory from the given accessory list type IN: - accessory - accessory to wear - acs_type - layer to wear the acs in. + accessory - accessory to remove + acs_type - ACS type """ - if self.lock_acs or accessory.name in self.acs_list_map: - # we never wear dupes + if self.lock_acs: return - # if the given layer does not match rec layer, force the correct - # layer unless override - if ( - acs_type != accessory.get_rec_layer() - and not self._override_rec_layer - ): - acs_type = accessory.get_rec_layer() - - # verify aso_type is valid for the desired acs layer - # unless override - if not self._override_rec_layer: - if acs_type in (self.BSE_ACS, self.ASE_ACS): - valid_aso_type = MASAccessoryBase.ASO_SPLIT - else: - valid_aso_type = MASAccessoryBase.ASO_REG - - if accessory.aso_type != valid_aso_type: - return - acs_list = self.__get_acs(acs_type) temp_space = { "acs_list": acs_list, } - if acs_list is not None and accessory not in acs_list: + if acs_list is not None and accessory in acs_list: - # run pre exclusion code - store.mas_sprites.acs_wear_mux_pre_change( + # run pre exit point code + store.mas_sprites.acs_rm_exit_pre_change( temp_space, self, accessory, acs_type ) - # abort wearing if we were told to abort + # abort removal if we were told to abort if temp_space.get("abort", False): return - # run mutual exclusion for acs - if accessory.mux_type is not None: - self.remove_acs_mux(accessory.mux_type) - - # run post exclusion code - store.mas_sprites.acs_wear_mux_pst_change( - temp_space, - self, - accessory, - acs_type - ) - - # now insert the acs - mas_insertSort(acs_list, accessory, MASAccessory.get_priority) - - # add to mapping - self.acs_list_map[accessory.name] = acs_type - - if accessory.name in mas_sprites.lean_acs_blacklist: - self.lean_acs_blacklist.append(accessory.name) + # run programming point + accessory.exit(self) - # run pre entry - store.mas_sprites.acs_wear_entry_pre_change( + # run post exit code + store.mas_sprites.acs_rm_exit_pst_change( temp_space, self, accessory, acs_type ) - # run programming point for acs - accessory.entry(self) + # cleanup blacklist + if accessory.name in self.lean_acs_blacklist: + self.lean_acs_blacklist.remove(accessory.name) - # run post entry - store.mas_sprites.acs_wear_entry_pst_change( - temp_space, - self, - accessory, - acs_type - ) + # cleanup mapping + if accessory.name in self.acs_list_map: + self.acs_list_map.pop(accessory.name) - def wear_acs_pre(self, acs): - """DEPRECATED - Wears the given accessory in the pre body accessory mode + # now remove + acs_list.remove(accessory) - IN: - acs - accessory to wear + def remove_all_acs(self): """ - self.wear_acs_in(acs, self.PRE_ACS) - - - def wear_acs_bbh(self, acs): - """DEPRECATED - Wears the given accessory in the post back hair accessory loc - - IN: - acs - accessory to wear + Removes all accessories from all accessory lists """ - self.wear_acs_in(acs, self.BBH_ACS) - + for rec_layer in self.REC_LAYERS: + self.remove_all_acs_in(rec_layer) - def wear_acs_bfh(self, acs): - """DEPRECATED - Wears the given accessory in the pre front hair accesory log + def remove_all_acs_in(self, acs_type): + """ + Removes all accessories from the given accessory type IN: - acs - accessory to wear + acs_type - ACS type to remove all """ - self.wear_acs_in(acs, self.BFH_ACS) + if self.lock_acs: + return + if acs_type in self.acs: + # need to clear blacklisted + for acs in self.acs[acs_type]: + # run programming point + acs.exit(self) - def wear_acs_afh(self, acs): - """DEPRECATED - Wears the given accessory in the between front hair and arms - acs log + # cleanup blacklist + if acs.name in self.lean_acs_blacklist: + self.lean_acs_blacklist.remove(acs.name) - IN: - acs - accessory to wear - """ - self.wear_acs_in(acs, self.AFH_ACS) + # remove from mapping + if acs.name in self.acs_list_map: + self.acs_list_map.pop(acs.name) + self.acs[acs_type] = list() - def wear_acs_mid(self, acs): - """DEPRECATED - Wears the given accessory in the mid body acessory mode - IN: - acs - acessory to wear + def reset_clothes(self, by_user=None): """ - self.wear_acs_in(acs, self.MID_ACS) - - - def wear_acs_pst(self, acs): - """DEPRECATED - Wears the given accessory in the post body accessory mode + Resets clothing to default IN: - acs - accessory to wear + by_user - True if this action was mandated by user, False if + not. If None, then we do NOT set force clothed vars + (Default: None) """ - self.wear_acs_in(acs, self.PST_ACS) - - - # hues, probably not going to use these -# hair_hue1 = im.matrix([ 1, 0, 0, 0, 0, -# 0, 1, 0, 0, 0, -# 0, 0, 1, 0, 0, -# 0, 0, 0, 1, 0 ]) -# hair_hue2 = im.matrix([ 3.734, 0, 0, 0, 0, -# 0, 3.531, 0, 0, 0, -# 0, 0, 1.375, 0, 0, -# 0, 0, 0, 1, 0 ]) -# hair_hue3 = im.matrix([ 3.718, 0, 0, 0, 0, -# 0, 3.703, 0, 0, 0, -# 0, 0, 3.781, 0, 0, -# 0, 0, 0, 1, 0 ]) -# hair_hue4 = im.matrix([ 3.906, 0, 0, 0, 0, -# 0, 3.671, 0, 0, 0, -# 0, 0, 3.375, 0, 0, -# 0, 0, 0, 1, 0 ]) -# skin_hue1 = hair_hue1 -# skin_hue2 = im.matrix([ 0.925, 0, 0, 0, 0, -# 0, 0.840, 0, 0, 0, -# 0, 0, 0.806, 0, 0, -# 0, 0, 0, 1, 0 ]) -# skin_hue3 = im.matrix([ 0.851, 0, 0, 0, 0, -# 0, 0.633, 0, 0, 0, -# 0, 0, 0.542, 0, 0, -# 0, 0, 0, 1, 0 ]) -# -# hair_huearray = [hair_hue1,hair_hue2,hair_hue3,hair_hue4] -# -# skin_huearray = [skin_hue1,skin_hue2,skin_hue3] - - - class MASTableChair(object): - """ - Representation of an available table + chair combo. + self.change_clothes(mas_clothes_def, by_user) - PROPERTIES: - has_shadow - True if this table has a shadow - table - table tag associated with this table chair combo - This will be used in bulding the table sprite string - chair - chair tag associated with tihs table chair combo - This will be used in building the chair sprite string - hl_map - MASHighlightMap associated with this table chair - keys: - t - table - ts - table shadow - Used instead of table whenever shadow - is applied. - c - chair - """ - from store.mas_sprites import TC_GEN, PREFIX_TABLE, SHADOW_SUFFIX, NIGHT_SUFFIX - __MHM_KEYS = ("t", "ts", "c") - def __init__(self, table, chair, hl_data=None): + def reset_hair(self, by_user=None): """ - constructor + Resets hair to default IN: - table - table tag to use - chair - chair tag to use - hl_data - highlight mapping data. format: - [0] - default highilght to use. Pass in None to not set - a default. - [1] - highlight mapping to use. Format: - key: "t" for table, "c" for chair - value: MASFilterMap object, or None if no highlight - pass in None if no highlights shoudl be used at all + by_user - True if this action was mandated by user, False if + not. If None, then we do NOT set forced hair vars + (Default: None) """ - self.table = table - self.chair = chair - self.has_shadow = False - self.prepare() - - if hl_data is None: - self.hl_map = None - else: - self.hl_map = MASHighlightMap.create_from_mapping( - self.__MHM_KEYS, - hl_data[0], - hl_data[1] - ) + self.change_hair(mas_hair_def, by_user) - def prepare(self): - """ - Prepares this table chair combo by checking for shadow. - """ - self.has_shadow = ( - renpy.loadable(self.TC_GEN.format( - self.PREFIX_TABLE, - self.table, - self.SHADOW_SUFFIX, - "" - )) - ) - def setTable(self, new_table): + def reset_outfit(self, by_user=None): """ - sets the table tag and checks shadow + Resetse clothing and hair to default IN: - new_table - the new table tag to set - if an invalid string or NOne is passed in, we reset to - default + by_user - True if this action was mandated by user, False if + not. If None, then we do NOT set forced vars + (Default: None) """ - if new_table: - self.table = new_table - else: - self.table = "def" + self.reset_clothes(by_user) + self.reset_hair(by_user) - self.prepare() + def restore(self, _data, as_prims=False): + """ + Restores monika to a previous state. This will reset outfit and + clear ACS before loading. - - class MASArm(object): - """ - Representation of an "Arm" + IN: + _data - see load_state + as_prims - see load_state + """ + self.reset_outfit() + self.remove_all_acs() + self.load_state(_data, as_prims=as_prims) - Each Arm consists of of a layered combination: - NOTE: we re using spaced layers so we can insert more if needed. - 0 - bottom layer. after body-0 but before table. Primary bottom layer. - 5 - middle layer. after table but before body-1. - 10 - top layer. after body-1. + def save(self, force_hair=False, force_clothes=False, force_acs=False): + """ + Saves hair/clothes/acs to persistent - PROPERTIES: - tag - the tag string of this arm - layer_map - mapping of layer exiestence to image code - key: layer code - value: True if exists, False if not - hl_map - MASHighlightMap with layer code keys - """ + IN: + force_hair - True means we force hair saving even if + stay_on_start is False + (Default: False) + force_clothes - True means we force clothes saving even if + stay_on_start is False + (Default: False) + force_acs - True means we force acs saving even if + stay_on_start is False + (Default: False) + """ + # hair and clothes + if force_hair or self.hair.stay_on_start: + store.persistent._mas_monika_hair = self.hair.name - LAYER_BOT = "0" - LAYER_MID = "5" - LAYER_TOP = "10" + if force_clothes or self.clothes.stay_on_start: + store.persistent._mas_monika_clothes = self.clothes.name - __MPA_KEYS = (LAYER_BOT, LAYER_MID, LAYER_TOP) + # acs + store.persistent._mas_acs_pre_list = self._save_acs( + self.PRE_ACS, + force_acs + ) + store.persistent._mas_acs_bbh_list = self._save_acs( + self.BBH_ACS, + force_acs + ) + store.persistent._mas_acs_bse_list = self._save_acs( + self.BSE_ACS, + force_acs + ) + store.persistent._mas_acs_bba_list = self._save_acs( + self.BBA_ACS, + force_acs + ) + store.persistent._mas_acs_ase_list = self._save_acs( + self.ASE_ACS, + force_acs + ) + store.persistent._mas_acs_mab_list = self._save_acs( + self.MAB_ACS, + force_acs + ) + store.persistent._mas_acs_bfh_list = self._save_acs( + self.BFH_ACS, + force_acs + ) + store.persistent._mas_acs_afh_list = self._save_acs( + self.AFH_ACS, + force_acs + ) + store.persistent._mas_acs_mid_list = self._save_acs( + self.MID_ACS, + force_acs + ) + store.persistent._mas_acs_pst_list = self._save_acs( + self.PST_ACS, + force_acs + ) - def __init__(self, tag, layer_map, hl_data=None): + + def same_state(self, data, as_prims=False): """ - Constructor + compares if the given state is the same as current monika IN: - tag - tag string for this arm - layer_map - layer map to use - key: image layer code - value: True if exists, False if not - hl_data - highlght map data. tuple of the following formaT: - [0] - default MASFilterMap to use. Pass in None to - not set a default highlight - [1] - highlight mapping to use. Format: - key: image layer code - value: MASFilterMap object, or None if no highlight - pass in None if no highlights should be used at all + data - data to compare + as_prims - True if prims, False if not + + RETURNS: True if same state, False if not """ - self.tag = tag - self.clean_map(layer_map) - self.layer_map = layer_map - - if hl_data is not None: - self.hl_map = MASHighlightMap.create_from_mapping( - self.__MPA_KEYS, - hl_data[0], - hl_data[1] - ) - else: - self.hl_map = None + if as_prims: + return self._same_state_prims(data) - def __build_loadstrs_hl(self, prefix, layer_code): + return self._same_state(data) + + def save_state(self, + force_hair=False, + force_clothes=False, + force_acs=False, + as_prims=False + ): """ - Builds load strings for a hlight from a given map + Saves hair/clothes/acs to a tuple data format that can be loaded + later using the load_state function. IN: - prefix - prefix to apply - layer_code - layer code to generate loadstrings for + force_hair - True means force hair saving even if stay_on_start + is False. If False and stay_on_start is False, the default + hair will be returned. + (Default: False) + force_clothes - True meanas force clothes saving even if + stay_on_start is False. If False and stay_on_start is + False, the default clothes will be returned. + (Default: False) + force_acs - True means force acs saving even if stay_on_start + is False. At minimum, this will be an empty list. + (Default: False) + as_prims - True means to save the data as primitive types + for persistent saving. False will save the data as + objects. + (Default: False) - RETURNS: list of lists of strings representing image path for all - highlights for a layer code + RETURNS tuple of the following format: + [0]: clothes data (Default: mas_clothes_def) + [1]: hair data (Default: mas_hair_def) + [2]: pre acs data (Default: []) + [3]: bbh acs data (Default: []) + [4]: bfh acs data (Default: []) + [5]: afh acs data (Default: []) + [6]: mid acs data (Default: []) + [7]: pst acs data (Default: []) + [8]: bba acs data (Default: []) + [9]: mab acs data (Default: []) + [10]: bse acs data (Default: []) + [11]: ase acs data (Default: []) + [12]: bat acs data (Default: []) + [13]: mat acs data (Default: []) """ - if self.hl_map is None: - return [] + # determine which clothes to save + if force_clothes or self.clothes.stay_on_start: + cloth_data = self.clothes + else: + cloth_data = mas_clothes_def - mfm = self.hl_map.get(layer_code) - if mfm is None: - return [] + # determine which hair to save + if force_hair or self.hair.stay_on_start: + hair_data = self.hair + else: + hair_data = mas_hair_def - # generate for all unique - return [ - prefix + [ - store.mas_sprites.HLITE_SUFFIX, - hlc, - store.mas_sprites.FILE_EXT - ] - for hlc in mfm.unique_values() - ] + state_data = [] - @classmethod - def _fromJSON(cls, json_obj, msg_log, ind_lvl, build_class): - """ - Builds a MASArm object based on the given JSON format of it + # save clothes and hair + if as_prims: + state_data.extend((cloth_data.name, hair_data.name)) + else: + state_data.extend((cloth_data, hair_data)) - IN: - json_obj - JSON object to parse - ind_lvl - indent level - build_class - actual MASArm derivative to build + # now acs + for rec_layer in self.REC_LAYERS: + if as_prims: + state_data.append(self._save_acs(rec_layer, force_acs)) + else: + state_data.append(self._save_acs_obj(rec_layer, force_acs)) - OUT: - msg_log - list to save messages to + # finally return results + return tuple(state_data) - RETURNS: MASArm instance built with the JSON, or None if failed + def wear_acs(self, acs): """ - params = {} - # start with required params - # tag - # layers - if not store.mas_sprites_json._validate_params( - json_obj, - params, - { - "tag": (str, store.mas_sprites_json._verify_str), - "layers": (str, store.mas_sprites_json._verify_str), - }, - True, - msg_log, - ind_lvl - ): - return None + Wears the given accessory in that accessory's recommended + spot, as defined by the accessory. - # additional processing for the layers to ensure valid data - layer_map = {} - for layer in params.pop("layers").split("^"): - if layer in cls.__MPA_KEYS: - layer_map[layer] = True + IN: + acs - accessory to wear + """ + self.wear_acs_in(acs, acs.get_rec_layer()) - else: - # invalid layer - # warn and ignore - msg_log.append(( - store.mas_sprites_json.MSG_WARN_T, - ind_lvl, - store.mas_sprites_json.MA_INVALID_LAYER.format(layer) - )) + def wear_acs_in(self, accessory, acs_type): + """ + Wears the given accessory - # NOTE: ERR if we have no layers for an arm. This is useless to - # include if no layers. - if len(layer_map) == 0: - msg_log.append(( - store.mas_sprites_json.MSG_ERR_T, - ind_lvl, - store.mas_sprites_json.MA_NO_LAYERS - )) - return None + NOTE: will not allow mismatching layers, unless overrides are + enabled. - # add layers to params - params["layer_map"] = layer_map + IN: + accessory - accessory to wear + acs_type - layer to wear the acs in. + """ + if self.lock_acs or accessory.name in self.acs_list_map: + # we never wear dupes + return - # now check highlight - if store.mas_sprites_json.HLITE in json_obj: - - # parse - vhl_data = {} - - if store.mas_sprites_json._validate_highlight( - json_obj, - vhl_data, - msg_log, - ind_lvl, - layer_map.keys() - ): - # success - hl_data = vhl_data.get("hl_data", None) - if hl_data is not None: - params["hl_data"] = hl_data + # if the given layer does not match rec layer, force the correct + # layer unless override + if ( + acs_type != accessory.get_rec_layer() + and not self._override_rec_layer + ): + acs_type = accessory.get_rec_layer() + # verify aso_type is valid for the desired acs layer + # unless override + if not self._override_rec_layer: + if acs_type in (self.BSE_ACS, self.ASE_ACS): + valid_aso_type = MASAccessoryBase.ASO_SPLIT else: - # failure - return None + valid_aso_type = MASAccessoryBase.ASO_REG - # warn for extra properties - for extra_prop in json_obj: - msg_log.append(( - store.mas_sprites_json.MSG_WARN_T, - ind_lvl, - store.mas_sprites_json.EXTRA_PROP.format(extra_prop) - )) + if accessory.aso_type != valid_aso_type: + return - # we now should have all the data we need to build - return build_class(**params) + acs_list = self.__get_acs(acs_type) + temp_space = { + "acs_list": acs_list, + } - def build_loadstrs(self, prefix): - """ - Builds loadstrs for this arm + if acs_list is not None and accessory not in acs_list: - IN: - prefix - prefix to apply to the loadstrs - should be list of strings + # run pre exclusion code + store.mas_sprites.acs_wear_mux_pre_change( + temp_space, + self, + accessory, + acs_type + ) - RETURNS: list of lists of strings representing the load strings - for this arm, + highlights - """ - if not self.tag: - return [] + # abort wearing if we were told to abort + if temp_space.get("abort", False): + return - loadstrs = [] + # run mutual exclusion for acs + if accessory.mux_type is not None: + self.remove_acs_mux(accessory.mux_type) - # add arms based on layer code - for layer_code in self.__MPA_KEYS: - # NOTE: we only add an arm + hlite if it exists for a layer - # code + # run post exclusion code + store.mas_sprites.acs_wear_mux_pst_change( + temp_space, + self, + accessory, + acs_type + ) - if self.layer_map.get(layer_code, False): + # now insert the acs + mas_insertSort(acs_list, accessory, MASAccessory.get_priority) - # generate image - new_img = prefix + [ - self.tag, - store.mas_sprites.ART_DLM, - str(layer_code) - ] + # add to mapping + self.acs_list_map[accessory.name] = acs_type - # add with extension - loadstrs.append(new_img + [store.mas_sprites.FILE_EXT]) + if accessory.name in mas_sprites.lean_acs_blacklist: + self.lean_acs_blacklist.append(accessory.name) - # generate highlights - loadstrs.extend(self.__build_loadstrs_hl( - new_img, - layer_code - )) + # run pre entry + store.mas_sprites.acs_wear_entry_pre_change( + temp_space, + self, + accessory, + acs_type + ) - return loadstrs + # run programming point for acs + accessory.entry(self) - def clean_map(self, mapping): - """ - cleans the given map, ensuring it contains only valid layer - keys. No errors are logged. + # run post entry + store.mas_sprites.acs_wear_entry_pst_change( + temp_space, + self, + accessory, + acs_type + ) + + def wear_acs_pre(self, acs): + """DEPRECATED + Wears the given accessory in the pre body accessory mode IN: - mapping - mapping to clean + acs - accessory to wear """ - for map_key in mapping.keys(): - if map_key not in self.__MPA_KEYS: - mapping.pop(map_key) + self.wear_acs_in(acs, self.PRE_ACS) - def get(self, layer_code, prefix=[]): - """ - Generates tag name + suffixes to use for - a given layer code. A tag name is a tuple of strings - that can be joined to build the full tag name, - Tag Names do NOT include file extensions - IN: - layer_code - layer code to fetch tag names for - prefix - prefix to apply to the tag string if desired - (Default: []) + def wear_acs_bbh(self, acs): + """DEPRECATED + Wears the given accessory in the post back hair accessory loc - RETURNS: list consisting of the tag strings and - appropriate suffixes + IN: + acs - accessory to wear """ - if not self.tag: - return [] + self.wear_acs_in(acs, self.BBH_ACS) - # should this item exist on tihs layer code? - if not self.layer_map.get(layer_code, False): - return [] - # should exist, generate the primary tag string - return prefix + [ - self.tag, - store.mas_sprites.ART_DLM, - str(layer_code) - ] + def wear_acs_bfh(self, acs): + """DEPRECATED + Wears the given accessory in the pre front hair accesory log - def gethlc(self, layer_code, flt, defval=None): + IN: + acs - accessory to wear """ - Gets highlight code. + self.wear_acs_in(acs, self.BFH_ACS) - IN: - layer_code - layer to get highlight for - flt - filter to get highilght for - defval - default value to return - (Default: None) - RETURNS: highlight code, or None if no highligiht - """ - return MASHighlightMap.o_fltget( - self.hl_map, - layer_code, - flt, - defval - ) + def wear_acs_afh(self, acs): + """DEPRECATED + Wears the given accessory in the between front hair and arms + acs log - def hl_keys(self): + IN: + acs - accessory to wear """ - Returns hl keys for a MASArm + self.wear_acs_in(acs, self.AFH_ACS) - RETURNS: tuple of hl keys - """ - return self.__MPA_KEYS - @classmethod - def hl_keys_c(cls): - """ - Class method version of hl_keys + def wear_acs_mid(self, acs): + """DEPRECATED + Wears the given accessory in the mid body acessory mode - RETURNS: tuple of hl keys + IN: + acs - acessory to wear """ - return cls.__MPA_KEYS + self.wear_acs_in(acs, self.MID_ACS) - class MASArmBoth(MASArm): - """ - Representation of an "arm" that actually covers both arms + def wear_acs_pst(self, acs): + """DEPRECATED + Wears the given accessory in the post body accessory mode + + IN: + acs - accessory to wear + """ + self.wear_acs_in(acs, self.PST_ACS) - This currently has no additional behavior. - It's primary use is to act as a type of MASArm - PROPERTIES: - see MASArm - """ - pass + # hues, probably not going to use these +# hair_hue1 = im.matrix([ 1, 0, 0, 0, 0, +# 0, 1, 0, 0, 0, +# 0, 0, 1, 0, 0, +# 0, 0, 0, 1, 0 ]) +# hair_hue2 = im.matrix([ 3.734, 0, 0, 0, 0, +# 0, 3.531, 0, 0, 0, +# 0, 0, 1.375, 0, 0, +# 0, 0, 0, 1, 0 ]) +# hair_hue3 = im.matrix([ 3.718, 0, 0, 0, 0, +# 0, 3.703, 0, 0, 0, +# 0, 0, 3.781, 0, 0, +# 0, 0, 0, 1, 0 ]) +# hair_hue4 = im.matrix([ 3.906, 0, 0, 0, 0, +# 0, 3.671, 0, 0, 0, +# 0, 0, 3.375, 0, 0, +# 0, 0, 0, 1, 0 ]) +# skin_hue1 = hair_hue1 +# skin_hue2 = im.matrix([ 0.925, 0, 0, 0, 0, +# 0, 0.840, 0, 0, 0, +# 0, 0, 0.806, 0, 0, +# 0, 0, 0, 1, 0 ]) +# skin_hue3 = im.matrix([ 0.851, 0, 0, 0, 0, +# 0, 0.633, 0, 0, 0, +# 0, 0, 0.542, 0, 0, +# 0, 0, 0, 1, 0 ]) +# +# hair_huearray = [hair_hue1,hair_hue2,hair_hue3,hair_hue4] +# +# skin_huearray = [skin_hue1,skin_hue2,skin_hue3] - class MASArmLeft(MASArm): + class MASTableChair(object): """ - Representation of a left arm. - - Overrides prefix-based functions + Representation of an available table + chair combo. PROPERTIES: - see MASArm + has_shadow - True if this table has a shadow + table - table tag associated with this table chair combo + This will be used in bulding the table sprite string + chair - chair tag associated with tihs table chair combo + This will be used in building the chair sprite string + hl_map - MASHighlightMap associated with this table chair + keys: + t - table + ts - table shadow - Used instead of table whenever shadow + is applied. + c - chair """ + from store.mas_sprites import TC_GEN, PREFIX_TABLE, SHADOW_SUFFIX, NIGHT_SUFFIX + __MHM_KEYS = ("t", "ts", "c") - def build_loadstrs(self, prefix): + def __init__(self, table, chair, hl_data=None): """ - Generates loadstrs for this arm + constructor IN: - prefix - prefix to apply to the loadstrs - list of strings + table - table tag to use + chair - chair tag to use + hl_data - highlight mapping data. format: + [0] - default highilght to use. Pass in None to not set + a default. + [1] - highlight mapping to use. Format: + key: "t" for table, "c" for chair + value: MASFilterMap object, or None if no highlight + pass in None if no highlights shoudl be used at all + """ + self.table = table + self.chair = chair + self.has_shadow = False + self.prepare() - RETURNS: list of lists of strings representing the loadstrs + if hl_data is None: + self.hl_map = None + else: + self.hl_map = MASHighlightMap.create_from_mapping( + self.__MHM_KEYS, + hl_data[0], + hl_data[1] + ) + + def prepare(self): """ - return super(MASArmLeft, self).build_loadstrs( - prefix + [store.mas_sprites.PREFIX_ARMS_LEFT] + Prepares this table chair combo by checking for shadow. + """ + self.has_shadow = ( + renpy.loadable(self.TC_GEN.format( + self.PREFIX_TABLE, + self.table, + self.SHADOW_SUFFIX, + "" + )) ) - def get(self, layer_code): + def setTable(self, new_table): """ - See MASArm.get + sets the table tag and checks shadow - This adds left- prefix to result + IN: + new_table - the new table tag to set + if an invalid string or NOne is passed in, we reset to + default """ - return super(MASArmLeft, self).get( - layer_code, - prefix=["left", store.mas_sprites.ART_DLM] - ) + if new_table: + self.table = new_table + else: + self.table = "def" + self.prepare() - class MASArmRight(MASArm): + + class MASArm(object): """ - Representation of a right arm. + Representation of an "Arm" - Overrides prefix-based functions + Each Arm consists of of a layered combination: + NOTE: we re using spaced layers so we can insert more if needed. + 0 - bottom layer. after body-0 but before table. Primary bottom layer. + 5 - middle layer. after table but before body-1. + 10 - top layer. after body-1. PROPERTIES: - see MASArm + tag - the tag string of this arm + layer_map - mapping of layer exiestence to image code + key: layer code + value: True if exists, False if not + hl_map - MASHighlightMap with layer code keys """ - def build_loadstrs(self, prefix): - """ - Generates loadstrs for this arm + LAYER_BOT = "0" + LAYER_MID = "5" + LAYER_TOP = "10" - IN: - prefix - prefix to apply to the loadstrs - list of strings + __MPA_KEYS = (LAYER_BOT, LAYER_MID, LAYER_TOP) - RETURNS: list of lists of strings representing the loadstrs + def __init__(self, tag, layer_map, hl_data=None): """ - return super(MASArmRight, self).build_loadstrs( - prefix + [store.mas_sprites.PREFIX_ARMS_RIGHT] - ) + Constructor - def get(self, layer_code): + IN: + tag - tag string for this arm + layer_map - layer map to use + key: image layer code + value: True if exists, False if not + hl_data - highlght map data. tuple of the following formaT: + [0] - default MASFilterMap to use. Pass in None to + not set a default highlight + [1] - highlight mapping to use. Format: + key: image layer code + value: MASFilterMap object, or None if no highlight + pass in None if no highlights should be used at all """ - See MASArm.get + self.tag = tag + self.clean_map(layer_map) + self.layer_map = layer_map + + if hl_data is not None: + self.hl_map = MASHighlightMap.create_from_mapping( + self.__MPA_KEYS, + hl_data[0], + hl_data[1] + ) + else: + self.hl_map = None - This adds right- prefix to result + def __build_loadstrs_hl(self, prefix, layer_code): """ - return super(MASArmRight, self).get( - layer_code, - prefix=["right", store.mas_sprites.ART_DLM] - ) + Builds load strings for a hlight from a given map + IN: + prefix - prefix to apply + layer_code - layer code to generate loadstrings for - class MASPoseArms(object): - """ - Collection of MASArm objects. An Arm object is the representation of - an arm sprite. + RETURNS: list of lists of strings representing image path for all + highlights for a layer code + """ + if self.hl_map is None: + return [] - PROPERTIES: - arms - dict mapping arms to MASArm objects - keys: number from NUM_ARMS - value: MASArm object. None means no arm for this arm + mfm = self.hl_map.get(layer_code) + if mfm is None: + return [] - """ - import store.mas_sprites_json as msj + # generate for all unique + return [ + prefix + [ + store.mas_sprites.HLITE_SUFFIX, + hlc, + store.mas_sprites.FILE_EXT + ] + for hlc in mfm.unique_values() + ] - def __init__(self, arm_data, def_base=True): + @classmethod + def _fromJSON(cls, json_obj, msg_log, ind_lvl, build_class): """ - Constructor + Builds a MASArm object based on the given JSON format of it IN: - arm_data - see arms property - def_base - True will use base arms for all missing data - False will not - (Default: True) + json_obj - JSON object to parse + ind_lvl - indent level + build_class - actual MASArm derivative to build + + OUT: + msg_log - list to save messages to + + RETURNS: MASArm instance built with the JSON, or None if failed """ - # must have a mas pose arm - if not store.mas_ev_data_ver._verify_dict( - arm_data, - allow_none=False + params = {} + # start with required params + # tag + # layers + if not store.mas_sprites_json._validate_params( + json_obj, + params, + { + "tag": (str, store.mas_sprites_json._verify_str), + "layers": (str, store.mas_sprites_json._verify_str), + }, + True, + msg_log, + ind_lvl ): - raise Exception("arm data required for MASPoseArms") - - # clean arms before setting - self._clean_arms(arm_data, def_base) - self.arms = arm_data + return None - def _clean_arms(self, arm_data, def_base): - """ - Cleans arm data given - Will Noneify invalid-typed data + # additional processing for the layers to ensure valid data + layer_map = {} + for layer in params.pop("layers").split("^"): + if layer in cls.__MPA_KEYS: + layer_map[layer] = True - IN: - arm_data - arm data to clean - def_base - True will use base arms for all missing data - False will not + else: + # invalid layer + # warn and ignore + msg_log.append(( + store.mas_sprites_json.MSG_WARN_T, + ind_lvl, + store.mas_sprites_json.MA_INVALID_LAYER.format(layer) + )) - OUT: - arm_data - cleaned arm data - """ - # first validate the arm data - for arm_key in arm_data.keys(): + # NOTE: ERR if we have no layers for an arm. This is useless to + # include if no layers. + if len(layer_map) == 0: + msg_log.append(( + store.mas_sprites_json.MSG_ERR_T, + ind_lvl, + store.mas_sprites_json.MA_NO_LAYERS + )) + return None - # then check - if arm_key in store.mas_sprites.NUM_ARMS: - # NOneify invalid data - if not isinstance(arm_data[arm_key], MASArm): - store.mas_utils.writelog( - "Invalid arm data at '{0}'\n".format(arm_key) - ) - arm_data[arm_key] = None + # add layers to params + params["layer_map"] = layer_map + + # now check highlight + if store.mas_sprites_json.HLITE in json_obj: + + # parse + vhl_data = {} + + if store.mas_sprites_json._validate_highlight( + json_obj, + vhl_data, + msg_log, + ind_lvl, + layer_map.keys() + ): + # success + hl_data = vhl_data.get("hl_data", None) + if hl_data is not None: + params["hl_data"] = hl_data else: - # remove invalid keys - arm_data.pop(arm_key) + # failure + return None - # now go through the arm data and set base for all non-included - # arm data - if def_base: - for arm_key in store.mas_sprites.NUM_ARMS: - if arm_key not in arm_data: - arm_data[arm_key] = store.mas_sprites.use_bma(arm_key) + # warn for extra properties + for extra_prop in json_obj: + msg_log.append(( + store.mas_sprites_json.MSG_WARN_T, + ind_lvl, + store.mas_sprites_json.EXTRA_PROP.format(extra_prop) + )) - # NOTE: if def_base is False, then get will auto return - # None so no need to set any data + # we now should have all the data we need to build + return build_class(**params) def build_loadstrs(self, prefix): """ - Generates loadstrs for this PoseArms object + Builds loadstrs for this arm IN: - prefix - list of strings to apply as prefix + prefix - prefix to apply to the loadstrs + should be list of strings - RETURNS: list of lists of strings representing the load strs + RETURNS: list of lists of strings representing the load strings + for this arm, + highlights """ + if not self.tag: + return [] + loadstrs = [] - # loop over all the arms we have - for arm_key in self.arms: - # only od actual arms - arm = self.arms[arm_key] + # add arms based on layer code + for layer_code in self.__MPA_KEYS: + # NOTE: we only add an arm + hlite if it exists for a layer + # code - if arm is not None: + if self.layer_map.get(layer_code, False): - # check for leans - lean = store.mas_sprites.ARMS_LEAN.get(arm_key, None) - if lean is None: - # no lean, do normal prefix - arm_prefix = prefix + [store.mas_sprites.PREFIX_ARMS] - else: - # have lean, do lean prefix - arm_prefix = prefix + [ - store.mas_sprites.PREFIX_ARMS_LEAN, - lean, - store.mas_sprites.ART_DLM - ] + # generate image + new_img = prefix + [ + self.tag, + store.mas_sprites.ART_DLM, + str(layer_code) + ] - # pass in prefix to the arm - loadstrs.extend(arm.build_loadstrs(arm_prefix)) + # add with extension + loadstrs.append(new_img + [store.mas_sprites.FILE_EXT]) + + # generate highlights + loadstrs.extend(self.__build_loadstrs_hl( + new_img, + layer_code + )) return loadstrs - @staticmethod - def fromJSON(json_obj, msg_log, ind_lvl): + def clean_map(self, mapping): """ - Builds a MASPoseArms object given a JSON format of it + cleans the given map, ensuring it contains only valid layer + keys. No errors are logged. IN: - json_obj - json object to parse - ind_lvl - indent level - - OUT: - msg_log - list to save messages to - - RETURNS: MASPoseArms object built using the JSON, None if no - data to be made, False if error occured + mapping - mapping to clean """ - # NOTE: we can assume type is checked already - - arm_data = {} - - # loop over valid arm data - isbad = False - for arm_id, arm_sid in store.mas_sprites.NUM_ARMS.iteritems(): - if arm_sid in json_obj: - arm_obj = json_obj.pop(arm_sid) - - # check object first - if arm_obj is None: - # None is okay - arm_data[arm_id] = None - - elif not store.mas_sprites_json._verify_dict(arm_obj): - # must be dict, however - msg_log.append(( - store.mas_sprites_json.MSG_ERR_T, - ind_lvl, - store.mas_sprites_json.MPA_BAD_TYPE.format( - arm_sid, - dict, - type(arm_obj) - ) - )) - isbad = True - - else: - # otherwise try and parse this data - - # log loading - msg_log.append(( - store.mas_sprites_json.MSG_INFO_T, - ind_lvl, - store.mas_sprites_json.MA_LOADING.format(arm_sid) - )) - - # parse - arm = MASArm._fromJSON( - arm_obj, - msg_log, - ind_lvl + 1, - store.mas_sprites.NUM_MARMS[arm_id] - ) - - # check valid - if arm is None: - # failure case - isbad = True - - else: - # log success - msg_log.append(( - store.mas_sprites_json.MSG_INFO_T, - ind_lvl, - store.mas_sprites_json.MA_SUCCESS.format( - arm_sid - ) - )) + for map_key in mapping.keys(): + if map_key not in self.__MPA_KEYS: + mapping.pop(map_key) - # and save the data - arm_data[arm_id] = arm + def get(self, layer_code, prefix=[]): + """ + Generates tag name + suffixes to use for + a given layer code. A tag name is a tuple of strings + that can be joined to build the full tag name, + Tag Names do NOT include file extensions - # now check for extras - for extra_prop in json_obj: - msg_log.append(( - store.mas_sprites_json.MSG_WARN_T, - ind_lvl, - store.mas_sprites_json.EXTRA_PROP.format(extra_prop) - )) + IN: + layer_code - layer code to fetch tag names for + prefix - prefix to apply to the tag string if desired + (Default: []) - # quit if failures - if isbad: - return False + RETURNS: list consisting of the tag strings and + appropriate suffixes + """ + if not self.tag: + return [] - # NOTE: no 0 data checks because a pose arms with nothing in it - # would be considered sleeve/armless (think bikinis) + # should this item exist on tihs layer code? + if not self.layer_map.get(layer_code, False): + return [] - # otherwise we are good so we can build - # NOTE: we never use base options with JSONS. - # spritepack creators should always describe each used arm - # incase the base poses change - return MASPoseArms(arm_data, def_base=False) + # should exist, generate the primary tag string + return prefix + [ + self.tag, + store.mas_sprites.ART_DLM, + str(layer_code) + ] - def get(self, arm_key): + def gethlc(self, layer_code, flt, defval=None): """ - Gets the arm data associated with the given arm key + Gets highlight code. IN: - arm_key - key of the arm data to get + layer_code - layer to get highlight for + flt - filter to get highilght for + defval - default value to return + (Default: None) - RETURNS: MASArm object requested, or NOne if not available for the - arm key + RETURNS: highlight code, or None if no highligiht """ - return self.arms.get(arm_key, None) + return MASHighlightMap.o_fltget( + self.hl_map, + layer_code, + flt, + defval + ) - def getflp(self, leanpose): + def hl_keys(self): """ - Retrieves arms assocaited with the given leanpose - - IN: - leanpose - the leanpose to get arms for + Returns hl keys for a MASArm - RETURNS: Tuple of arms associated with the leanpose. None may be - returned if no arms for the leanpose. The number of arms is - not a guarantee. + RETURNS: tuple of hl keys """ - arm_data = [] - for arm_key in store.mas_sprites.base_mpm.get(leanpose, []): - arm = self.get(arm_key) - if arm is not None: - arm_data.append(arm) + return self.__MPA_KEYS - if len(arm_data) > 0: - return tuple(arm_data) + @classmethod + def hl_keys_c(cls): + """ + Class method version of hl_keys - # otherwse return None because no arms - return None + RETURNS: tuple of hl keys + """ + return cls.__MPA_KEYS - class MASHighlightMap(object): + class MASArmBoth(MASArm): """ - Maps arbitrary keys to objects - - NOTE: values dont have to be MASFilterMAP objects, but certain - functions will fail if not. + Representation of an "arm" that actually covers both arms - NOTE: this can iterated over to retrieve all objects in here - EXCEPT for the default. + This currently has no additional behavior. + It's primary use is to act as a type of MASArm PROPERTIES: - None. Use provided functions to manipulate the map. + see MASArm """ - __KEY_ALL = "*" - - def __init__(self, keys, default=None): - """ - Constructor + pass - IN: - keys - iterable of keys that we are allowed to use. - NOTE: the default catch all key of "*" (KEY_ALL) does NOt - need to be in here. - default - value to use as the default/catch all object. This - is assigned to the KEY_ALL key. - """ - self.__map = { self.__KEY_ALL: default } - # remove keyall - key_list = list(keys) - if self.__KEY_ALL in key_list: - key_list.remove(self.__KEY_ALL) + class MASArmLeft(MASArm): + """ + Representation of a left arm. - # remove duplicate - self.__valid_keys = tuple(set(key_list)) + Overrides prefix-based functions - def __iter__(self): - """ - Iterator object (generator) - """ - for key in self.__valid_keys: - item = self.get(key) - if item is not None: - yield item + PROPERTIES: + see MASArm + """ - def _add_key(self, new_key, new_value=None): + def build_loadstrs(self, prefix): """ - Adds a key to the valid keys list. Also adds a value if desired - NOTE: this is not intended to be done wildly. Please do not - make a habit of adding keys after construction. - NOTE: do not use this to add values. if the given key already - exists, the value is ignored. + Generates loadstrs for this arm IN: - new_key - new key to add - new_value - new value to associate with this key - (Default: None) + prefix - prefix to apply to the loadstrs + list of strings + + RETURNS: list of lists of strings representing the loadstrs """ - if new_key not in self.__valid_keys and new_key != self.__KEY_ALL: - key_list = list(self.__valid_keys) - key_list.append(new_key) - self.__valid_keys = tuple(key_list) - self.add(new_key, new_value) + return super(MASArmLeft, self).build_loadstrs( + prefix + [store.mas_sprites.PREFIX_ARMS_LEFT] + ) - def add(self, key, value): + def get(self, layer_code): """ - Adds value to map. - NOTE: type is enforced here. If the given item is NOT a - MASFilterMap or None, it is ignored. - NOTE: this will NOT set the default even if KEY_ALL is passed in + See MASArm.get - IN: - key - key to store item to - value - value to add - if None is passed, this is equivalent to clear + This adds left- prefix to result """ - if value is None: - self.clear(key) - return + return super(MASArmLeft, self).get( + layer_code, + prefix=["left", store.mas_sprites.ART_DLM] + ) - if key == self.__KEY_ALL or key not in self.__valid_keys: - return - # otherwise valid to add - self.__map[key] = value + class MASArmRight(MASArm): + """ + Representation of a right arm. - def apply(self, mapping): + Overrides prefix-based functions + + PROPERTIES: + see MASArm + """ + + def build_loadstrs(self, prefix): """ - Applies the given dict mapping to this MASHighlightMap. - NOTE: will not add invalid keys. + Generates loadstrs for this arm IN: - mapping - dict of the following format: - key: valid key for this map - value: MASFilterMap object or None - """ - for key in mapping: - self.add(key, mapping[key]) + prefix - prefix to apply to the loadstrs + list of strings - def clear(self, key): + RETURNS: list of lists of strings representing the loadstrs """ - Clears value with the given key. - NOTE: will NOT clear the default even if KEY_ALL is passed in + return super(MASArmRight, self).build_loadstrs( + prefix + [store.mas_sprites.PREFIX_ARMS_RIGHT] + ) - IN: - key - key to clear with + def get(self, layer_code): """ - if key != self.__KEY_ALL and key in self.__map: - self.__map.pop(key) + See MASArm.get - @staticmethod - def clear_hl_mapping(hl_mpm_data): + This adds right- prefix to result """ - Clears hl mapping in the given hl data object. AKA: Sets the - hl mapping portion of a pre-MHM MPM to {}. + return super(MASArmRight, self).get( + layer_code, + prefix=["right", store.mas_sprites.ART_DLM] + ) - NOTE: this should only be used with the MASPoseMap._transform - function with MASAccessory - IN: - hl_mpm_data - hl data set in a MASPoseMap. + class MASPoseArms(object): + """ + Collection of MASArm objects. An Arm object is the representation of + an arm sprite. - RETURNS: hl data to set in a MASPoseMap. + PROPERTIES: + arms - dict mapping arms to MASArm objects + keys: number from NUM_ARMS + value: MASArm object. None means no arm for this arm + + """ + import store.mas_sprites_json as msj + + def __init__(self, arm_data, def_base=True): """ - if hl_mpm_data is None: - return None - return (hl_mpm_data[0], {}) + Constructor - @staticmethod - def convert_mpm(hl_keys, mpm): + IN: + arm_data - see arms property + def_base - True will use base arms for all missing data + False will not + (Default: True) """ - Converts hl mappings in a MASPoseMap to MASHighlightMAp objects + # must have a mas pose arm + if not store.mas_ev_data_ver._verify_dict( + arm_data, + allow_none=False + ): + raise Exception("arm data required for MASPoseArms") + + # clean arms before setting + self._clean_arms(arm_data, def_base) + self.arms = arm_data + + def _clean_arms(self, arm_data, def_base): + """ + Cleans arm data given + Will Noneify invalid-typed data IN: - hl_keys - highlight keys to use - mpm - MASPoseMap object to convert + arm_data - arm data to clean + def_base - True will use base arms for all missing data + False will not + + OUT: + arm_data - cleaned arm data """ - if mpm is None: - return + # first validate the arm data + for arm_key in arm_data.keys(): - # first, build modify pargs map - pargs = {} - for param in MASPoseMap.P_PARAM_NAMES: - hl_data = mpm.get(MASPoseMap.pn2lp(param), None) - if hl_data is None: - pargs[param] = None - else: - pargs[param] = MASHighlightMap.create_from_mapping( - hl_keys, - hl_data[0], - hl_data[1] - ) + # then check + if arm_key in store.mas_sprites.NUM_ARMS: + # NOneify invalid data + if not isinstance(arm_data[arm_key], MASArm): + store.mas_utils.writelog( + "Invalid arm data at '{0}'\n".format(arm_key) + ) + arm_data[arm_key] = None - # then modify the mpm - mpm._modify(**pargs) + else: + # remove invalid keys + arm_data.pop(arm_key) - @staticmethod - def create_from_mapping(hl_keys, hl_def, hl_mapping): + # now go through the arm data and set base for all non-included + # arm data + if def_base: + for arm_key in store.mas_sprites.NUM_ARMS: + if arm_key not in arm_data: + arm_data[arm_key] = store.mas_sprites.use_bma(arm_key) + + # NOTE: if def_base is False, then get will auto return + # None so no need to set any data + + def build_loadstrs(self, prefix): """ - Creates a MASHighlightMap using keys/default/mapping + Generates loadstrs for this PoseArms object IN: - hl_keys - list of keys to use - hl_def - default highlight to use. Can be None - hl_mapping - mapping to use. + prefix - list of strings to apply as prefix - RETURNS: created MASHighlightMap + RETURNS: list of lists of strings representing the load strs """ - mhm = MASHighlightMap(hl_keys, default=hl_def) - mhm.apply(hl_mapping) - return mhm + loadstrs = [] - def fltget(self, key, flt, defval=None): - """ - Combines getting from here and getting the resulting MASFilterMap - object. + # loop over all the arms we have + for arm_key in self.arms: + # only od actual arms + arm = self.arms[arm_key] - IN: - key - key to get from this map - flt - filter to get from associated MASFilterMap, if found - defval - default value to return if no flt value could be - found. - (Default: None) + if arm is not None: - RETURNS: value in the MASFilterMap associated with the given - flt, using the MASFilterMap associated with the given key. - or defval if no valid MASfilterMap or value found. - """ - mfm = self.get(key) - if mfm is None: - return defval + # check for leans + lean = store.mas_sprites.ARMS_LEAN.get(arm_key, None) + if lean is None: + # no lean, do normal prefix + arm_prefix = prefix + [store.mas_sprites.PREFIX_ARMS] + else: + # have lean, do lean prefix + arm_prefix = prefix + [ + store.mas_sprites.PREFIX_ARMS_LEAN, + lean, + store.mas_sprites.ART_DLM + ] - return mfm.get(flt, defval=defval) + # pass in prefix to the arm + loadstrs.extend(arm.build_loadstrs(arm_prefix)) + + return loadstrs @staticmethod - def fromJSON(json_obj, msg_log, ind_lvl, hl_keys): + def fromJSON(json_obj, msg_log, ind_lvl): """ - Builds hl data from JSON data + Builds a MASPoseArms object given a JSON format of it IN: - json_obj - JSON object to parse - ind_lvl - indentation level - NOTE: this function handles loading/success log so - do NOT increment indent when passing in - hl_keys - expected keys of this highlight map + json_obj - json object to parse + ind_lvl - indent level OUT: - msg_log - list to add messagse to + msg_log - list to save messages to - RETURNS: hl_data, ready to passed split and passed into - create_from_mapping. Tuple: - [0] - default MASFilterMap object - [1] - dict: - key: hl_key - value: MASFilterMap object - or None if no data, False if failure in parsing occured + RETURNS: MASPoseArms object built using the JSON, None if no + data to be made, False if error occured """ - # first log loading - msg_log.append(( - store.mas_sprites_json.MSG_INFO_T, - ind_lvl, - store.mas_sprites_json.MHM_LOADING - )) + # NOTE: we can assume type is checked already - # parse the data - hl_data = MASHighlightMap._fromJSON_hl_data( - json_obj, - msg_log, - ind_lvl + 1, - hl_keys - ) + arm_data = {} - # check fai/succ - if hl_data is False: - # loggin should take care of this already - return False + # loop over valid arm data + isbad = False + for arm_id, arm_sid in store.mas_sprites.NUM_ARMS.iteritems(): + if arm_sid in json_obj: + arm_obj = json_obj.pop(arm_sid) - # log success - msg_log.append(( - store.mas_sprites_json.MSG_INFO_T, - ind_lvl, - store.mas_sprites_json.MHM_SUCCESS - )) + # check object first + if arm_obj is None: + # None is okay + arm_data[arm_id] = None - return hl_data + elif not store.mas_sprites_json._verify_dict(arm_obj): + # must be dict, however + msg_log.append(( + store.mas_sprites_json.MSG_ERR_T, + ind_lvl, + store.mas_sprites_json.MPA_BAD_TYPE.format( + arm_sid, + dict, + type(arm_obj) + ) + )) + isbad = True - def get(self, key): - """ - Gets value wth the given key. + else: + # otherwise try and parse this data - IN: - key - key of item to get + # log loading + msg_log.append(( + store.mas_sprites_json.MSG_INFO_T, + ind_lvl, + store.mas_sprites_json.MA_LOADING.format(arm_sid) + )) - RETURNS: MASFilterMap object, or None if not found - """ - if key in self.__map: - return self.__map[key] + # parse + arm = MASArm._fromJSON( + arm_obj, + msg_log, + ind_lvl + 1, + store.mas_sprites.NUM_MARMS[arm_id] + ) - # otherwise return default - return self.getdef() + # check valid + if arm is None: + # failure case + isbad = True - def getdef(self): - """ - Gets the default value + else: + # log success + msg_log.append(( + store.mas_sprites_json.MSG_INFO_T, + ind_lvl, + store.mas_sprites_json.MA_SUCCESS.format( + arm_sid + ) + )) - RETURNS: MASFilterMap object, or NOne if not found - """ - return self.__map.get(self.__KEY_ALL, None) + # and save the data + arm_data[arm_id] = arm - def keys(self): - """ - gets keys in this map + # now check for extras + for extra_prop in json_obj: + msg_log.append(( + store.mas_sprites_json.MSG_WARN_T, + ind_lvl, + store.mas_sprites_json.EXTRA_PROP.format(extra_prop) + )) - RETURNS: tuple of keys - """ - return self.__valid_keys + # quit if failures + if isbad: + return False - @staticmethod - def o_fltget(mhm, key, flt, defval=None): + # NOTE: no 0 data checks because a pose arms with nothing in it + # would be considered sleeve/armless (think bikinis) + + # otherwise we are good so we can build + # NOTE: we never use base options with JSONS. + # spritepack creators should always describe each used arm + # incase the base poses change + return MASPoseArms(arm_data, def_base=False) + + def get(self, arm_key): """ - Similar to fltget, but on a MASHighlightMap object. - NOTE: does None checks of mhm and flt. + Gets the arm data associated with the given arm key IN: - mhm - MASHighlightMap object to run fltget on - key - key to get MASFilterMap from mhm - flt - filter to get from associated MASFilterMap - defval - default value to return if no flt value could be found - (Default: None) + arm_key - key of the arm data to get - RETURNS: See fltget + RETURNS: MASArm object requested, or NOne if not available for the + arm key """ - if mhm is None or flt is None: - return defval - - return mhm.fltget(key, flt, defval=defval) + return self.arms.get(arm_key, None) - def setdefault(self, value): + def getflp(self, leanpose): """ - Sets the default value + Retrieves arms assocaited with the given leanpose IN: - value - value to use as default + leanpose - the leanpose to get arms for + + RETURNS: Tuple of arms associated with the leanpose. None may be + returned if no arms for the leanpose. The number of arms is + not a guarantee. """ - if value is None or isinstance(value, MASFilterMap): - self.__map[self.__KEY_ALL] = value + arm_data = [] + for arm_key in store.mas_sprites.base_mpm.get(leanpose, []): + arm = self.get(arm_key) + if arm is not None: + arm_data.append(arm) + + if len(arm_data) > 0: + return tuple(arm_data) + + # otherwse return None because no arms + return None + # pose map helps map poses to an image class MASPoseMap(renpy.store.object): diff --git a/Monika After Story/game/updates.rpy b/Monika After Story/game/updates.rpy index fd34b13265..6efff10d89 100644 --- a/Monika After Story/game/updates.rpy +++ b/Monika After Story/game/updates.rpy @@ -494,6 +494,10 @@ label v0_11_3(version="v0_11_3"): else: gender_ev.start_date = mas_getFirstSesh() + datetime.timedelta(minutes=30) + # Unlock quit smoking pool topic if we smoke + if persistent._mas_pm_do_smoke: + mas_unlockEVL("monika_smoking_quit","EVE") + #Unlock the leaving already fare leaving_already_ev = mas_getEV("bye_leaving_already") if leaving_already_ev: diff --git a/Monika After Story/game/zz_backgrounds.rpy b/Monika After Story/game/zz_backgrounds.rpy index 1da989b780..41f635ce35 100644 --- a/Monika After Story/game/zz_backgrounds.rpy +++ b/Monika After Story/game/zz_backgrounds.rpy @@ -3,23 +3,1830 @@ default persistent._mas_background_MBGdata = {} #Store the persistent background #Defaults to def (spaceroom) -default persistent._mas_current_background = "def" +default persistent._mas_current_background = "spaceroom" #START: Class definition init -10 python: - # TODO: the background class needs to decide the filters to use. - # *AS WELL AS THE PROGRESSION* - # TODO: move the current DAY/NIGHT filters from mas_sprites to here. - # NOTE: I will do this when adding sunset progression - class MASBackground(object): + class MASBackgroundFilterTypeException(Exception): + """ + Type exception for MASBackgroundFilter objects + """ + + def __init__(self, obj, mbf_type): + """ + Constructor + + IN: + obj - object that was not what we expected + mbf_type - type we expected + """ + self.msg = "{0} is not of {1}".format(obj, mbf_type) + + def __str__(self): + """ + String + """ + return self.msg + + + class MASBackgroundFilterSliceDuplicateException(Exception): + """ + Exception for when a Background fitler slice is in both a day chunk + and a night chunk. + """ + + def __init__(self, flt): + """ + Constructor + + IN: + flt - the filter name of the offending fitler slice. + """ + self.msg = "filter '{0}' found in both day chunk and night chunk".format(flt) + + def __str__(self): + return self.msg + + + class MASBackgroundFilterSlice(object): + """ + Represntation of a filter for a MASBackground. + this is related to the sprite filters, but gives each filter extra + oomph. + + BG filters are designed to be flexible to work with BGs. + See the MASBackgroundFilterChunk for more info on how this works. + + PROPERTIES: + name - the name of this filter. This should be a filter ENUM. + NOTE: this not checked until init level 0 + minlength - the amount of time in seconds that this filter must + be able to be used for it to be shown. If the filter cannot be + shown for this amount of time, it will not be shown at all. + maxlength - the amount of time in seconds that this filter can + be shown. The filter will never be shown more than this amt + of seconds. If None, then max is unbounded. + priority - the priority of this filter object. Larger number means + higher priority. Lower priority means filter will be removed + first. + flt - the filter (imagematrix) objects to use. OPTIONAL. + if this is None, it is assumed the filter data is handled + elsewhere. + """ + cache = {} + # internal cache of MASBackgroundFilterSlice objects to avoid + # building duplicates. + + def __init__(self, + name, + minlength, + maxlength=None, + priority=1, + flt=None, + cache=True + ): + """ + Constructor + + IN: + name - name of the filter. This is NOT checked against filter + ENUMS until init level 0. + minlength - amount of time in seconds that this filter must + be at least shown for. + maxlength - amount of time in seconds that this at most can be + shown for. + if None, max time is unbounded + (Default: None) + priority - priority of this filter object. Larger number means + higher priority. + Must be between 1 and 10, inclusive. + Defaults to 10 if invalid. + (Default: 10) + flt - imagemanip/matrix compatible filter to use. + only pass in if you wish to use the `add_to_filters` + function + (Default: None) + cache - pass False to not cache this object. + Only for debug purposes + (Default: True) + """ + self.name = name + self.minlength = minlength + self.maxlength = maxlength + self.flt = flt + + if 1 <= priority <= 10: + self.priority = priority + else: + self.priority = 10 + + if cache: + # store in cache + self.cache[hash(self)] = self + + def __eq__(self, other): + """ + EQ implementation. + Based on hash + """ + if isinstance(self, other.__class__): + return hash(self) == hash(other) + return False + + def __hash__(self): + """ + Hash implementation. FilterSlices are unique based on name, + minlength and priority + """ + return MASBackgroundFilterSlice.gen_hash( + self.name, + self.minlength, + self.maxlength, + self.priority + ) + + def __ne__(self, other): + """ + Not equals implementation + """ + return not self.__eq__(other) + + def __str__(self): + """ + Slice as string + """ + return "M: {1:>6}|X: {3:>6}|N: {0} |P: {2}".format( + self.name, + self.minlength, + self.priority, + self.maxlength + ) + + def add_to_filters(self): + """ + Adds this filter to the filters dict. Wil fail and log if used + after init level -1. + Only works if self.flt is not None. + """ + if self.flt is not None: + store.mas_sprites.add_filter(self.name, self.flt) + + @classmethod + def cachecreate(cls, + name, + minlength, + maxlength=None, + priority=10, + flt=None + ): + """ + Builds a MASBackgroundFilterSlice unless we have one in cache + + IN: + See Constructor + + RETURNS: MASBackgroundFilterSlice object + """ + hash_key = cls.gen_hash(name, minlength, maxlength, priority) + if hash_key in cls.cache: + return cls.cache[hash_key] + + return MASBackgroundFilterSlice( + name, + minlength, + maxlength=maxlength, + priority=priority, + flt=flt + ) + + def can_fit(self, seconds): + """ + Checks if this filter can fit in the time allotted + + IN: + seconds - number of seconds to check + + RETURNS: True if this filter can fit in the given number of seconds + FAlse if not + """ + return self.minlength <= seconds + + def can_fit_td(self, td): + """ + Checks if the filter can fit in the time allotted + + IN: + td - timedelta object to check + + RETURNS: True if this filter can fit in the given timedelta, + False if not + """ + return self.minlength <= int(td.total_seconds()) + + @staticmethod + def gen_hash(name, minlength, maxlength, priority): + """ + Generates a hash of the components of a MASBackgroundFilterSlice + + IN: + name - name to use + minlength - minlength to use + maxlength - maxlength to use + priority - priority to use + + RETURNS: hash of the object that would be created with the given + properties. + """ + return hash("-".join(( + name, + str(minlength), + str(maxlength), + str(priority), + ))) + + def is_max(self, value): + """ + Checks if the given vlaue is larger than max length. + If this slice is unbounded, this will always return False + + IN: + value - value to check + + RETURNS: True if the value is larger than maxlength, False if not + """ + if self.maxlength is None: + return False + + return value > self.maxlength + + def verify(self): + """ + Verifies if this filter's name is a valid filter. Call this + after init level -1. + + RETURNS: True if filter is valid, False if not + """ + return store.mas_sprites.is_filter(self.name) + + + class MASBackgroundFilterSliceData(object): + """ + Relates a MASBackgroundFilterSlice to its order and offset + + PROPERTIES: + offset - the offset associated with this slice + length - the length of this slice data + order - the order associated to this slice + flt_slice - the slice to associate + """ + + def __init__(self, order, flt_slice): + """ + Constructor + + IN: + order - the order for this slice + flt_slice - the slice to associate with this order + """ + self.order = order + self.offset = order + self.flt_slice = flt_slice + self.length = flt_slice.minlength + + def __gt__(self, other): + """ + Greater than uses order + """ + if isinstance(other, MASBackgroundFilterSliceData): + return self.order > other.order + return NotImplemented + + def __lt__(self, other): + """ + Less than uses order + """ + if isinstance(other, MASBackgroundFilterSliceData): + return self.order < other.order + return NotImplemented + + def __str__(self): + """ + strings are offset + order + name + """ + return "{0:>5}|ORD: {1} |AL: {3}|{2}".format( + self.offset, + self.order, + self.flt_slice, + self.length + ) + + def __len__(self): + """ + Returns length + """ + return self.length + + def eff_minlength(self): + """ + Calculates the ending offset assuming min length + + RETURNS: offset + minlength + """ + return self.offset + self.flt_slice.minlength + + @staticmethod + def highest_priority(sl_data_list): + """ + Finds the MASBackgroundFilterSliceData with the highest priority + and returns its index + + IN: + sl_data_list - list containig MASBackgroundFilterSliceData + objects to check + + RETURNS: index of the MASBackgroundFilterSliceData with the + highest priority + """ + h_priority = 0 + h_index = 0 + for index, sl_data in enumerate(sl_data_list): + if sl_data.flt_slice.priority > h_priority: + h_priority = sl_data.flt_slice.priority + h_index = index + + return h_index + + @staticmethod + def lowest_priority(sl_data_list): + """ + Finds the MASBackgroundFilterSliceData with the lowest priority + and returns its index + + IN: + sl_data_list - list containing MASBackgroundFilterSliceData + objects to check + + RETURNS: index of the MASBackgroundFilterSliceData with the + lowest priority + """ + l_priority = 10 + l_index = 0 + for index, sl_data in enumerate(sl_data_list): + if sl_data.flt_slice.priority < l_priority: + l_priority = sl_data.flt_slice.priority + l_index = index + + return l_index + + @staticmethod + def sk(obj): + """ + order sort key + """ + return obj.order + + @staticmethod + def sko(obj): + """ + offset sort key + """ + return obj.offset + + + class MASBackgroundFilterChunk(object): + """ + Chunk of filters for backgrounds. + + A BG filter chunk is a set of slices that represent a progression + of filters througout the time range. The slices are handled in a way + where they are intelligently picked depending on suntimes. This allows + for handling of cases where there isn't enough time for each slice to + exist. + + Each slice is a MASBackgroundFilterSlice object. The minlength property + is used to determine if the object canfit in its allocated time slice. + Priorities are used to determine the slices to keep. By default we + try to keep every slice we can. Minlength is also used to determine + when to swap filters. + + Slices are organized by an order. The order value determines the + desired order of slices. + + Code can be ran during a filter change by passing in function to + appropriate param. Any exceptions are caught and loggged. + + PROPERTIES: + is_day - True if this is a day chunk, False if not + """ + + _ERR_PP_STR = ( + "[ERROR] error in slice pp | {0}\n" + "=====FROM: {1} -> {2}\n" + ) + _ERR_PP_STR_G = ( + "[ERROR] error in global slice pp | {0}\n" + "=====FROM: {1} -> {2}\n" + ) + + def __init__(self, is_day, pp, *slices): + """ + Constructor + + IN: + is_day - True if this is a "Day" chunk. False if not + pp - progpoint to run on a filter change (or slice change) + This is ran multiple times if multiple filter changes + occur, but NOT at all if a chunk change occurs. + This is NOT guaranteed to run if more a than day goes by + between progressions. + the following args are passed to the progpoint: + flt_old - the outgoing filter (string) + NOTE: this is None if we are starting the game + flt_new - the incoming filter (string) + curr_time - the current time + pass None to not use a progpoint + *slices - slice arguments. Each item should be a + MASBackgroundFilterSlice object. + This should be in the desired order. + NOTE: there must be at least one slice with unbounded time + """ + self.is_day = is_day + + self._slices = [] + # MASBackgroundFilterSliceData objects in standard order + + self._eff_slices = [] + # MASBackgroundFilterSliceData objects that we are actually + # using in standard order + + self._pp = pp + # progpoint + + self._index = 0 + # the index in eff_slices of the last filter change + + self._length = 0 + # the last length value passed into build + + self._parse_slices(slices) + + def __str__(self): + """ + Shows effective slice information + """ + output = [ + "Current Slice: {0}".format(self._index), + "Total Length: {0}".format(self._length), + "Slices:", + ] + + # string other slices + for index in range(len(self._eff_slices)): + + # determine appropriate end time for eff size + if index < len(self._eff_slices)-1: + endl = self._eff_slices[index+1].offset + else: + endl = self._length + + # string slice + sl_data = self._eff_slices[index] + output.append("ES: {0:>5}|{1}".format( + endl - sl_data.offset, + sl_data + )) + + return "\n".join(output) + + def __len__(self): + """ + Length of this chunk + """ + return self._length + + def _adjust_offset(self, index, amt): + """ + Adjust offset of all eff_slices, starting from the given index. + + IN: + index - index to start adjusting offsets. + amt - amount to add to offsets. can be negative to + subtract. + """ + for sl_data in self._eff_slices[index:]: + sl_data.offset += amt + + def adv_slice(self, sfco, st_index, run_pp, curr_time): + """ + Runs advance slice alg, running progpoints, but does NOT actually + set new index. + + IN: + sfco - seconds from chunk offset + st_index - index to start at + run_pp - True will run the progpoints, False will not + curr_time - passed to the progpoint, should be current time + as a datetime.time object + + RETURNS: new slice index + """ + # slices length + s_len = len(self._eff_slices) + if s_len < 2: + return 0 + + st_index -= 1 + + # loop until sfco in range of current slice + # or we reach last slice + while ( + st_index < s_len-1 + and self._adv_slice_change( + st_index + 1, + sfco, + run_pp, + curr_time + ) + ): + st_index += 1 + + return st_index + + def _adv_slice_change(self, slidx, sfco, run_pp, curr_time): + """ + Checks if a slice offset at the given index is smaller than + sfco, and runs pps if so. This is mainly to combine a condition + check and work together so we don't need extra if statements. + + IN: + slidx - index of the NEXT slice to check + Assumes will not go past eff_slices length + sfco - seconds from chunk offset + run_pp - True will run progpoints, False will not + curr_time - passed to progpoints, should be current time as + datetime.time object + + RETURNS: True if we should continue looping index, False if we + have found the slice sfco belongs in. + """ + if self._eff_slices[slidx].offset > sfco: + return False + + # determine current and next slice data for a movement + if slidx > 0: + csl_data = self._eff_slices[slidx-1] + else: + csl_data = None + + if csl_data is not None: + nsl_data = self._eff_slices[slidx] + + # run progs + if run_pp: + self._pp_exec( + csl_data.flt_slice.name, + nsl_data.flt_slice.name, + curr_time + ) + + # always run global + store.mas_background.run_gbl_flt_change( + csl_data.flt_slice.name, + nsl_data.flt_slice.name, + curr_time + ) + + return True + + def build(self, length): + """ + Builds the effective slices array using the given length as + guidance. + + slices are built in a greedy fashion starting from 0, respecting + priorities and etc. + + IN: + length - the amount of seconds this chunk encompasses + """ + self._length = length + + if length < 1: + # this chunk has no length. clear it out + self._eff_slices = [] + return + + if len(self._slices) < 2: + # with only once slice, just set that slice as the + # main slice + sl_data = self._slices[0] + sl_data.length = length + sl_data.offset = 0 + self._eff_slices = [sl_data] + return + + # always start with the minimum length expansion alg + leftovers = self._min_fill(length) + + if len(leftovers) > 0: + # if we have leftovers, then use complex alg + self._priority_fill(length, leftovers) + + # set everyones length to minimum + for sl_data in self._eff_slices: + sl_data.length = sl_data.flt_slice.minlength + + # lastly, expand to fill voids + self._expand(length) + + self.reset_index() + + def current(self): + """ + Gets current filter + + RETURNS: current filter, or None if could not + """ + if 0 <= self._index < len(self._eff_slices): + return self._eff_slices[self._index].flt_slice.name + return None + + def current_pos(self): + """ + Generates internal information related to current position + + RETURNS: tuple: + [0] - current slice index + [1] - beginning offset of the current slice + [2] - beginning offset of the next slice + NOTE: this is -1 if no next slice + """ + if 0 <= self._index < len(self._eff_slices)-1: + next_offset = self._eff_slices[self._index+1].offset + else: + next_offset = -1 + + return (self._index, self._current_sldata().offset, next_offset) + + def _current_sldata(self): + """ + Gets current slice data + + RETURNS: current slice data + """ + return self._eff_slices[self._index] + + def _eff_chunk_min_end(self): + """ + Gets the minimal chunk end length. + + RETURNS: last eff_slice's eff_offset + its minlength + """ + return self._eff_slices[-1].eff_minlength() + + def _expand(self, length): + """ + Expands all slices in effective slices until it fills the given + length + + IN: + length - the amount of length we need to fill + """ + es_count = len(self._eff_slices) + diff = length - self._eff_chunk_min_end() + + # make a list of inc amounts for easy looping + inc_amts = [diff / es_count] * es_count + + # reverse add lefovers + store.mas_utils.lo_distribute( + inc_amts, + diff % es_count, + reverse=True + ) + + # apply amounts to values until we are out + while sum(inc_amts) > 0: + self._expand_once(inc_amts) + + # reform inc_amts so we continue adding leftover amounts to + # slices that can still be expanded + leftovers = store.mas_utils.fz_distribute(inc_amts) + if leftovers > 0: + store.mas_utils.lo_distribute( + inc_amts, + leftovers, + reverse=True, + nz=True + ) + + def _expand_once(self, value_list): + """ + Runs expansion alg. This will add index-based numbers from the + given value list to effective slice offsets and subtract added amts + from the given corresponding position in value list. + + IN: + value_list - list of amounts to add to individual items in + eff_slices + + OUT: + value_list - leftover amounts to distribute to eff_slices + """ + # apply inc amounts + # start by figuring the base new length + c_off = 0 + for index in range(len(self._eff_slices)): + c_off = self._expand_sld(index, value_list, c_off) + + def _expand_sld(self, index, value_list, c_off): + """ + Expands a slicedata item. Also adjusts value list as appropriate + + IN: + index - position to expand slice data and value list + value_list - list of amounts to add to individual items in + eff_slices + c_off - current offset value + + OUT: + value_list - value at index changed to leftover amounts or 0 + + RETURNS: new current offset value + """ + # get sl data + sl_data = self._eff_slices[index] + + # the current offset is always this sl data's new start + sl_data.offset = c_off + + # how long is this slice? + sl_len = sl_data.length + value_list[index] + + # if too big, adjust + if sl_data.flt_slice.is_max(sl_len): + diff = sl_len - sl_data.flt_slice.maxlength + value_list[index] = diff + sl_len = sl_data.flt_slice.maxlength + else: + value_list[index] = 0 + + # and set new length + sl_data.length = sl_len + + # calculate next offset + return c_off + sl_len + + def filters(self, ordered=False): + """ + Gets list of filters + + IN: + ordered - True will return the filters in an ordered list. + This may contain duplicates. + + RETURNS: list of all the filters associatd with this filter chunk + (list of strings) + """ + if ordered: + # ordered list + return [sl_data.flt_slice.name for sl_data in self._slices] + + # otherwise use a dict so we only return each filter once + filters = {} + for sl_data in self._slices: + filters[sl_data.flt_slice.name] = None + + return filters.keys() + + def first_flt(self): + """ + Gets the first filter in this chunk + + RETURNS: first filter in this chunk, or None if no eff slices + """ + if len(self._eff_slices) > 0: + return self._eff_slices[0].flt_slice.name + + return None + + def last_flt(self): + """ + Gets the last filter in this chunk + + RETURNS: last filter in this chunk, or None if no eff slices + """ + last_idx = len(self._eff_slices)-1 + if last_idx < 0: + return None + + return self._eff_slices[last_idx].flt_slice.name + + def _min_fill(self, length): + """ + Fills the effective slices using minlength logic. + + IN: + length - the length we are filling + + RETURNS: leftovers - contains slices that we could not fit. Could + be empty if we managed to fit all slices. + """ + built_length = 0 + index = 1 + # start at the 2nd slice as thats where we start adjusting offsets + + # always start with the first slice + built_length = self._slices[0].flt_slice.minlength + self._eff_slices = [self._slices[0]] + + # add slices + while built_length < length and index < len(self._slices): + # retrieve slice info for this index + curr_sl_data = self._slices[index] + + # add the slice + curr_sl_data.offset = built_length + self._eff_slices.append(curr_sl_data) + + # increment built length + built_length += curr_sl_data.flt_slice.minlength + index += 1 + + if built_length < length: + # we managed to fit every slice! + return [] + + # otherwise, we have a leftover slice in some way + last_sl_data = self._eff_slices.pop() + + # and add any remaining slices + return [last_sl_data] + self._slices[index:] + + def _parse_slices(self, slices): + """ + Parses the slices data + """ + # verify slices + if len(slices) < 1: + raise Exception("No slices found") + + has_unbounded = False + for index, bg_flt in enumerate(slices): + + # check slice + if not isinstance(bg_flt, MASBackgroundFilterSlice): + raise MASBackgroundFilterTypeException( + bg_flt, + MASBackgroundFilterSlice + ) + + if bg_flt.maxlength is None: + has_unbounded = True + bg_flt.priority = 11 # force this slice to be important + + # add to slices + store.mas_utils.insert_sort( + self._slices, + MASBackgroundFilterSliceData(index, bg_flt), + MASBackgroundFilterSliceData.sk + ) + + if not has_unbounded: + raise Exception("No unbounded slice found") + + # set offset of initial slice to 0 + self._slices[0].offset = 0 + + def _pf_insert(self, index, sl_data): + """ + Inserts a filter slice offset into the effective slices list + based on a starting index. + + IN: + index - starting index + sl_data - the slice data to insert + """ + # looop, finding the right place for the sl_off + while ( + index < len(self._eff_slices) + and self._eff_slices[index] < sl_data + ): + index += 1 + + # we must have the correct location now + # determine the offset to use + if index == 0: + sl_data.offset = 0 + else: + sl_data.offset = self._eff_slices[index-1].eff_minlength() + self._eff_slices.insert(index, sl_data) + + # now adjust offsets for all remaining sl datas + self._adjust_offset(index + 1, sl_data.flt_slice.minlength) + + def _pp_exec(self, flt_old, flt_new, curr_time): + """ + Executes a progpoint + + Exceptions are logged + + IN: + flt_old - outgoing filter (string) + flt_new - incoming filter (string) + curr_time - current time as datetime.time + """ + if self._pp is None: + return + + try: + self._pp(flt_old=flt_old, flt_new=flt_new, curr_time=curr_time) + except Error as e: + store.mas_utils.writelog(self._ERR_PP_STR.format( + repr(e), + flt_old, + flt_new + )) + + def _priority_fill(self, length, leftovers): + """ + Fills the effective slices using priority logic. + This assumes the eff slices has been filled with minimal logic + + IN: + length - the amount of length we need to fill + leftovers - slices that have not been added yet + """ + # Gist: + # 1. reverse through the eff_slices, removing elements with + # lower priorities than leftovers, and adding high priority + # elements from leftovers. + + # highest priority in leftovers + hpsl_data = leftovers.pop( + MASBackgroundFilterSliceData.highest_priority(leftovers) + ) + + for es_index in range(len(self._eff_slices)-1, -1, -1): + + # get current slice + csl_data = self._eff_slices[es_index] + + if csl_data.flt_slice.priority < hpsl_data.flt_slice.priority: + # current has a lower priority + + # remove the lower priority item, and store in leftovers + leftovers.insert(0, self._eff_slices.pop(es_index)) + + # then clean up the offsets + self._adjust_offset( + es_index, + csl_data.flt_slice.minlength * -1 + ) + + # add the higher priority item + self._pf_insert(es_index, hpsl_data) + + # and find newest high leftover priority + hpsl_data = leftovers.pop( + MASBackgroundFilterSliceData.highest_priority( + leftovers + ) + ) + + # clean up if our current min length is too large + while ( + len(self._eff_slices) > 1 + and self._eff_chunk_min_end() > length + ): + # obtain lowest priority filter slice offset object and remove + llop_index = MASBackgroundFilterSliceData.lowest_priority( + self._eff_slices + ) + lpsl_data = self._eff_slices.pop(llop_index) + + # fix eff offsets + self._adjust_offset( + llop_index, + lpsl_data.flt_slice.minlength * -1 + ) + + # ensure first slice has 0 offset + self._eff_slices[0].offset = 0 + + def progress(self, sfco, curr_time): + """ + Progresses the filter, running progpoints and updating indexes + + NOTE: we assume that our next target is in this chunk. + + Progpoints are ran for every slice we go through. + + IN: + sfco - seconds from chunk offset to progress to + curr_time - current time in datetime.time + + RETURNS: current filter after progression + """ + # advance slices + self._index = self.adv_slice(sfco, self._index, True, curr_time) + + return self.current() + + def update(self, ct_off): + """ + Updates the internal indexes. + NOTE: this will NOT call any progpoints + + IN: + ct_off - offset of current time, with respect to the chunk this + slice is in. + """ + s_len = len(self._eff_slices) + if s_len < 2: + self._index = 0 + return + + # determine current slice offsets + sidx = -1 + + while ( + sidx < s_len-1 + and self._eff_slices[sidx+1].offset <= ct_off + ): + # deteremine next current offset and next index + sidx += 1 + + # now we should have the correct index probably + self._index = sidx + + def reset_index(self): + """ + Resets slice index to 0 + """ + self._index = 0 + + def verify(self): + """ + Verifies the filters in this filter Chunk + Assumed to be called at least at init level 0 + Filters should all exist. + + Exceptions are raised if a bad filter is found. + """ + for sl_data in self._slices: + flt_slice = sl_data.flt_slice + if not flt_slice.verify(): + raise MASInvalidFilterException(flt_slice.name) + + + class MASBackgroundFilterManager(object): + """ + Filter Management class for backgrounds. + + The BG filter system slices a day into 3 chunks. + these chunks correspond to the suntimes system. + MidNight to SunRise (MN - SR) + SunRise to SunSet (SR - SS) + SunSet to MidNignt (SS - MN) + + Each chunk is marked day/night, and is used when determining if a + filter is day or night. This means two separate chunks with different + day/night settings can NOT contain same filters. If you need the same + filter content to be considerd day AND night, make two separate filter + enums. This is to avoid ambiguities. + + PROPERTIES: + None + """ + + _ERR_PP_STR = ( + "[ERROR] error in chunk pp | {0}\n\n" + "=====FROM:\n{1}\n\n" + "=====TO\n{2}\n" + ) + _ERR_PP_STR_G = ( + "[ERROR] error in global chunk pp | {0}\n\n" + "=====FROM:\n{1}\n\n" + "=====TO\n{2}\n" + ) + + def __init__(self, mn_sr, sr_ss, ss_mn, pp=None): + """ + Constructor + + IN: + mn_sr - MASBackgroundFilterChunk for midnight to sunrise + sr_ss - MASBackgroundFilterChunk for sunrise to sunset + ss_mn - MASBackgroundFilterChunk for sunset to midnight + pp - progpoint to run on a chunk change. + This may run multiple times if multiple chunk changes + have occurred. + This is NOT guaranteed to run if more than a day of time + passes between progressions. + the following args are passed to the progpoint: + chunk_old - the outgoing chunk (MBGFChunk) + NOTE: this is None if we are staring the game + chunk_new - the incoming chunk (MBGFChunk) + curr_time - the current time + (Default: None) + """ + if not isinstance(mn_sr, MASBackgroundFilterChunk): + raise MASBackgroundFilterTypeException( + mn_sr, + MASBackgroundFilterChunk + ) + if not isinstance(sr_ss, MASBackgroundFilterChunk): + raise MASBackgroundFilterTypeException( + sr_ss, + MASBackgroundFilterChunk + ) + if not isinstance(ss_mn, MASBackgroundFilterChunk): + raise MASBackgroundFilterTypeException( + ss_mn, + MASBackgroundFilterChunk + ) + + self._mn_sr = mn_sr + # midnight to sunrise + + self._sr_ss = sr_ss + # sunrise to sunset + + self._ss_mn = ss_mn + # sunset to midnight + + self._chunks = [self._mn_sr, self._sr_ss, self._ss_mn] + # ordered chunks for easier swapping + + self._pp = pp + # progpoint + + self._day_filters = {} + self._night_filters = {} + # organized filter dicts + # key: name of filter + # value: Ignored + # NOTE: organized in verify. + + self._index = 0 + # the index in _chunks of the current chunk + + self._prev_flt = None + # set to the current filter if update was used + + self._updated = False + # set to True upon an update, set to False upon progress + + def __str__(self): + """ + Shows chunks and curr chunk information + """ + output = [] + + # mn to sr chunk + chunk_name = "Midnight to Sunrise" + if self._index == 0: + chunk_name += "| CURRENT CHUNK" + output.append(chunk_name) + output.append(str(self._mn_sr)) + + # sr to ss chunk + chunk_name = "Sunrise to Sunset" + if self._index == 1: + chunk_name += "| CURRENT CHUNK" + output.append("") + output.append(chunk_name) + output.append(str(self._sr_ss)) + + # ss to mn chunk + chunk_name = "Sunset to Midnight" + if self._index == 2: + chunk_name += "| CURRENT CHUNK" + output.append("") + output.append(chunk_name) + output.append(str(self._ss_mn)) + + return "\n".join(output) + + def adv_chunk(self, sfmn, st_index, run_pp, curr_time, force_co): + """ + Runs advance chunks alg, running progpoints but does NOT actually + set new index. This WILL SET SLICE INDEXES. + + IN: + sfmn - number of seconds since midnight + st_index - index to start at + run_pp - True will run the progpoints, FAlse will not + curr_time - passed to the progpoint. should be current time + as a datetime.time object + force_co - True will force one chunk advancement. False will + not. This is for cases where we are in the same chunk, but + earlier than the current slice. Doing this allows us to + reset the slice index. + + RETURNS: new chunk index + """ + # chunk length + c_len = len(self._chunks) + + # determine current chunk offsets + cb_off, nb_off = self._calc_off(st_index) + + # loop unfil sfmn in range of current chunk + while sfmn < cb_off or nb_off <= sfmn or force_co: + # always set this to false after one iteration + force_co = False + + # get chunk chunk + curr_chunk = self._chunks[st_index] + + # determine the next current offset and next index + + # next offset or 0 if 86400 + cb_off = nb_off % (store.mas_utils.secInDay()) + st_index = (st_index + 1) % c_len # next index or 0 if max len + + # now calc next offset + nb_off = cb_off + len(self._chunks[st_index]) + + # new chunk is + new_chunk = self._chunks[st_index] + + # lastly run pp if desired + if run_pp: + self._pp_exec( + curr_chunk, + new_chunk, + curr_time + ) + + # always run global after + try: + store.mas_background._gbl_chunk_change( + curr_chunk, + new_chunk, + curr_time + ) + except Error as e: + store.mas_utils.writelog(self._ERR_PP_STR_G.format( + repr(e), + str(curr_chunk), + str(new_chunk), + )) + + # then finally reset slice index for this chunk + curr_chunk.reset_index() + + return st_index + + def backmap(self, anchors): + """ + Generates a backwords lookback map with a set of anchors. + Basically, this creates a mapping of the internal filters such that + each filter is mapped to an "anchor" filter, based on the order + of the filters. The lookback for determining an anchor also loops + upon reaching the end of a day. + + Example: + Anchors: flt_1, flt_3 + Order: flt_0, flt_1, flt_2, flt_3, flt_4, flt_5 + Resulting mapping: + flt_0: flt_3 - (because we loop to flt_5 when looking back + from flt_0) + flt_1: flt_1 + flt_2: flt_1 - (flt_1 is the closest previous anchor from + flt_2) + flt_3: flt_3 + flt_4: flt_3 + flt_5: flt_3 + + IN: + anchors - dict of anchors. Set the keys to the anchor filters. + + OUT: + anchors - the values will be set to lists of all filtesr mapped + to those anchors + + RETURNS: reverse map where each filter is a key, and the values are + anchors. + """ + if len(anchors) < 1: + return {} + + # organize all filters in order + ordered_flts = [] + for chunk in self._chunks: + ordered_flts.extend(chunk.filters(True)) + + if len(ordered_flts) < 1: + return {} + + # init anchors lists + for anc_key in anchors: + anchors[anc_key] = [] + + # init reverse map + r_map = {} + + # loop over filters + curr_anchor = None + orphans = [] + for flt in ordered_flts: + # check for new anchor + if flt in anchors: + curr_anchor = flt + + # now organize + if curr_anchor is None: + # orphans are added to the last anchor + orphans.append(flt) + + else: + # have anchor, add to lists and maps + r_map[flt] = curr_anchor + anchors[curr_anchor].append(flt) + + # add orphans to the last filter + if curr_anchor is not None: + anchors[curr_anchor].extend(orphans) + for orphan in orphans: + r_map[orphan] = curr_anchor + + return r_map + + def build(self, sunrise, sunset): + """ + Builds each chunk with the given sunrise and sunset values. + + IN: + sunrise - sunrise time in number of seconds from midnight + sunset - sunset time in number of seconds from midnight + """ + self._mn_sr.build(sunrise) + self._sr_ss.build(sunset - sunrise) + self._ss_mn.build((store.mas_utils.secInDay()) - sunset) + self._index = 0 + + def buildupdate(self, sunrise, sunset, curr_time): + """ + Builds each chunk with given sunrise/sunset values, then runs + update to set the correct index. + + Mostly a combination of build and update. + + Properly sets prev_flt in this scenario. + + IN: + sunrise - see build + sunset - see build + curr_time - see update + """ + # save current, pre-build filter + prev_flt = self.current() + + # build new slices + self.build(sunrise, sunset) + + # run update + self.update(curr_time) + + # set prev flt correctly + self._prev_flt = prev_flt + + def _calc_off(self, index): + """ + caluates beginning and next offset from chunk at the given index + + IN: + index - index of chunk to check + + RETURNS: tuple: + [0] - beginning offset of chunk at index + [1] - beginning offset of chunk at index+1 (or next chunk) + """ + # calclulate current + cb_off = 0 + for idx in range(index): + cb_off += len(self._chunks[idx]) + + # now for next + if index < len(self._chunks)-1: + nb_off = cb_off + len(self._chunks[index]) + else: + nb_off = store.mas_utils.secInDay() + + return cb_off, nb_off + + def current(self): + """ + Gets current filter + + RETURNS: current filter + """ + return self._chunks[self._index].current() + + def _current_chunk(self): + """ + Gets current chunk + + RETURNS: current chunk + """ + return self._chunks[self._index] + + def current_pos(self): + """ + Generates internal informatiom related to the current position + + RETURNS: tuple: + [0] - current chunk index + [1] - beginning offset of the current chunk + [2] - beginning offset of the next chunk + NOTE: this is number of seconds in day if no next chunk + [3] - current slice index + [4] - beginning offset of the current slice + [5] - beginning offset of the next slice + NOTE: this is set to the next chunk offset if no next + slice + """ + curr_offset, next_offset = self._calc_off(self._index) + + # now get slice info + sl_index, sl_begin, sl_end = self._current_chunk().current_pos() + + # end sl might be different + if sl_end < 0: + sl_end = next_offset + + # and return info + return ( + self._index, + curr_offset, + next_offset, + sl_index, + sl_begin, + sl_end + ) + + def filters(self): + """ + RETURNS: list of all filters associated with this filter manager + (list of strings) + NOTE: does not contain duplicates. + """ + both = {} + both.update(self._day_filters) + both.update(self._night_filters) + return both.keys() + + def filters_day(self): + """ + RETURNS: list of all day filters associated with this filter + manager. + (list of stirngs) + NOTE: does not contain duplicates + """ + return self._day_filters.keys() + + def filters_night(self): + """ + RETURNS: list of all night filters associated with this filter + manager. + (list of strings) + NOTE: does not contain duplicates + """ + return self._night_filters.keys() + + def is_flt_day(self, flt): + """ + Checks if the given filter is day according to this filter manager + NOTE: assumes we are organized already. + + IN: + flt - filter to check + + RETURNS: True if day, false if not + """ + return flt in self._day_filters + + def _organize(self): + """ + Organize filters into day and night dicts + """ + self._organize_chunk(self._mn_sr) + self._organize_chunk(self._sr_ss) + self._organize_chunk(self._ss_mn) + + def _organize_chunk(self, chunk): + """ + Organizes a single chunk into the day and night dicts + + IN: + chunk - MASBackgroundFilterChunk to organize + """ + if chunk.is_day: + flt_d = self._day_filters + else: + flt_d = self._night_filters + + for flt in chunk.filters(): + flt_d[flt] = None + + def _pp_exec(self, chunk_old, chunk_new, curr_time): + """ + Executes a progpoint + + Exceptions are logged + + IN: + chunk_old - outgoing MASBackgroundFilterChunk + chunk_new - incoming MASBackgroundFilterChunk + curr_time - current time as datetime.time + """ + if self._pp is None: + return + + try: + self._pp( + chunk_old=chunk_old, + chunk_new=chunk_new, + curr_time=curr_time + ) + except Error as e: + store.mas_utils.writelog(self._ERR_PP_STR.format( + repr(e), + str(chunk_old), + str(chunk_new) + )) + + def progress(self): + """ + Progresses the filter, running progpoints and updating indexes. + + NOTE: if update was called before this, then we only run the + global filter change progpoint if there was a filter change. + + NOTE: we do NOT do full loop arounds. This means that even if + there was a literal day between progressions, this will only run + as if it were same day progression. + + Progpoint execution rules: + * progression remains in the same chunk: + 1. progpoint in that chunk is ran for every slice change. + 2. global progpoint is ran for every slice change. + * progression moves to next chunk: + 1. progpoint from chunk to chunk is ran. + 2. global progpoint from chunk to chunk is ran. + 3. progpoints in the NEW chunk is ran for every slice change. + 4. global progpoint is ran for every slice change in the NEW + chunk. + * progression moves through multiple chunks: + 1. progpoint from chunk to chunk is ran for every chunk change. + 2. global progpoint from chunk to chunk is ran for every + chunk change. + 3. progpoints in the chunk we END UP IN is ran for every + slice change. + 4. global progpoint is ran for every slice change in the chunk + we END UP IN. + * progression changes via update: + 1. global progpoint is ran for one slice change if the filter + changes. + + RETURNS: the current filter after progression + """ + # seconds from midnight + curr time + curr_time = datetime.datetime.now().time() + sfmn = store.mas_utils.time2sec(curr_time) + + if self._updated: + # if we just updated, then we just need to run global prog + # point upon flt change and return new + new_flt = self.current() + self._updated = False + if new_flt != self._prev_flt: + store.mas_background.run_gbl_flt_change( + self._prev_flt, + new_flt, + curr_time + ) + + return new_flt + + # determine our current position range + pos_data = self.current_pos() + + # are we technically in same chunk but before in time? + # if so, we need to force a chunk move + force_co = ( + pos_data[1] <= sfmn < pos_data[2] # in same chunk + and sfmn < (pos_data[1] + pos_data[4]) # earlier than slice + ) + + # start by advancing chunks correctly, if needed + self._index = self.adv_chunk( + sfmn, + self._index, + True, + curr_time, + force_co + ) + + # now we can start advancing slices + return self._chunks[self._index].progress( + sfmn - (self._calc_off(self._index)[0]), + curr_time + ) + + def update(self, curr_time=None): + """ + Updates the internal indexes. + NOTE: this will NOT call any progpoints. Call progress after this + to run (some) progpoints if needed + + IN: + curr_time - datetime.time object to update internal indexes + to. + If NOne, then we use current. + (Default: None) + """ + if curr_time is None: + curr_time = datetime.datetime.now().time() + + # keep track of current filter + self._prev_flt = self.current() + + # establish seconds + sfmn = store.mas_utils.time2sec(curr_time) + + # establish chunk index + boff, eoff = self._calc_off(0) + cindex = 0 + while cindex < len(self._chunks)-1 and (sfmn < boff or eoff <= sfmn): + # determine next offsets + cindex += 1 + boff, eoff = self._calc_off(cindex) + + # we should now be in the correct index probably + self._chunks[self._index].reset_index() + self._index = cindex + self._chunks[cindex].update(sfmn - boff) + + # mark that we used update + self._updated = True + + def verify(self): + """ + Verifies the filters in this filter manager. + Assumed to be called at least at init level 0 + Filters cannot be in both day and night chunks. If this happens, + an exception will be raised. + + We also verify filters in each chunk here. + """ + # first organize filters into day and night + self._organize() + + # now compare the lists + for day_flt in self._day_filters: + if day_flt in self._night_filters: + raise MASBackgroundFilterSliceDuplicateException(day_flt) + + # now verify each chunk + self._mn_sr.verify() + self._sr_ss.verify() + self._ss_mn.verify() + + + def MASBackground( + background_id, + prompt, + image_day, + image_night, + image_rain_day=None, + image_rain_night=None, + image_overcast_day=None, + image_overcast_night=None, + image_snow_day=None, + image_snow_night=None, + hide_calendar=False, + hide_masks=False, + disable_progressive=None, + unlocked=False, + entry_pp=None, + exit_pp=None + ): + """DEPRECATED + Old-style MASBackground objects. + This is mapped to a MASFilterableBackground with default + (aka pre0.11.3 filters) slice management + + IN: + background_id: + id that defines the background object + NOTE: Must be unique + + prompt: + button label for this bg + + image_day: + the renpy.image object we use for this bg during the day + NOTE: Mandatory + + image_night: + the renpy.image object we use for this bg during the night + NOTE: Mandatory + + image_rain_day: + the image tag we use for the background while it's raining (day) + (Default: None, not required) + + image_rain_night: + the image tag we use for the background while it's raining (night) + (Default: None, not required) + + image_overcast_day: + the image tag we use for the background while it's overcast (day) + (Default: None, not required) + + image_overcast_night: + the image tag we use for the background while it's overcast (night) + (Default: None, not required) + + image_snow_day: + the image tag we use for the background while it's snowing (day) + (Default: None, not required) + + image_snow_night: + the image tag we use for the background while it's snowing (night) + (Default: None, not required) + + hide_calendar: + whether or not we want to display the calendar + (Default: False) + + hide_masks: + weather or not we want to show the windows + (Default: False) + + disable_progressive: + weather or not we want to disable progressive weather + (Default: None, if hide masks is true and this is not provided, we assume True, otherwise False) + + unlocked: + whether or not this background starts unlocked + (Default: False) + + entry_pp: + Entry programming point for the background + (Default: None) + + exit_pp: + Exit programming point for this background + (Default: None) + + RETURNS: MASFilterableBackground object + """ + # build map data + img_map_data = { + store.mas_sprites.FLT_DAY: MASWeatherMap(precip_map={ + store.mas_weather.PRECIP_TYPE_DEF: image_day, + store.mas_weather.PRECIP_TYPE_RAIN: image_rain_day, + store.mas_weather.PRECIP_TYPE_OVERCAST: image_overcast_day, + store.mas_weather.PRECIP_TYPE_SNOW: image_snow_day, + }), + store.mas_sprites.FLT_NIGHT: MASWeatherMap(precip_map={ + store.mas_weather.PRECIP_TYPE_DEF: image_night, + store.mas_weather.PRECIP_TYPE_RAIN: image_rain_night, + store.mas_weather.PRECIP_TYPE_OVERCAST: image_overcast_night, + store.mas_weather.PRECIP_TYPE_SNOW: image_snow_night, + }), + } + + # build object + return MASFilterableBackground( + background_id, + prompt, + MASFilterWeatherMap(**img_map_data), + store.mas_background.default_MBGFM(), + hide_calendar=hide_calendar, + hide_masks=hide_masks, + disable_progressive=disable_progressive, + unlocked=unlocked, + entry_pp=entry_pp, + exit_pp=exit_pp + ) + + + class MASFilterableBackground(object): """ Background class to get display props for bgs PROPERTIES: background_id - the id which defines this bg prompt - button label for the bg - image_map - Dict mapping all images for the bgs, keys are precip types (See MASWeather) + image_map - MASFilterWeatherMap object containing mappings of + filter + weather to images hide_calendar - whether or not we display the calendar with this hide_masks - whether or not we display the window masks disable_progressive - weather or not we disable progesssive weather @@ -30,24 +1837,17 @@ init -10 python: import store.mas_background as mas_background import store.mas_weather as mas_weather - def __init__( - self, + def __init__(self, background_id, prompt, - image_day, - image_night, - image_rain_day=None, - image_rain_night=None, - image_overcast_day=None, - image_overcast_night=None, - image_snow_day=None, - image_snow_night=None, + image_map, + filter_man, hide_calendar=False, hide_masks=False, disable_progressive=None, unlocked=False, entry_pp=None, - exit_pp=None + exit_pp=None, ): """ Constructor for background objects @@ -60,37 +1860,15 @@ init -10 python: prompt: button label for this bg - image_day: - the renpy.image object we use for this bg during the day - NOTE: Mandatory - - image_night: - the renpy.image object we use for this bg during the night - NOTE: Mandatory - - image_rain_day: - the image tag we use for the background while it's raining (day) - (Default: None, not required) - - image_rain_night: - the image tag we use for the background while it's raining (night) - (Default: None, not required) + image_map: + MASFilterWeatherMap of bg images to use. + Use image tags for MASWeatherMap values. - image_overcast_day: - the image tag we use for the background while it's overcast (day) - (Default: None, not required) + filter_man: + MASBackgroundFilterManager to use - image_overcast_night: - the image tag we use for the background while it's overcast (night) - (Default: None, not required) - - image_snow_day: - the image tag we use for the background while it's snowing (day) - (Default: None, not required) - - image_snow_night: - the image tag we use for the background while it's snowing (night) - (Default: None, not required) + backup_img: + image tag/image path to use as a backup hide_calendar: whether or not we want to display the calendar @@ -116,26 +1894,40 @@ init -10 python: Exit programming point for this background (Default: None) """ - + # sanity checks if background_id in self.mas_background.BACKGROUND_MAP: raise Exception("duplicate background ID") + if not isinstance(image_map, MASFilterWeatherMap): + raise TypeError( + "Expected MASFilterWeatherMap, got {0}".format( + type(image_map) + ) + ) + if not isinstance(filter_man, MASBackgroundFilterManager): + raise TypeError( + "Exepcted MASBackroundFilterManager, got {0}".format( + type(filter_man) + ) + ) self.background_id = background_id self.prompt = prompt - self.image_day = image_day - self.image_night = image_night - - - self.image_map = { - #Def - mas_weather.PRECIP_TYPE_DEF: (image_day, image_night), - #Rain - mas_weather.PRECIP_TYPE_RAIN: (image_rain_day if image_rain_day else image_day, image_rain_night if image_rain_night else image_night), - #Overcast - mas_weather.PRECIP_TYPE_OVERCAST: (image_overcast_day if image_overcast_day else image_day, image_overcast_night if image_overcast_night else image_night), - #Snow - mas_weather.PRECIP_TYPE_SNOW: (image_snow_day if image_snow_day else image_day, image_snow_night if image_snow_night else image_night) - } + self.image_map = image_map + self._flt_man = filter_man + + # internal mapping of filters to their latest image. + # see MASBackgroundFilterManager.backmap for explanation. + # we use this to ensure that every filter has an appropriate + # set of BG images to use. + # key: filter + # value: filter to check for images + self._flt_img_map = {} + + # reverse map of the above, mapping filters-with-images to lists + # of dependent filters. + # key: filter + # value: list of filters that use the key's images. + self._flt_img_anc = {} #Then the other props self.hide_calendar = hide_calendar @@ -154,19 +1946,73 @@ init -10 python: # add to background map self.mas_background.BACKGROUND_MAP[background_id] = self - def __eq__(self, other): - if isinstance(other, MASBackground): + if isinstance(other, MASFilterableBackground): return self.background_id == other.background_id return NotImplemented - def __ne__(self, other): result = self.__eq__(other) if result is NotImplemented: return result return not result + def build(self): + """ + Builds filter slices using current suntimes. + Also builds appropraite BG image maps. + + NOTE: should only be called during init. + NOTE: IF YOU PLAN TO CALL UPDATE AFTER THIS, use buildupdate + instead. + """ + # build filter slices + self._flt_man.build( + persistent._mas_sunrise * 60, + persistent._mas_sunset * 60 + ) + + # now build flt image maps + self._flt_img_map = self._flt_man.backmap(self._flt_img_anc) + + def buildupdate(self, curr_time=None): + """ + Builds filter slices appropriately, then runs update. + This will set prev_flt correctly when doing an update after a + build. + + IN: + curr_time - see MASFilterableBackground.update + """ + if store.mas_background.dbg_log: + store.mas_utils.writelog("\nCalled from - bupd\n") + if store.mas_background.dbg_log_st: + store.mas_utils.writestack() + + store.mas_utils.writelog( + store.mas_background.DBG_MSG_C.format( + self._flt_man.current(), + str(self._flt_man.current_pos()) + ) + ) + + # build and update slices + self._flt_man.buildupdate( + persistent._mas_sunrise * 60, + persistent._mas_sunset * 60, + curr_time + ) + + # build flt image maps + self._flt_img_map = self._flt_man.backmap(self._flt_img_anc) + + if store.mas_background.dbg_log: + store.mas_utils.writelog( + store.mas_background.DBG_MSG_NU.format( + self._flt_man.current(), + str(self._flt_man.current_pos()) + ) + ) def entry(self, old_background): """ @@ -175,7 +2021,6 @@ init -10 python: if self.entry_pp is not None: self.entry_pp(old_background) - def exit(self, new_background): """ Run the exit programming point @@ -183,7 +2028,6 @@ init -10 python: if self.exit_pp is not None: self.exit_pp(new_background) - def fromTuple(self, data_tuple): """ Loads data from tuple @@ -194,7 +2038,6 @@ init -10 python: """ self.unlocked = data_tuple[0] - def toTuple(self): """ Converts this MASWeather object into a tuple @@ -204,44 +2047,152 @@ init -10 python: """ return (self.unlocked,) + def getRoom(self, flt, weather=None): + """ + Gets room associated with the given flt and weather + This performs lookback and other checks to try and find an image. + + Calling this before init level 0 may result in undefined behavior. + + IN: + flt - filter to check + weather - weather to check. If None, we use the current + weather + (Default: None) + + RETURNS: room image, or None if not found + """ + precip_type = MASFilterableWeather.getPrecipTypeFrom(weather) + + # get image using normal ways + img = self._get_image(flt, precip_type) + if img is None: + # no image, so we should try lookback + m_w_m = self._lookback(flt) + if m_w_m is not None: + img = m_w_m.get(precip_type) + + return img + + def getCurrentRoom(self): + """ + Gets current Room + + RETURNS: Current room image, may be None if this BG is badly built + """ + return self.getRoom(self._flt_man.current()) + def getDayRoom(self, weather=None): + """DEPRECATED + Can't use this anymore since there's no single image that defines + "day" anymore. It's all filter based. + See getDayRooms instead + """ + pass + + def getDayRooms(self, weather=None): + """ + Gets all day images for a weather. + + IN: + weather - weather to check. If None, we use the current + weather. + (Default: None) + + RETURNS: dict of the following format: + key: flt + value: day according to the weather. + NOTE: only filters that have a room with the given weather + are returned. No lookback. """ - Returns the day masks to use given the conditions/availablity of present assets + precip_type = MASFilterableWeather.getPrecipTypeFrom(weather) + + results = {} + for flt in self._flt_man.filters_day(): + img = self._get_image(flt, precip_type) + if img is not None: + results[flt] = img + + return results + + def _get_image(self, flt, precip_type): """ - if weather is None: - weather = store.mas_current_weather + Gets image associated with the given flt and precip_type + does NOT perform lookback checks. - return self.image_map[weather.precip_type][0] + IN: + flt - filter to check + precip_type - precip type to check + + RETURNS: image, or None if not found + """ + m_w_m = self.image_map.get(flt) + if m_w_m is None: + return None + return m_w_m.get(precip_type) def getNightRoom(self, weather=None): + """DEPRECATED + Can't use this anymore since there's no single image that defines + "night" anymore. It's all filter-based + See getNightRooms instead """ - Returns the night masks to use given the conditions/availablity of present assets + pass + + def getNightRooms(self, weather=None): + """ + Gets all night images for a weather. + + IN: + weather - weather to check. If None, we use the current + weather. + (Default: None) + + RETURNS: dict of the following format: + key: flt + value: night according to the weather. + NOTE: only filters that have a room with the given weather + are returned. No lookback. """ - if weather is None: - weather = store.mas_current_weather + precip_type = MASFilterableWeather.getPrecipTypeFrom(weather) - return self.image_map[weather.precip_type][1] + results = {} + for flt in self._flt_man.filters_night(): + img = self._get_image(flt, precip_type) + if img is not None: + results[flt] = img + + return results def getRoomForTime(self, weather=None): """ - Gets the room for the current time + Gets the room for the current time and desired weather + + NOTE: if you just want current room to use, use getCurrentRoom. IN: weather - get the room bg for the time and weather (Default: current weather) + + RETURNS: room image for the current weather and time """ - if weather is None: - weather = store.mas_current_weather - if store.mas_isMorning(): - return self.getDayRoom(weather) - return self.getNightRoom(weather) + return self.getRoom(self._flt_man.current(), weather) def isChangingRoom(self, old_weather, new_weather): """ - If the room has a different look for the new weather we're going into, the room is "changing" and we need to flag this to - scene change and dissolve the spaceroom in the spaceroom label + Checks if the room would change because of a change in weather + + IN: + old_weather - weather to start from + new_weather - weather to change to + + RETURNS: true if the room would change, False otherwise """ - return self.getRoomForTime(old_weather) != self.getRoomForTime(new_weather) + curr_flt = self._flt_man.current() + return ( + self._get_image(curr_flt, old_weather.precip_type) + != self._get_image(curr_flt, new_weather.precip_type) + ) def isFltDay(self, flt=None): """ @@ -254,13 +2205,10 @@ init -10 python: RETURNS: True if flt is a "day" filter according to this bg """ - # TODO: a BG will be in charge of which filters are "day" and - # which are "night". This will be implemented in the future. - # for now we just assume "day" is day and "night" is night if flt is None: flt = store.mas_sprites.get_filter() - return flt == store.mas_sprites.FLT_DAY + return self._flt_man.is_flt_day(flt) def isFltNight(self, flt=None): """ @@ -273,15 +2221,185 @@ init -10 python: RETURNS: True if flt is a "night" filter according to this BG """ - # TODO: see isFltDay return not self.isFltDay(flt) + def _lookback(self, flt): + """ + Gets MASWeatherMap for a filter, using lookback + + IN: + flt - filter to check + + RETURNS: MASWeatherMap, or None if not found + """ + return self.image_map.get(self._flt_img_map.get(flt)) + + def progress(self): + """ + Progresses the filter. + If update was called before this, then we only run the global + filter change progpoint if there was a filter change. + + RETURNS: the new filter + """ + if store.mas_background.dbg_log: + store.mas_utils.writelog("\nCalled from - prog\n") + if store.mas_background.dbg_log_st: + store.mas_utils.writestack() + + store.mas_utils.writelog( + store.mas_background.DBG_MSG_C.format( + self._flt_man.current(), + str(self._flt_man.current_pos()) + ) + ) + + new_flt = self._flt_man.progress() + + if store.mas_background.dbg_log: + store.mas_utils.writelog( + store.mas_background.DBG_MSG_N.format( + new_flt, + self._flt_man.current(), + str(self._flt_man.current_pos()) + ) + ) + + return new_flt + + def update(self, curr_time=None): + """ + Updates the internal indexes. + NOTE: this will NOT call any progpoints. Call progress after this + to run (some) progpoints if needed. + NOTE: IF YOU PLAN TO CALL BUILD BEFORE THIS, use buildupdate + instead. + + IN: + curr_time - datetime.time object to update internal indexes to + if None, then we use current. + (Default: None) + """ + if store.mas_background.dbg_log: + store.mas_utils.writelog("\nCalled from - upd:\n") + if store.mas_background.dbg_log_st: + store.mas_utils.writestack() + + store.mas_utils.writelog( + store.mas_background.DBG_MSG_C.format( + self._flt_man.current(), + str(self._flt_man.current_pos()) + ) + ) + + self._flt_man.update(curr_time) + + if store.mas_background.dbg_log: + store.mas_utils.writelog( + store.mas_background.DBG_MSG_NU.format( + self._flt_man.current(), + str(self._flt_man.current_pos()) + ) + ) + + def verify(self): + """ + Verifies all internal filter and weather data is valid. + Raises exception upon errors. + Assumed to be called at least at init level 0 + """ + self._flt_man.verify() + self._verify_img_flts(self._flt_man._day_filters.keys()) + self._verify_img_flts(self._flt_man._night_filters.keys()) + + def _verify_img_flts(self, flts): + """ + Verifies that at least one image exists for the given flts. + Also organizes filters that have a default image + + Raises an exception if no images found + + IN: + flts - list of filters to check + """ + img_found = False + for flt in flts: + m_w_m = self.image_map.get(flt) + if m_w_m is not None: + img = m_w_m.get(store.mas_weather.PRECIP_TYPE_DEF) + if img is not None: + img_found = True + self._flt_img_anc[flt] = [] + return + + if not img_found: + raise Exception("No images found for these filters") + #Helper methods and such init -20 python in mas_background: import store BACKGROUND_MAP = {} BACKGROUND_RETURN = "Nevermind" + dbg_log = False + dbg_log_st = False + DBG_MSG_C = "\nCurrent: {0} | {1}\n" + DBG_MSG_N = "\nNew: ret: {0} | {1} | {2}\n" + DBG_MSG_NU = "\nNew: {0} | {1}\n" + + def build(): + """ + Builds all background objects using current time settings. + """ + for flt_bg in BACKGROUND_MAP.itervalues(): + flt_bg.build() + + + def buildupdate(): + """ + Builds all background objects and updates current time settings. + This properly saves prev_flt. + """ + prev_flt = store.mas_current_background._flt_man.current() + build() + store.mas_current_background.update() + store.mas_current_background._flt_man._prev_flt = prev_flt + + + def default_MBGFM(): + """ + Generates a MASBackgroundFilterManager using the default + (aka pre0.11.3) settings. + + RETURNS: MASBackgroundFilterManager object + """ + return store.MASBackgroundFilterManager( + store.MASBackgroundFilterChunk( + False, + None, + store.MASBackgroundFilterSlice.cachecreate( + store.mas_sprites.FLT_NIGHT, + 60 + ) + ), + store.MASBackgroundFilterChunk( + True, + None, + store.MASBackgroundFilterSlice.cachecreate( + store.mas_sprites.FLT_DAY, + 60 + ) + ), + store.MASBackgroundFilterChunk( + False, + None, + store.MASBackgroundFilterSlice.cachecreate( + store.mas_sprites.FLT_NIGHT, + 60 + ) + ), + ) + def loadMBGData(): """ @@ -320,20 +2438,26 @@ init 800 python: """ Sets the initial bg + Does not do anything if the current bg is same. + NOTE: We don't handle exit pp's here IN: _background: - The background we're changing to + The background we're changing to. + Assumes this is already built. """ - global mas_current_background - old_background = mas_current_background - mas_current_background = _background - mas_current_background.entry(old_background) + if _background != mas_current_background: + global mas_current_background + old_background = mas_current_background + mas_current_background = _background + mas_current_background.entry(old_background) def mas_changeBackground(new_background, by_user=None, set_persistent=False): """ - changes the background w/o any scene changes + changes the background w/o any scene changes. Will not run progpoints + or do any actual bg changes if the current background is already set to + the background we are changing to. IN: new_background: @@ -351,8 +2475,9 @@ init 800 python: if set_persistent: persistent._mas_current_background = new_background.background_id - mas_current_background.exit(new_background) - mas_setBackground(new_background) + if new_background != mas_current_background: + mas_current_background.exit(new_background) + mas_setBackground(new_background) def mas_startupBackground(): """ @@ -390,10 +2515,85 @@ init 800 python: mas_setBackground(mas_background_def) - #START: Programming points init -2 python in mas_background: import store + import store.mas_sprites as mspr + + + def run_gbl_flt_change(old_flt, new_flt, curr_time): + """ + Runs global filter change progpoint, logging for errors + + IN: + See _gbl_flt_change + """ + try: + _gbl_flt_change(old_flt, new_flt, curr_time) + except Exception as e: + store.mas_utils.writelog( + store.MASBackgroundFilterChunk._ERR_PP_STR_G.format( + repr(e), + old_flt, + new_flt + ) + ) + + + def _gbl_flt_change(old_flt, new_flt, curr_time): + """ + Runs when a filter change occurs + + IN: + old_flt - outgoing filter. Will be None on startup. + new_flt - incoming filter. + curr_time - current time as datetime.time + """ + if new_flt == mspr.FLT_DAY or new_flt == mspr.FLT_NIGHT: + # allow islands to be shown + store.mas_unflagEVL( + "mas_monika_islands", + "EVE", + store.EV_FLAG_HFM + ) + store.mas_unflagEVL( + "greeting_ourreality", + "GRE", + store.EV_FLAG_HFRS + ) + else: + # hide islands + store.mas_flagEVL("mas_monika_islands", "EVE", store.EV_FLAG_HFM) + store.mas_flagEVL("greeting_ourreality", "GRE", store.EV_FLAG_HFRS) + + + def _gbl_chunk_change(old_chunk, new_chunk, curr_time): + """ + Runs when a chunk change occurs + + IN: + old_chunk - outgoing chunk, will be None on startup + new_flt - incoming chunk + curr_time - current time as datetime.time + """ + first_flt = new_chunk.first_flt() + if first_flt == mspr.FLT_DAY or first_flt == mspr.FLT_NIGHT: + # allow islands to be shown + store.mas_unflagEVL( + "mas_monika_islands", + "EVE", + store.EV_FLAG_HFM + ) + store.mas_unflagEVL( + "greeting_ourreality", + "GRE", + store.EV_FLAG_HFRS + ) + else: + # hide islands + store.mas_flagEVL("mas_monika_islands", "EVE", store.EV_FLAG_HFM) + store.mas_flagEVL("greeting_ourreality", "GRE", store.EV_FLAG_HFRS) + def _def_background_entry(_old): """ @@ -429,43 +2629,104 @@ init -2 python in mas_background: #START: bg defs init -1 python: #Default spaceroom - mas_background_def = MASBackground( - #Identification + mas_background_def = MASFilterableBackground( + # ID "spaceroom", "Spaceroom", - #Day/Night - "monika_day_room", - "monika_room", - - #Rain Day/Night - image_rain_day="monika_rain_room", - - image_overcast_day="monika_rain_room", + # mapping of filters to MASWeatherMaps + MASFilterWeatherMap( + day=MASWeatherMap({ + store.mas_weather.PRECIP_TYPE_DEF: "monika_day_room", + store.mas_weather.PRECIP_TYPE_RAIN: "monika_rain_room", + store.mas_weather.PRECIP_TYPE_OVERCAST: "monika_rain_room", + store.mas_weather.PRECIP_TYPE_SNOW: "monika_snow_room_day", + }), + night=MASWeatherMap({ + store.mas_weather.PRECIP_TYPE_DEF: "monika_room", + store.mas_weather.PRECIP_TYPE_SNOW: "monika_snow_room_night", + }), + sunset=MASWeatherMap({ + store.mas_weather.PRECIP_TYPE_DEF: "monika_ss_room", + store.mas_weather.PRECIP_TYPE_RAIN: "monika_rain_room_ss", + store.mas_weather.PRECIP_TYPE_OVERCAST: "monika_rain_room_ss", + store.mas_weather.PRECIP_TYPE_SNOW: "monika_snow_room_ss", + }), + ), - image_snow_day="monika_snow_room_day", - image_snow_night="monika_snow_room_night", + # filter manager + MASBackgroundFilterManager( + MASBackgroundFilterChunk( + False, + None, + MASBackgroundFilterSlice.cachecreate( + store.mas_sprites.FLT_NIGHT, + 60 + ) + ), + MASBackgroundFilterChunk( + True, + None, + MASBackgroundFilterSlice.cachecreate( + store.mas_sprites.FLT_SUNSET, + 60, + 30*60, + 10, + ), + MASBackgroundFilterSlice.cachecreate( + store.mas_sprites.FLT_DAY, + 60 + ), + MASBackgroundFilterSlice.cachecreate( + store.mas_sprites.FLT_SUNSET, + 60, + 30*60, + 10, + ), + ), + MASBackgroundFilterChunk( + False, + None, + MASBackgroundFilterSlice.cachecreate( + store.mas_sprites.FLT_NIGHT, + 60 + ) + ) + ), - #Def room should always be unlocked unlocked=True, - - #Programming points for the spaceroom entry_pp=store.mas_background._def_background_entry, - exit_pp=store.mas_background._def_background_exit + exit_pp=store.mas_background._def_background_exit, ) #Now load data store.mas_background.loadMBGData() + +init 1 python in mas_background: + + # verify all backgrounds + for flt_bg in BACKGROUND_MAP.itervalues(): + flt_bg.verify() + #START: Image definitions #Spaceroom image monika_day_room = "mod_assets/location/spaceroom/spaceroom.png" image monika_room = "mod_assets/location/spaceroom/spaceroom-n.png" +image monika_ss_room = MASFilteredSprite( + store.mas_sprites.FLT_SUNSET, + "mod_assets/location/spaceroom/spaceroom.png" +) #Thanks Orca image monika_rain_room = "mod_assets/location/spaceroom/spaceroom_rain.png" +image monika_rain_room_ss = MASFilteredSprite( + store.mas_sprites.FLT_SUNSET, + "mod_assets/location/spaceroom/spaceroom_rain.png" +) #Thanks Velius/Orca image monika_snow_room_day = "mod_assets/location/spaceroom/spaceroom_snow.png" image monika_snow_room_night = "mod_assets/location/spaceroom/spaceroom_snow-n.png" +image monika_snow_room_ss = "mod_assets/location/spaceroom/spaceroom_ss_snow.png" #TODO: locking/unlocking of this based on other backgrounds #START: Location Selector diff --git a/Monika After Story/game/zz_history.rpy b/Monika After Story/game/zz_history.rpy index 53276a4316..ac17092c96 100644 --- a/Monika After Story/game/zz_history.rpy +++ b/Monika After Story/game/zz_history.rpy @@ -981,6 +981,7 @@ init -810 python: # lifestyle / smoking "_mas_pm_do_smoke": "pm.lifestyle.smoking.smokes", "_mas_pm_do_smoke_quit": "pm.lifestyle.smoking.trying_to_quit", + "_mas_pm_do_smoke_quit_succeeded_before": "pm.lifestyle.smoking.successfully_quit_before", # lifestyle / food "_mas_pm_eat_fast_food": "pm.lifestyle.food.eats_fast_food", diff --git a/Monika After Story/game/zz_hotkeys.rpy b/Monika After Story/game/zz_hotkeys.rpy index 9bbd623879..c7e7424c09 100644 --- a/Monika After Story/game/zz_hotkeys.rpy +++ b/Monika After Story/game/zz_hotkeys.rpy @@ -198,17 +198,48 @@ init python: if store.mas_hotkeys.bookmark_enabled and not _windows_hidden: mas_bookmark_topic() + + def _mas_game_menu_start(scope): + """ + Runs code prior to opening the game menu in any way. + + OUT: + scope - use this dict as temp space + """ + scope["disb_ani"] = persistent._mas_disable_animations + scope["sr_time"] = store.mas_suntime.sunrise + scope["ss_time"] = store.mas_suntime.sunset + + + def _mas_game_menu_end(scope): + """ + Runs code after exiting the game menu in any way. + + IN: + scope - temp space used in `_mas_game_menu_start` + """ + # call backs for the game menu + if scope.get("disb_ani") != persistent._mas_disable_animations: + mas_drawSpaceroomMasks(dissolve_masks=False) + + if ( + scope.get("sr_time") != store.mas_suntime.sunrise + or scope.get("ss_time") != store.mas_suntime.sunset + ): + store.mas_background.buildupdate() + + def _mas_game_menu(): """ Wrapper aound _invoke_game_menu that follows additional ui rules """ if not _windows_hidden: - prev_disable_animations = persistent._mas_disable_animations + temp_space = {} + _mas_game_menu_start(temp_space) + _invoke_game_menu() - # call backs for the game menu - if prev_disable_animations != persistent._mas_disable_animations: - mas_drawSpaceroomMasks(dissolve_masks=False) + _mas_game_menu_end(temp_space) def _mas_quick_menu_cb(screen_name): @@ -217,15 +248,15 @@ init python: NOTE: no checks are done here, please do not fuck this. """ if not _windows_hidden: - prev_disable_animations = persistent._mas_disable_animations + temp_space = {} + _mas_game_menu_start(temp_space) + renpy.call_in_new_context( "_game_menu", _game_menu_screen=screen_name ) - # call backs for the game menu - if prev_disable_animations != persistent._mas_disable_animations: - mas_drawSpaceroomMasks(dissolve_masks=False) + _mas_game_menu_end(temp_space) def _mas_hide_windows(): diff --git a/Monika After Story/game/zz_weather.rpy b/Monika After Story/game/zz_weather.rpy index 4c129b397c..6f5f370f18 100644 --- a/Monika After Story/game/zz_weather.rpy +++ b/Monika After Story/game/zz_weather.rpy @@ -4,62 +4,83 @@ default persistent._mas_current_weather = "auto" ### spaceroom weather art #Big thanks to Legendkiller21/Orca/Velius for helping out with these -image def_weather_day = Movie( - channel="window_1", - play="mod_assets/window/def_day_mask.mp4", - mask=None +image def_weather = MASFallbackFilterDisplayable( + day=Movie( + channel="window_1", + play="mod_assets/window/def_day_mask.mp4", + mask=None + ), + sunset=Movie( + play="mod_assets/window/def_sunset_mask.mp4", + mask=None + ), + night=Movie( + channel="window_2", + play="mod_assets/window/def_night_mask.mp4", + mask=None + ) ) -image def_weather_day_fb = "mod_assets/window/def_day_mask_fb.png" - -image def_weather_night = Movie( - channel="window_2", - play="mod_assets/window/def_night_mask.mp4", - mask=None +image def_weather_fb = MASFallbackFilterDisplayable( + day="mod_assets/window/def_day_mask_fb.png", + sunset="mod_assets/window/def_sunset_mask_fb.png", + night="mod_assets/window/def_night_mask_fb.png", ) -image def_weather_night_fb = "mod_assets/window/def_night_mask_fb.png" -image rain_weather_day = Movie( - channel="window_3", - play="mod_assets/window/rain_day_mask.mpg", - mask=None +image rain_weather = MASFallbackFilterDisplayable( + day=Movie( + channel="window_3", + play="mod_assets/window/rain_day_mask.mpg", + mask=None + ), + night=Movie( + channel="window_4", + play="mod_assets/window/rain_night_mask.mpg", + mask=None + ) ) -image rain_weather_day_fb = "mod_assets/window/rain_day_mask_fb.png" - -image rain_weather_night = Movie( - channel="window_4", - play="mod_assets/window/rain_night_mask.mpg", - mask=None +image rain_weather_fb = MASFallbackFilterDisplayable( + day="mod_assets/window/rain_day_mask_fb.png", + night="mod_assets/window/rain_night_mask_fb.png", ) -image rain_weather_night_fb = "mod_assets/window/rain_night_mask_fb.png" -image overcast_weather_day = Movie( - channel="window_5", - play="mod_assets/window/overcast_day_mask.mpg", - mask=None +image overcast_weather = MASFallbackFilterDisplayable( + day=Movie( + channel="window_5", + play="mod_assets/window/overcast_day_mask.mpg", + mask=None + ), + night=Movie( + channel="window_6", + play="mod_assets/window/overcast_night_mask.mpg", + mask=None + ), ) -image overcast_weather_day_fb = "mod_assets/window/overcast_day_mask_fb.png" - -image overcast_weather_night = Movie( - channel="window_6", - play="mod_assets/window/overcast_night_mask.mpg", - mask=None +image overcast_weather_fb = MASFallbackFilterDisplayable( + day="mod_assets/window/overcast_day_mask_fb.png", + night="mod_assets/window/overcast_night_mask_fb.png", ) -image overcast_weather_night_fb = "mod_assets/window/overcast_night_mask_fb.png" -image snow_weather_day = Movie( - channel="window_7", - play="mod_assets/window/snow_day_mask.mp4", - mask=None +image snow_weather = MASFallbackFilterDisplayable( + day=Movie( + channel="window_7", + play="mod_assets/window/snow_day_mask.mp4", + mask=None + ), + sunset=Movie( + play="mod_assets/window/snow_sunset_mask.mp4", + mask=None + ), + night=Movie( + channel="window_8", + play="mod_assets/window/snow_night_mask.mp4", + mask=None + ), ) -image snow_weather_day_fb = "mod_assets/window/snow_day_mask_fb.png" - -image snow_weather_night = Movie( - channel="window_8", - play="mod_assets/window/snow_night_mask.mp4", - mask=None +image snow_weather_fb = MASFallbackFilterDisplayable( + day="mod_assets/window/snow_day_mask_fb.png", + sunset="mod_assets/window/snow_sunset_mask_fb.png", + night="mod_assets/window/snow_night_mask_fb.png", ) -image snow_weather_night_fb = "mod_assets/window/snow_night_mask_fb.png" - ## end spaceroom weather art @@ -184,7 +205,7 @@ init python in mas_weather: return None -init -20 python in mas_weather: +init -99 python in mas_weather: import random import datetime import store @@ -192,7 +213,6 @@ init -20 python in mas_weather: #NOTE: Not persistent since weather changes on startup force_weather = False - WEATHER_MAP = {} # weather constants @@ -208,9 +228,44 @@ init -20 python in mas_weather: PRECIP_TYPE_OVERCAST = "overcast" PRECIP_TYPE_SNOW = "snow" + # all precip types + PRECIP_TYPES = ( + PRECIP_TYPE_DEF, + PRECIP_TYPE_RAIN, + PRECIP_TYPE_OVERCAST, + PRECIP_TYPE_SNOW + ) + #Keep a temp store of weather here for if we're changing backgrounds temp_weather_storage = None + old_weather_tag = "mas_old_style_weather_{0}" + old_weather_id = 1 + + OLD_WEATHER_OBJ = {} + # key: generated ID + # value: assocaited displayable + + def _generate_old_image(disp): + """ + Generates an image for the old-style weather. + + IN: + disp - displayable to pass to renpy.image + + RETURNS: the created image tag + """ + global old_weather_id + tag = old_weather_tag.format(old_weather_id) + store.renpy.image(tag, disp) + OLD_WEATHER_OBJ[old_weather_id] = tag + old_weather_id += 1 + + return tag + + +init -20 python in mas_weather: + # def canChangeWeather(): # """ # Returns true if the user can change weather @@ -416,8 +471,78 @@ init -20 python in mas_weather: init -10 python: - # weather class - class MASWeather(object): + + def MASWeather( + weather_id, + prompt, + sp_day, + sp_night=None, + precip_type=store.mas_weather.PRECIP_TYPE_DEF, + isbg_wf_day=None, + isbg_wof_day=None, + isbg_wf_night=None, + isbg_wof_night=None, + entry_pp=None, + exit_pp=None, + unlocked=False + ): + """DEPRECATED + Old-style MASWeather objects. + This is mapped to a MASFilterableWeather with day/night filter settings + NOTE: for all image tags, `_fb` is appeneded for fallbacks + + IN: + weather_id - id that defines this weather object + NOTE: must be unique + prompt - button label for this weathe robject + sp_day - image tag for spaceroom's left window in daytime + sp_night - image tag for spaceroom's left window in night + (Default: None) + precip_type - type of precipitation, def, rain, overcast, or snow + (Default: def) + isbg_wf_day - ignored + isbg_wof_day - ignored + isbg_wf_night - ignored + isbg_wof_night - ignored + entry_pp - programming point to execute after switching to + this weather + (Default: None) + exit_pp - programming point to execute before leaving this + weather + (Default: None) + unlocked - True if this weather object starts unlocked, + False otherwise + (Default: False) + + RETURNS: MASFitlerableWeather object + """ + if sp_night is None: + sp_night = sp_day + + sp_day_fb = sp_day + "_fb" + sp_night_fb = sp_night + "_fb" + + # create weather images + dyn_tag = store.mas_weather._generate_old_image( + MASFallbackFilterDisplayable(day=sp_day, night=sp_night) + ) + stt_tag = store.mas_weather._generate_old_image( + MASFallbackFilterDisplayable(day=sp_day_fb, night=sp_night_fb) + ) + + return MASFilterableWeather( + weather_id, + prompt, + stt_tag, + ani_img_tag=dyn_tag, + precip_type=precip_type, + unlocked=unlocked, + entry_pp=entry_pp, + exit_pp=exit_pp + ) + + + class MASFilterableWeather(object): """ Weather class to determine some props for weather @@ -425,117 +550,74 @@ init -10 python: weather_id - Id that defines this weather object prompt - button label for this weater unlocked - determines if this weather is unlocked/selectable - sp_day - image tag for windows in day time - sp_night - image tag for windows in nighttime precip_type - type of precipitation (to use for the room type) - isbg_wf_day - image PATH for islands bg daytime with frame - isbg_wof_day = image PATH for islands bg daytime without frame - isbg_wf_night - image PATH for island bg nighttime with frame - isbg_wof_night - image PATH for island bg nighttime without framme - + img_tag - image tag to use for the static version of weather + ani_img_tag - image tag to use for the animated version of weather entry_pp - programming point to execute when switching to this weather exit_pp - programming point to execute when leaving this weather - - NOTE: for all image tags, `_fb` is appeneded for fallbacks """ import store.mas_weather as mas_weather - def __init__( - self, + def __init__(self, weather_id, prompt, - sp_day, - sp_night=None, + img_tag, + ani_img_tag=None, precip_type=store.mas_weather.PRECIP_TYPE_DEF, - isbg_wf_day=None, - isbg_wof_day=None, - isbg_wf_night=None, - isbg_wof_night=None, + unlocked=False, entry_pp=None, - exit_pp=None, - unlocked=False - ): + exit_pp=None + ): """ - Constructor for a MASWeather object + Constructor for a MASFilterableWeather object IN: weather_id - id that defines this weather object NOTE: must be unique prompt - button label for this weathe robject - sp_day - image tag for spaceroom's left window in daytime - unlocked - True if this weather object starts unlocked, - False otherwise - (Default: False) - sp_night - image tag for spaceroom's left window in night - If None, we use sp_day for this + img_tag - image tag to use for the static version of weather + ani_img_tag - image tag to use for the animated version of + weather. If None, we always use the static version. (Default: None) precip_type - type of precipitation, def, rain, overcast, or snow (Default: def) - isbg_wf_day - image PATH for islands bg daytime with frame - (Default: None) - isbg_wof_day = image PATH for islands bg daytime without frame - (Default: None) - isbg_wf_night - image PATH for island bg nighttime with frame - If None, we use isbg_wf_day - (Default: None) - isbg_wof_night - image PATH for island bg nighttime without - framme - If None, we use isbg_wof_day - (Default: None) + unlocked - True if this weather object starts unlocked, + False otherwise + (Default: False) entry_pp - programming point to execute after switching to this weather (Default: None) exit_pp - programming point to execute before leaving this weather (Default: None) - - #NOTE: Defaulting to the day frame stuff to avoid tracebacks """ if weather_id in self.mas_weather.WEATHER_MAP: raise Exception("duplicate weather ID") self.weather_id = weather_id self.prompt = prompt - self.sp_day = sp_day - self.sp_night = sp_night + self.img_tag = img_tag + self.ani_img_tag = ani_img_tag self.precip_type = precip_type - self.isbg_wf_day = isbg_wf_day - self.isbg_wof_day = isbg_wof_day - self.isbg_wf_night = isbg_wf_night - self.isbg_wof_night = isbg_wof_night self.unlocked = unlocked self.entry_pp = entry_pp self.exit_pp = exit_pp - # clean day/night - if sp_night is None: - self.sp_night = sp_day - - # clean islands - if isbg_wf_night is None: - self.isbg_wf_night = isbg_wf_day - - if isbg_wof_night is None: - self.isbg_wof_night = isbg_wof_day - # add to weather map self.mas_weather.WEATHER_MAP[weather_id] = self - def __eq__(self, other): - if isinstance(other, MASWeather): + if isinstance(other, MASFilterableWeather): return self.weather_id == other.weather_id return NotImplemented - def __ne__(self, other): result = self.__eq__(other) if result is NotImplemented: return result return not result - def entry(self, old_weather): """ Runs entry programming point @@ -543,7 +625,6 @@ init -10 python: if self.entry_pp is not None: self.entry_pp(old_weather) - def exit(self, new_weather): """ Runs exit programming point @@ -551,6 +632,32 @@ init -10 python: if self.exit_pp is not None: self.exit_pp(new_weather) + def get_mask(self): + """ + Returns the appropriate weathermask based on animation settings + + RETURNS: image tag to use + """ + if persistent._mas_disable_animations or self.ani_img_tag is None: + return self.img_tag + + return self.ani_img_tag + + @staticmethod + def getPrecipTypeFrom(weather=None): + """ + Gets precip type of the given weather object. + + IN: + weather - weather object to get precip type for. + if None, we use the current weather + (Default: None) + + RETURNS: precip_type + """ + if weather is None: + return mas_current_weather.precip_type + return weather.precip_type def fromTuple(self, data_tuple): """ @@ -562,189 +669,335 @@ init -10 python: """ self.unlocked = data_tuple[0] - def sp_window(self, day): + """DEPRECATED + Use get_mask instead. + This returns whatever get_mask returns. + """ + return self.get_mask() + + def isbg_window(self, day, no_frame): + """DEPRECATED + Islands are now separate images. See script-islands-event. + """ + return "" + + def toTuple(self): + """ + Converts this MASWeather object into a tuple + + RETURNS: tuple of the following format: + [0]: unlocked property + """ + return (self.unlocked,) + + + class MASWeatherMap(object): + """ + A weather map is an extension of MASHighlightMap except using precip + types as keys. + + NOTE: actual implementation is by wrapping around MASHighlightMap. + This is to avoid calling functions that would crash. + + PROPERTIES: + None + """ + + def __init__(self, precip_map=None): + """ + Constructor + + IN: + precip_map - mapping of precip types and values to map to + key: precip type + value: value to map to precip type + NOTE: not required, you can also use add functions instead + NOTE: PRECIP_TYPE_DEF is used as a default if given. + """ + self.__mhm = MASHighlightMap.create_from_mapping( + store.mas_weather.PRECIP_TYPES, + None, + precip_map + ) + + def __iter__(self): + """ + Returns MHM iterator """ - Returns spaceroom masks for window + return iter(self.__mhm) + + def add(self, key, value): + """ + Adds value to map. + See MASHighlightMap.add + """ + self.__mhm.add(key, value) + + def apply(self, mapping): + """ + Applies a dict mapping to this map. + See MASHlightMap.apply + """ + self.__mhm.apply(mapping) + + def get(self, key): + """ + Gets value with the given key + + Uses PRECIP_TYPE_DEF as a default if key not found + + See MASHighlightMap.get + """ + value = self._raw_get(key) + if value is None: + return self._raw_get(store.mas_weather.PRECIP_TYPE_DEF) + + return value + + def _mhm(self): + """ + Returns the internal MASHighlightMap. Only use if you know what + you are doing. + + RETURNS: MASHighlightMap object + """ + return self.__mhm + + def _raw_get(self, precip_type): + """ + Gets value with given precip_type. this does Not do defaulting. IN: - day - True if we want day time masks + precip_type - precip type to get value for - RETURNS: - image tag for the corresponding mask to use + RETURNS: value """ - # TODO: swap to filter-based - if day: - return self.sp_day + return self.__mhm.get(precip_type) - return self.sp_night + class MASFilterWeatherMap(MASFilterMapSimple): + """ + Extension of MASFilterMap. - def isbg_window(self, day, no_frame): + Use this to map weather maps to filters. + + NOTE: this does NOT verify filters. + + PROPERTIES: + use_fb - if True, this will default to using fallback-based + getting when using fw_get. + Defaults to False and must be set after creation. + """ + + def __init__(self, **filter_pairs): """ - Returns islands bg PATH for window + Constructor + + Will throw exceptions if not given MASWeatherMap objects IN: - day - True if we want daytime bg - no_frame - True if we want no frame + **filter_pairs - filter=val args to use. Invalid filters are + ignored. Values should be MASWeatherMap objects. """ - if day: - if no_frame: - return self.isbg_wof_day + # validate MASWeatherMap objects + for wmap in filter_pairs.itervalues(): + if not isinstance(wmap, MASWeatherMap): + raise TypeError( + "Expected MASWeatherMap object, not {0}".format( + type(wmap) + ) + ) + + super(MASFilterWeatherMap, self).__init__(**filter_pairs) + self.use_fb = False + + def fw_get(self, flt, weather=None): + """ + Gets value from map based on filter and current weather. + May do fallback-based getting if configured to do so. - return self.isbg_wf_day + fallback-baesd getting uses the FLT_FB dict to find the NEXT + available value for a given precip_type. This overrides the + MASWeatherMap's handling of using PRECIP_TYPE_DEF as a fallback + until the final MASWeatherMap in the chain. + For more, see MASFilterWeatherDisplayable in sprite-chart-matrix. - # else night - if no_frame: - return self.isbg_wof_night + IN: + flt - filter to lookup + weather - weather to lookup. If None, we use the current + weather. + (Default: None) - return self.isbg_wf_night + RETURNS: value for the given filter and weather + """ + return self._raw_fw_get( + flt, + MASFilterableWeather.getPrecipTypeFrom(weather) + ) + def get(self, flt): + """ + Gets value from map based on filter. - def toTuple(self): + IN: + flt - filter to lookup + + RETURNS: value for the given filter """ - Converts this MASWeather object into a tuple + return self._raw_get(flt) - RETURNS: tuple of the following format: - [0]: unlocked property + def has_def(self, flt): """ - return (self.unlocked,) + Checks if the given flt has a MASWeatherMap that contains a + non-None value for the default precip type. + IN: + flt - filter to check -### define weather objects here + RETURNS: True if the filter has a non-None default precip type, + False otherwise. + """ + wmap = self._raw_get(flt) + if wmap is not None: + return ( + wmap._raw_get(store.mas_weather.PRECIP_TYPE_DEF) + is not None + ) -init -1 python: + return False - # default weather (day + night) - mas_weather_def = MASWeather( - "def", - "Clear", + def _raw_fw_get(self, flt, precip_type): + """ + Gets the actual value from a filter and precip type. This may + do fallback-based getting if configured to do so. - # sp day - "def_weather_day", + NOTE: if the given filter doesn't have an associated MASWeatherMap, + we ALWAYS use the fallback-based system, but find the next + available default. See MASFilterWeatherDisplayable in + sprite-chart-matrix. - # sp night - "def_weather_night", + IN: + flt - filter to lookup + precip_type - precip type to lookup - precip_type=store.mas_weather.PRECIP_TYPE_DEF, + RETURNS: value for a given filter and precip type + """ + wmap = self._raw_get(flt) + if not self.use_fb and wmap is not None: + # if not use fallback get, then use the MASWeatherMap's + # default handling. + return wmap.get(precip_type) + + # otherwise, use our special handling + + # wmap could be None because of a not-defined filter. In that case + # set value to None so we can traverse filters until we find a + # valid wmap. + if wmap is not None: + value = wmap._raw_get(precip_type) + else: + value = None - # islands bg day - isbg_wf_day="mod_assets/location/special/with_frame.png", - isbg_wof_day="mod_assets/location/special/without_frame.png", + curr_flt = flt + while value is None: + nxt_flt = store.mas_sprites._rslv_flt(curr_flt) - # islands bg night - isbg_wf_night="mod_assets/location/special/night_with_frame.png", - isbg_wof_night="mod_assets/location/special/night_without_frame.png", + # if the filters match, we foudn the last one. + if nxt_flt == curr_flt: + # in this case, use standard MASWeatherMap handling. + if wmap is None: + # without a wmap, we cant do anything except fail. + return None - unlocked=True - ) + return wmap.get(precip_type) - # rain weather - mas_weather_rain = MASWeather( - "rain", - "Rain", + # otherwise, get the wmap if possible and check value + wmap = self._raw_get(nxt_flt) + if wmap is not None: + if self.use_fb: + value = wmap._raw_get(precip_type) + else: + # if not in fallback mode, use regular gets + value = wmap.get(precip_type) - # sp day and night - "rain_weather_day", + curr_flt = nxt_flt - # sp night - "rain_weather_night", + # non-None value means we use this + return value - precip_type=store.mas_weather.PRECIP_TYPE_RAIN, + def _raw_get(self, flt): + """ + Gets value from map based on filter. - # islands bg day - isbg_wf_day="mod_assets/location/special/rain_with_frame.png", - isbg_wof_day="mod_assets/location/special/rain_without_frame.png", + IN: + flt - filter to lookup - # islands bg night - isbg_wf_night="mod_assets/location/special/night_rain_with_frame.png", - isbg_wof_night="mod_assets/location/special/night_rain_without_frame.png", + RETURNS: value for the given filter + """ + return super(MASFilterWeatherMap, self).get(flt) - entry_pp=store.mas_weather._weather_rain_entry, - exit_pp=store.mas_weather._weather_rain_exit, +### define weather objects here + +init -1 python: + + # default weather (day + night) + mas_weather_def = MASFilterableWeather( + "def", + "Clear", + "def_weather_fb", + "def_weather", + precip_type=store.mas_weather.PRECIP_TYPE_DEF, unlocked=True ) + # rain weather + mas_weather_rain = MASFilterableWeather( + "rain", + "Rain", + "rain_weather_fb", + "rain_weather", + precip_type=store.mas_weather.PRECIP_TYPE_RAIN, + unlocked=True, + entry_pp=store.mas_weather._weather_rain_entry, + exit_pp=store.mas_weather._weather_rain_exit, + ) + # snow weather - mas_weather_snow = MASWeather( + mas_weather_snow = MASFilterableWeather( "snow", "Snow", - - # sp day - "snow_weather_day", - - # sp night - "snow_weather_night", - + "snow_weather_fb", + "snow_weather", precip_type=store.mas_weather.PRECIP_TYPE_SNOW, - - # islands bg day - isbg_wf_day="mod_assets/location/special/snow_with_frame.png", - isbg_wof_day="mod_assets/location/special/snow_without_frame.png", - - # islands bg night - isbg_wf_night="mod_assets/location/special/night_snow_with_frame.png", - isbg_wof_night="mod_assets/location/special/night_snow_without_frame.png", - + unlocked=True, entry_pp=store.mas_weather._weather_snow_entry, exit_pp=store.mas_weather._weather_snow_exit, - - unlocked=True ) # thunder/lightning - mas_weather_thunder = MASWeather( + mas_weather_thunder = MASFilterableWeather( "thunder", "Thunder/Lightning", - - # sp day and night - "rain_weather_day", - - # sp night - "rain_weather_night", - + "rain_weather_fb", + "rain_weather", precip_type=store.mas_weather.PRECIP_TYPE_RAIN, - - # islands bg day - isbg_wf_day="mod_assets/location/special/rain_with_frame.png", - isbg_wof_day="mod_assets/location/special/rain_without_frame.png", - - # islands bg night - isbg_wf_night="mod_assets/location/special/night_rain_with_frame.png", - isbg_wof_night="mod_assets/location/special/night_rain_without_frame.png", - + unlocked=True, entry_pp=store.mas_weather._weather_thunder_entry, exit_pp=store.mas_weather._weather_thunder_exit, - - unlocked=True ) #overcast - mas_weather_overcast = MASWeather( + mas_weather_overcast = MASFilterableWeather( "overcast", "Overcast", - - # sp day - "overcast_weather_day", - - # sp night - "overcast_weather_night", - + "overcast_weather_fb", + "overcast_weather", precip_type=store.mas_weather.PRECIP_TYPE_OVERCAST, - - # islands bg day - isbg_wf_day="mod_assets/location/special/overcast_with_frame.png", - isbg_wof_day="mod_assets/location/special/overcast_without_frame.png", - - # islands bg night - isbg_wf_night="mod_assets/location/special/night_overcast_with_frame.png", - isbg_wof_night="mod_assets/location/special/night_overcast_without_frame.png", - + unlocked=True, entry_pp=store.mas_weather._weather_overcast_entry, exit_pp=store.mas_weather._weather_overcast_exit, - - unlocked=True ) ### end defining weather objects diff --git a/tools/menutils.py b/tools/menutils.py index f7f57c7e11..6e434d8186 100644 --- a/tools/menutils.py +++ b/tools/menutils.py @@ -4,13 +4,13 @@ # to run a menu, prepare a list of menu entries. Each entry should consist of # a tuple of the following format: # [0]: Text to display for this entry -# [1]: return value +# [1]: return value # # NOTE: the first item of the list should be a tuple of thef ollowing format: # [0]: Title / header to display # [1]: Prompt text # -# the menu generator function (menu) will add an exit / back option +# the menu generator function (menu) will add an exit / back option # automatically. this option always returns None import os @@ -142,7 +142,7 @@ def paginate(title, items, per_page=20, str_func=str): IN: title - title to show at the top of each page items - list of items to show - per_page - number of items to show per page + per_page - number of items to show per page Only accepts values between 10 - 50 (Default: 20) str_func - function to use to convert an item into a string @@ -222,7 +222,7 @@ def restrict(page_value): if page < last_page: # default to next page page += 1 - + else: # otherwise, quit return @@ -232,7 +232,7 @@ def ask(question, def_no=True): Ask user something. Only allows for yes/no selections. IN: - question - question to ask. a ? and the (y/n) will be applied + question - question to ask. a ? and the (y/n) will be applied automatically. def_no - True will default No, False will default yes diff --git a/tools/sprite.py b/tools/sprite.py index 9ff7bc1e63..4850d393ea 100644 --- a/tools/sprite.py +++ b/tools/sprite.py @@ -8,9 +8,9 @@ class StaticSprite(object): """ A static sprite is a sprite that knows its sprite codes and more - + PROPERTIES: - invalid - True if this sprite object should be treated as invalid, + invalid - True if this sprite object should be treated as invalid, False otherwise """ _sprite_map = { @@ -168,7 +168,7 @@ class StaticSprite(object): _dbl_tab = " " * 8 _tri_tab = " " * 12 _for_tab = " " * 16 - + _img_monika = "image monika " _q_monika_static = '"monika {0}_static"' _q_monika = '"monika {0}"' @@ -248,7 +248,7 @@ def __str__(self): '"', ] - # now for the position lines + # now for the position lines if self.is_lean: # leaning uses both arms and lean, as well as single lean, arms = self.position @@ -360,7 +360,7 @@ def __str__(self): def alias_static(self): """ - returns a string where this sprite's sprite code just aliases its + returns a string where this sprite's sprite code just aliases its static version. """ return 'image monika {0} = "{1}"'.format(self.spcode, self.scstr()) @@ -374,6 +374,9 @@ def atlify(self): if self.is_wink_eyes(): return self.__atlify_wink() + if self.is_tear_spr() and not self.is_closed_eyes(): + return self.__atlify_tears() + return self.__atlify_closed() def is_closed_eyes(self): @@ -394,18 +397,27 @@ def is_wink_eyes(self): """ return self.eyes in self._wink_eyes + def is_tear_spr(self): + """ + Returns true if this is a tear sprite. False if not + """ + return ( + self.tears in self._mod_map["tears"] + and self.tears not in ["pooled", "dried"] + ) + def make_atl(self): """ - MAKES atl version of this sprite. + MAKES atl version of this sprite. - RETURNS: the atl STaticSprite, or None if we didnt need to make one + RETURNS: the atl StaticSprite, or None if we didnt need to make one """ if self.is_normal_eyes(): # normal eyes require cloesd sad return StaticSprite(self.__swap_eyes("d")) if self.is_wink_eyes(): - # wink eyes require normal + # wink eyes require normal return StaticSprite(self.__swap_eyes("e")) return None @@ -500,6 +512,13 @@ def as_is_wink_eyes(ss_obj): """ return ss_obj.is_wink_eyes() + @staticmethod + def as_is_tear_spr(ss_obj): + """ + static method version of is_tear_spr + """ + return ss_obj.is_tear_spr() + def _get_smap(self, mainkey, code, defval): """ Gets a value from the sprite map @@ -543,8 +562,8 @@ def _rip_sprite(self, spcode): # [-1] - the mouth # # all the remaining parts are in a specific order, but each has - # a specific prefix. - # NOTE: we are retiring eyebag codes (eb_) there is no reason for + # a specific prefix. + # NOTE: we are retiring eyebag codes (eb_) there is no reason for # these. # (e) will be saved for emotes @@ -618,7 +637,7 @@ def _rip_sprite(self, spcode): ) except Exception as e: # any sort of exception is a bad state - # TODO Chnage + # TODO Chnage self.invalid = False return @@ -720,6 +739,44 @@ def __atlify_wink(self): self._q_monika.format(self.__swap_eyes("e")) ]) + def __atlify_tears(self): + """ + Creates a tears version of an ATL. + """ + return "".join([ + self._img_monika, + self.spcode, + self._onl, + + self._tab, + self._block, + self._onl, + + self._dbl_tab, + self._q_monika_static.format(self.spcode), + "\n", + + self._dbl_tab, + self._block, + self._onl, + + self.__build_choice(self._tri_tab, self._for_tab, 9), + + self.__build_choice(self._tri_tab, self._for_tab, 11), + + self.__build_choice(self._tri_tab, self._for_tab, 12), + + self._dbl_tab, + self._q_monika_static.format(self.__swap_eyes("d")), + "\n", + + self._dbl_tab, + "0.15\n", + + self._dbl_tab, + self._repeat, + ]) + def __build_choice(self, first_indent, sec_indent, num): """ Builds a choice with teh appropriate num level @@ -898,7 +955,7 @@ def __process_tears(self, spcode, index, sorder, *prefixes): tears = self._get_smap("tears", "".join(fullcode), None) if tears is None: return False, 0 - + # otherwise ok self.tears = tears sorder.append(("".join(prefixes), "".join(fullcode))) diff --git a/tools/spritemaker.py b/tools/spritemaker.py index 6c47b0bfe4..5219df559b 100644 --- a/tools/spritemaker.py +++ b/tools/spritemaker.py @@ -160,7 +160,7 @@ def set_filter(self, category, code): def build_menu(category): """ Builds a menu based on the given category - :param category: one of the class constants + :param category: one of the class constants :returns: menu list usable by menutils. May return None if could not build list """ @@ -261,7 +261,7 @@ def _build_menu(category): def _status(self, useheader, - headerstring, + headerstring, shownose, showemote ): @@ -290,7 +290,7 @@ def _status(self, position = StaticSprite.lean_tostring(self.position) is_lean = True else: - position = self.position + position = self.position is_lean = self.is_lean # now add each filter piece @@ -509,7 +509,7 @@ def gen_sprite_files( skip_continue=True ): """ - Generates sprite files. + Generates sprite files. IN: sprites - the list of sprite objects to generate stuff for @@ -621,7 +621,7 @@ def make_sprite(sprite_db, sprite_db_keys): (FilterSprite.BLH, True), (FilterSprite.TRS, True), (FilterSprite.SWD, True), - # NOTE: emote skipped + # NOTE: emote skipped # FilterSprite.EMO, (FilterSprite.MTH, False), ) @@ -927,7 +927,7 @@ def run_gss(sprite_db, sprite_db_keys, quiet=False, sp_per_file=500): def run_mkspr(sprite_db, sprite_db_keys): """ - Makes a sprite. + Makes a sprite. Returns an updated sprite_db_keys, or None if no changes """ @@ -1018,7 +1018,7 @@ def run_lstc_setfilter(sprite_db, sprite_db_keys, ss_filter): category_menu = FilterSprite.build_menu(choice) if category_menu is not None: code = menutils.menu(category_menu) - + # set if not none if code is not None: ss_filter.set_filter(choice, code) @@ -1109,5 +1109,9 @@ def _load_sprites(): # otherwise add sprite_db[sprite_code] = sprite_obj - return sprite_db + # make as atl if possible + atl_sprite = sprite_obj.make_atl() + if atl_sprite is not None: + sprite_db[atl_sprite.spcode] = atl_sprite + return sprite_db