Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

username0000006

13
Posts
A member registered Nov 11, 2025

Recent community posts

Following this implementation of multiuse items in https://itch.io/post/14860080, the striked lines on comments https://itch.io/post/14821654 and https://itch.io/post/14821660 complement the gain_item Function for multiuse items.

;)

On version 0.2.1, the mechanic for multiple-use items, like the "Large Tub Of Ice Cream" that says it has 3 uses in the item description, is not implemented yet.

Maybe it is plained to take into account side effects according to uses, but for now, a quick fix to enable this straightforward mechanic is to check "uses" property on the item dictionary on inventory ("current uses" consumed) and "uses" property on the item data definitions ("maximum uses") before deleting the item; incrementing "item uses" by 1 until it equals "maximum uses", then deleting the consumable item. Additionally, also to add the "(Uses left: X)" at the end of the "Used TYPE: NAME" notification, and show the "remaining uses" correctly in the item icon on the center-left HUD inventory (maximum - current = remaining). The HUD icon solution will only work for the "Large Tub Of Ice Cream" for now.

 

The following list show all the places affected by the bug, and the quick fixes I applied (all code line numbers based on original v0.2.1 unaltered files):

[code]

  File "game/init.rpy", lines 1649-1655, inside use_item:

        if item_type in ["potion", "food"]:

            max_uses = item_data.get("uses", 1)

            current_uses = item_dict.get("uses", 0) # Before used

            if max_uses > 1:

                item_dict["uses"] = current_uses +1

            if current_uses +1 >= max_uses:

                try:

                    persistent.player_inventory.pop(index_in_list)

                    renpy.restart_interaction()

                except IndexError:

                    notify(f"Error removing {item_name}!")

                    return

[/code]

 

[code]

  File "game/init.rpy", lines 1742-1744, inside use_item:

            if item_type in ["potion", "food"]:

                max_uses = item_data.get("uses", 1)

                current_uses = item_dict.get("uses", 1) # After used

                remaining_uses = max_uses - current_uses

                _message = f"Used {item_type.title()}: {item_name}"

                _message += f"{' (Uses left: ' + str(remaining_uses) + ')!' if current_uses < max_uses else '!'}"

                notify(_message)

            

            if item_type in ["unique", "map"]:

                notify(f"Used {item_type.title()}: {item_name}!")

                if item_type == "unique": renpy.restart_interaction()

[/code]

 

[code]

  File "game/screens.rpy", lines 3606-3607, inside the "# --- Inventory Section ---" fixed block of the hud screen:

                                if _item_id == "food_large_tub_ice_cream":

                                    add Transform(Text(f"{_item_data.get('uses', 3) - _item_dict.get('uses', 0)}x", style="unique_stack_text_style"), alpha=_alpha_for_column)

[/code]

;)

On version 0.2.1, the tooltips of the bodypart inflation levels and the food level bars on the top-left HUD are not displayed when hovered.  I dont know why the hovered/unhovered properties on 'bar' elements are not working, or the xsize and xminimum on 'text' elements.

The quick fix I got after trying different things was to wrap the 'text' elements fro each row with 'box' elements and set in there the xsize properties (and xalign on the 'text'), and for the tooltips, to wrap the 'bar' boxes for each row with a 'box' elements and a 'button' element, where the boxes sets position properties and the button sets the hovered/unhovered Functions.

 

The following block show constains the code affected by the minor bug, and the quick fix (all code line numbers based on original v0.2.1 unaltered files, deindented 6 tabs / 24 spaces because of char limit on comments):

[code]

  File "game/screens.rpy", lines 3641-3728, at hud screen in the "# --- TOP-LEFT HUD SECTION (Inflation Stats and Status Effects) ---" section:

# --- Chest Stat and Segmented Bar ---

hbox:

    spacing 5

    hbox:

        xsize 65

        text "Chest:" style "hud_text" xalign 1.0

    hbox:

        spacing 0

        yalign 0.5

        ysize 15

        button:

            action NullAction()

            hovered [SetVariable("store.hud_elements_hovered", True), SetVariable("global_tooltip_text", get_segmented_bar_tooltip_text("Chest", store.yuki_chest_level, _min_c_hud_display, _max_c_hud_display))]

            unhovered [SetVariable("store.hud_elements_hovered", False), SetVariable("global_tooltip_text", None)]

            background None

            yalign 0.5

            ysize 15

            hbox:

                spacing 1

                yalign 0.5

                ysize 15

                $ num_base_segments_c = (_base_max_c + 9) // 10 if _base_max_c > 0 else 0

                for i in range(num_base_segments_c):

                    python:

                        segment_start_value = i * 10

                        segment_value = min(max(0, store.yuki_chest_level - segment_start_value), 10)

                        style_to_use_c = "min_inflation_bar_style" if segment_start_value < _min_c_hud_display else "inflation_bar_style"

                    bar style style_to_use_c value segment_value range 10 xsize 40

                

                $ num_bonus_10_segments_c = _bonus_max_c // 10

                for k in range(num_bonus_10_segments_c):

                    $ bonus_segment_value_10 = min(max(0, store.yuki_chest_level - _base_max_c - (k * 10)), 10)

                    bar style "inflation_bar_style" value bonus_segment_value_10 range 10 xsize 40

                

                $ num_bonus_1_segments_c = _bonus_max_c % 10

                for j in range(num_bonus_1_segments_c):

                    $ bonus_segment_value_1 = min(max(0, store.yuki_chest_level - _base_max_c - (num_bonus_10_segments_c * 10) - j), 1)

                    bar style "bonus_inflation_bar_style" value bonus_segment_value_1 range 1 xsize 3

    

    hbox:

        xminimum 60

        text "[store.yuki_chest_level]{color=#888888}[display_change(chest_change)]{/color}" style "hud_text"

 

# --- Belly Stat and Segmented Bar ---

hbox:

    spacing 5

    hbox:

        xsize 65

        text "Belly:" style "hud_text" xalign 1.0

    hbox:

        spacing 0

        yalign 0.5

        ysize 15

        button:

            action NullAction()

            hovered [SetVariable("store.hud_elements_hovered", True), SetVariable("global_tooltip_text", get_segmented_bar_tooltip_text("Belly", store.yuki_belly_level, _min_b_hud_display, _max_b_hud_display))]

            unhovered [SetVariable("store.hud_elements_hovered", False), SetVariable("global_tooltip_text", None)]

            background None

            yalign 0.5

            ysize 15

            hbox:

                spacing 1

                yalign 0.5

                ysize 15

                $ num_base_segments_b = (_base_max_b + 9) // 10 if _base_max_b > 0 else 0

                for i in range(num_base_segments_b):

                    python:

                        segment_start_value = i * 10

                        segment_value = min(max(0, store.yuki_belly_level - segment_start_value), 10)

                        style_to_use_b = "min_inflation_bar_style" if segment_start_value < _min_b_hud_display else "inflation_bar_style"

                    bar style style_to_use_b value segment_value range 10 xsize 40

                

                $ num_bonus_10_segments_b = _bonus_max_b // 10

                for k in range(num_bonus_10_segments_b):

                    $ bonus_segment_value_10 = min(max(0, store.yuki_belly_level - _base_max_b - (k * 10)), 10)

                    bar style "inflation_bar_style" value bonus_segment_value_10 range 10 xsize 40

                

                $ num_bonus_1_segments_b = _bonus_max_b % 10

                for j in range(num_bonus_1_segments_b):

                    $ bonus_segment_value_1 = min(max(0, store.yuki_belly_level - _base_max_b - (num_bonus_10_segments_b * 10) - j), 1)

                    bar style "bonus_inflation_bar_style" value bonus_segment_value_1 range 1 xsize 3

    

    hbox:

        xminimum 60

        text "[store.yuki_belly_level]{color=#888888}[display_change(belly_change)]{/color}" style "hud_text"

 

# --- Butt Stat and Segmented Bar ---

hbox:

    spacing 5

    hbox:

        xsize 65

        text "Butt:" style "hud_text" xalign 1.0

    hbox:

        spacing 0

        yalign 0.5

        ysize 15

        button:

            action NullAction()

            hovered [SetVariable("store.hud_elements_hovered", True), SetVariable("global_tooltip_text", get_segmented_bar_tooltip_text("Butt", store.yuki_butt_level, _min_bu_hud_display, _max_bu_hud_display))]

            unhovered [SetVariable("store.hud_elements_hovered", False), SetVariable("global_tooltip_text", None)]

            background None

            yalign 0.5

            ysize 15

            hbox:

                spacing 1

                yalign 0.5

                ysize 15

                $ num_base_segments_bu = (_base_max_bu + 9) // 10 if _base_max_bu > 0 else 0

                for i in range(num_base_segments_bu):

                    python:

                        segment_start_value = i * 10

                        segment_value = min(max(0, store.yuki_butt_level - segment_start_value), 10)

                        style_to_use_bu = "min_inflation_bar_style" if segment_start_value < _min_bu_hud_display else "inflation_bar_style"

                    bar style style_to_use_bu value segment_value range 10 xsize 40

                

                $ num_bonus_10_segments_bu = _bonus_max_bu // 10

                for k in range(num_bonus_10_segments_bu):

                    $ bonus_segment_value_10 = min(max(0, store.yuki_butt_level - _base_max_bu - (k * 10)), 10)

                    bar style "inflation_bar_style" value bonus_segment_value_10 range 10 xsize 40

                

                $ num_bonus_1_segments_bu = _bonus_max_bu % 10

                for j in range(num_bonus_1_segments_bu):

                    $ bonus_segment_value_1 = min(max(0, store.yuki_butt_level - _base_max_bu - (num_bonus_10_segments_bu * 10) - j), 1)

                    bar style "bonus_inflation_bar_style" value bonus_segment_value_1 range 1 xsize 3

    

    hbox:

        xminimum 60

        text "[store.yuki_butt_level]{color=#888888}[display_change(butt_change)]{/color}" style "hud_text"

 

# --- Food Stat and Bar ---

hbox:

    spacing 5

    hbox:

        xsize 65

        text "Food:" style "hud_text" xalign 1.0

    hbox:

        spacing 0

        yalign 0.5

        ysize 15

        button:

            action NullAction()

            hovered [SetVariable("store.hud_elements_hovered", True), SetVariable("global_tooltip_text", get_simple_bar_tooltip_text("Food", store.yuki_food_level, _current_max_food))]

            unhovered [SetVariable("store.hud_elements_hovered", False), SetVariable("global_tooltip_text", None)]

            background None

            yalign 0.5

            ysize 15

            hbox:

                spacing 1

                yalign 0.5

                ysize 15

                bar style "food_bar_style" value store.yuki_food_level range _current_max_food

    hbox:

        xminimum 60

        text "[store.yuki_food_level] / [_current_max_food]" style "hud_text"

[/code]

;)

(1 edit)

    def _gain_unique_item(item_id):

        # --- NEW: Handle Unique Items (check for duplicates) ---

        if item_id in persistent.unique_items_acquired:

            # Player already has this unique item. Give a consolation prize instead.

            renpy.notify(f"Duplicate Unique Item! Converted to 100 Gold Coins.")

            gain_item("gold_coins", 100)

            return False # Dont add the duplicate item.

        else:

            # This is a new unique item. Add it to the master list.

            persistent.unique_items_acquired.add(item_id)

            return True # Add the first unique item.

 

    def _gain_artifact_item(item_id):

        # --- NEW: Handle Artifacts Separately ---

        if not hasattr(persistent, 'artifacts_collected'):

            persistent.artifacts_collected = set()

        persistent.artifacts_collected.add(item_id)

        # Artifacts are not added to persistent.player_inventory, so we notify and return.

        renpy.notify(f"Artifact Acquired: {item_definitions[item_id].get('name', item_id)}!")

 

    def _gain_stackable_item(item_id, amount=1):

        # --- Handle Stackable Items (like Gold Coins) ---

        current_max_capacity = inventory_capacity + getattr(persistent, 'inventory_capacity_bonus', 0) + getattr(store, 'temporary_inventory_bonus', 0) # Include temporary bonus

        item_data = item_definitions[item_id]

        item_name = item_data.get("name", item_id)

        stack_limit = item_data.get("stack_limit") # Stack limit for the item

        

        amount_to_add = max(amount, 1) # Start with the full amount requested (min 1)

        

        # 1. Try to add to existing, non-full stacks

        for item_dict in persistent.player_inventory:

            if not item_dict.get("id") == item_id:

                continue # Skip other items

            current_quantity = item_dict.get("quantity", 0)

            can_add_to_stack = stack_limit - current_quantity

            if not can_add_to_stack > 0:

                continue # Item is already max quantity

            add_now = min(amount_to_add, can_add_to_stack)

            item_dict["quantity"] = current_quantity + add_now

            amount_to_add -= add_now

            message_text = f"Added {add_now} {item_name} (Total: {item_dict['quantity']})."

            store._turn_notification_counter += 1 # Increment counter regardless of context

            add_chat_notification(message_text, store._turn_notification_counter)

            if not renpy.get_screen("chat_notify_display"): # Ensure chat display is shown

                renpy.show_screen("chat_notify_display")

            if amount_to_add <= 0: break # Added everything

        

        # 2. If amount still remains, try adding new stacks

        while amount_to_add > 0:

            if len(persistent.player_inventory) < current_max_capacity:

                add_this_stack = min(amount_to_add, stack_limit)

                persistent.player_inventory.append({"id": item_id, "quantity": add_this_stack})

                amount_to_add -= add_this_stack

                message_text = f"Added new stack of {add_this_stack} {item_name}."

                store._turn_notification_counter += 1

                add_chat_notification(message_text, store._turn_notification_counter)

                if not renpy.get_screen("chat_notify_display"):

                    renpy.show_screen("chat_notify_display")

            else:

                message_text = "Inventory Full!"

                store._turn_notification_counter += 1

                add_chat_notification(message_text, store._turn_notification_counter)

                if not renpy.get_screen("chat_notify_display"):

                    renpy.show_screen("chat_notify_display")

                break # Stop trying to add new stacks

 

    def _gain_upgradeable_item(item_id, level=1, _reset_cooldown=False, _allow_duplicates=False):

        # --- Handle Upgradeable Items (Equipment/Skill logic: upgrade if exists, else add new if space) ---

        global current_skill_cooldowns

        current_max_capacity = inventory_capacity + getattr(persistent, 'inventory_capacity_bonus', 0) + getattr(store, 'temporary_inventory_bonus', 0) # Include temporary bonus

        item_data = item_definitions[item_id]

        item_name = item_data.get("name", item_id)

        max_level = item_data.get("max_level", 1) # Max level for the item

        #is_upgradeable = item_data.get("upgradeable", False) # LEGACY

        is_upgradeable = max_level > 1

        

        if is_upgradeable or not _allow_duplicates: # Only skip non-upgradeable duplicable items (Equipment)

            found_existing = False # Found existing item

            for found_existing_index, existing_item_dict in enumerate(persistent.player_inventory):

                if not existing_item_dict.get("id") == item_id:

                    continue # Skip other items

                # Item already exists

                found_existing = True

                current_level = existing_item_dict.get("level", 1)

                # max_level is already defined from item_data

                if not current_level < max_level:

                    continue # Item is already max level

                new_level = max(level, 1)

                new_level = min(current_level + new_level, max_level)

                existing_item_dict["level"] = new_level

                message_text = f"Upgraded {item_name} to Level {new_level}!"

                store._turn_notification_counter += 1

                add_chat_notification(message_text, store._turn_notification_counter)

                if not renpy.get_screen("chat_notify_display"): # Ensure chat display is shown

                    renpy.show_screen("chat_notify_display")

                if not _reset_cooldown:

                    continue # No cooldown reset

                if item_id in current_skill_cooldowns: # Reset cooldown on upgrade

                    del current_skill_cooldowns[item_id]

                    if store.in_dungeon: renpy.restart_interaction()

                return # Stop the function here to prevent the "already Max Level!" notification.

            

            if found_existing: # All items are already max level

                message_text = f"{item_name} is already Max Level!"

                store._turn_notification_counter += 1

                add_chat_notification(message_text, store._turn_notification_counter)

                if not renpy.get_screen("chat_notify_display"): renpy.show_screen("chat_notify_display")

                return # Stop the function here to prevent adding the item.

        

        if len(persistent.player_inventory) < current_max_capacity: # Item doesn't exist or its non-upgradeable and duplicable, add new if space

            new_level = max(level, 1)

            new_level = min(new_level, max_level)

            persistent.player_inventory.append({"id": item_id, "level": new_level}) # Always add new items at level 1 (default if not given)

            if is_upgradeable: # Only show starting level for upgradeable items

                message_text = f"Obtained: {item_name} (Level {new_level})!"

            else:

                message_text = f"Obtained: {item_name}!"

            store._turn_notification_counter += 1

            add_chat_notification(message_text, store._turn_notification_counter)

            if not renpy.get_screen("chat_notify_display"):

                renpy.show_screen("chat_notify_display")

        

        else: # No space for new item

            message_text = "Inventory Full!"

            store._turn_notification_counter += 1

            add_chat_notification(message_text, store._turn_notification_counter)

            if not renpy.get_screen("chat_notify_display"):

                renpy.show_screen("chat_notify_display")

 

    def _gain_consumable_item(item_id, uses=0):

        # --- NEW: Handle Consumable Items (Potions/Food logic: limited uses) ---

        current_max_capacity = inventory_capacity + getattr(persistent, 'inventory_capacity_bonus', 0) + getattr(store, 'temporary_inventory_bonus', 0) # Include temporary bonus

        item_data = item_definitions[item_id]

        item_name = item_data.get("name", item_id)

        max_uses = item_data.get("uses", 1) # Max amount of uses for the item

        

        if len(persistent.player_inventory) < current_max_capacity: # Add new if space

            new_item_dict = {"id": item_id, "level": 1} # Base item dict

            remaining_uses = max_uses

            if not uses == 0:

                starting_uses = min(uses, max_uses - 1) if uses > 0 else max(max_uses + uses, 0) # Uses done from 0 to max-1, or, Uses remaining from max-abs(uses) to 0

                if not starting_uses == 0: # Only set if not default 0

                    new_item_dict["uses"] = starting_uses # Initialize with an amount of uses done

                remaining_uses -= starting_uses

            persistent.player_inventory.append(new_item_dict)

            

            if not max_uses == 1:

                message_text = f"Obtained: {item_name} ({remaining_uses} Uses)!" if remaining_uses > 1 else f"Obtained: {item_name} ({remaining_uses} Use)!"

            else:

                message_text = f"Obtained: {item_name}!"

            store._turn_notification_counter += 1

            add_chat_notification(message_text, store._turn_notification_counter)

            if not renpy.get_screen("chat_notify_display"):

                renpy.show_screen("chat_notify_display")

        

        else: # No space for new item

            message_text = "Inventory Full!"

            store._turn_notification_counter += 1

            add_chat_notification(message_text, store._turn_notification_counter)

            if not renpy.get_screen("chat_notify_display"):

                renpy.show_screen("chat_notify_display")

 

    def _gain_other_item(item_id):

        # --- Handle Non-Stackable, Non-Upgradeable Items ---

        current_max_capacity = inventory_capacity + getattr(persistent, 'inventory_capacity_bonus', 0) + getattr(store, 'temporary_inventory_bonus', 0) # Include temporary bonus

        item_data = item_definitions[item_id]

        item_name = item_data.get("name", item_id)

        

        if len(persistent.player_inventory) < current_max_capacity: # Add new if space

            # MODIFICATION START: Data-driven stack initialization

            new_item_dict = {"id": item_id, "level": 1} # Base item dict

            if item_data.get("has_stacks", False):

                new_item_dict["stacks"] = 0 # Initialize with 0 stacks if defined

            persistent.player_inventory.append(new_item_dict)

            # MODIFICATION END

            

            message_text = f"Obtained: {item_name}!"

            store._turn_notification_counter += 1

            add_chat_notification(message_text, store._turn_notification_counter)

            if not renpy.get_screen("chat_notify_display"):

                renpy.show_screen("chat_notify_display")

        

        else: # No space for new item

            message_text = "Inventory Full!"

            store._turn_notification_counter += 1

            add_chat_notification(message_text, store._turn_notification_counter)

            if not renpy.get_screen("chat_notify_display"):

                renpy.show_screen("chat_notify_display")

 

    def use_item(item_id, index_in_list):

[/code]

;)

On version 0.2.1, if the first upgradable SKill or Equipment of a specific kind (id) is at Max level, and a second one is not at Max level, when the player gains an item of that specific kind, the second one is not upgraded and the notification section show a "[item_name] is already Max Level!" message.

The main cause of this behavior is some outdated code in the gain_item Function.

The legacy code from version 0.1.X only takes into account the first upgradable item of a specific kind in the inventory, since the dungeon was the only way for the player to obtain an instance of a specific item.

But since version 0.2.X, with the introduction of a "Home Inventory", the player can now move to/from the "Player Inventry" any item from previous explorations or purchases. Because of this, its possible for the player to have multiple instances of the same kind of upgradable item in the player's inventory before exploring the Dungeon, what needs to be taken into account for the "gain_item" Function in version 0.2.X.

 

A quick fix to allow upgrading any instance of the gained item in inventory is to reuse part of the logic for stackable items, upgrading a matching item on inventory instead of stacking it (if not Max level), and preventing the addition of a new item if there was at least one on the inventory (meaning that all instances of that item are already Max Level).

Additionally, to reuse the "upgradable_item" logic for Skills and Equipment, the different sections of the gain_item Function can be rearanged to new "private" Functions to do each separate logic to handle different item types. This makes easy to implement a way of setting a custom "starting level" for Skills and Equipment, and "starting uses" for Potions and Food.

[code]

  File "game/init.rpy", lines 1448-1618, at gain_item in the "# INVENTORY MANAGEMENT #" python block:

init -1 python: # INVENTORY MANAGEMENT #

    def gain_item(item_id, amount=None): # Added amount parameter, defaults defined by handlers

        if item_id not in item_definitions:

            renpy.log(f"Error: Tried to gain unknown item '{item_id}'")

            return

        

        item_data = item_definitions[item_id]

        item_type = item_data.get("type", "unknown")

        stack_limit = item_data.get("stack_limit") # Get stack limit if defined

        

        # --- NEW: Handle Unique Items (check for duplicates) ---

        if item_type == "unique":

            if not _gain_unique_item(item_id):

                return # Stop the function here to prevent adding the duplicate.

        

        # --- NEW: Handle Artifacts ---

        if item_type == "artifact":

            _gain_artifact_item(item_id)

            return # Artifacts are not added to persistent.player_inventory, so we notify and return.

        

        # --- Handle Stackable Items (like Gold Coins) ---

        if stack_limit is not None:

            if amount is None: _gain_stackable_item(item_id)

            else: _gain_stackable_item(item_id, amount)

        

        # --- Handle Skills (logic: upgrade and reset cooldown if exists, else add new if space) ---

        elif item_type == "skill":

            if amount is None: _gain_upgradeable_item(item_id, _reset_cooldown=True)

            else: _gain_upgradeable_item(item_id, amount, _reset_cooldown=True)

        

        # --- Handle Equipment (logic: upgrade if exists and is upgradeable, else add new if space) ---

        elif item_type == "equipment":

            if amount is None: _gain_upgradeable_item(item_id, _allow_duplicates=True)

            else: _gain_upgradeable_item(item_id, amount, _allow_duplicates=True)

        

        # --- NEW: Handle Potions and Food Items (logic: limited uses) ---

        elif item_type in ["potion", "food"]:

            if amount is None: _gain_consumable_item(item_id)

            else: _gain_consumable_item(item_id, amount)

        

        # --- Handle Non-Stackable, Non-Upgradeable Items (Uniques) ---

        # --- Handle Non-Stackable, Non-Upgradeable Items (Potions, Food, Uniques) ---

        else:

            _gain_other_item(item_id)

...

3.1) Implement the new functions in the Cubi room:

# pay out the debt to Cubi; pay out the current services to Cubi.

# 10 gold per requested service after used.

[code]

  File "game/dungeon_room.rpy", lines 7592-7610, inside room_cubi label:

        python:

            _can_afford_debt = get_current_gold_amount() >= persistent.cubi_debt_owed

        

        menu:

            "Pay the [persistent.cubi_debt_owed] Gold debt." if _can_afford_debt:

                y "Grrr... fine. Here's your blood money, you little toaster."

                python:

                    lose_item("gold_coins", persistent.cubi_debt_owed) # Should return persistent.cubi_debt_owed

                    persistent.cubi_debt_owed = 0

                    renpy.restart_interaction()

                cubi "\"Payment accepted. Account balance is now zero. Standard services are available.\""

[/code]

[/code]

  File "game/dungeon_room.rpy", lines 7692-7711, inside cubi_checkout label:

    python:

        _can_afford_checkout = get_current_gold_amount() >= _total_cost

    

    menu:

        "Pay the [_total_cost] Gold." if _can_afford_checkout:

            y "Grrr... fine! Here's your payment, you little extortionist!"

            python:

                lose_item("gold_coins", _total_cost) # Should return not _total_cost

                persistent.knows_about_cubi_fee = True

                renpy.restart_interaction()

            cubi "\"Payment accepted. Your compliance is... optimal. We value your patronage.\""

            jump cubi_exit_room

[/code]

 

3.2) Implement the new functions in the Vending Machine room:

# pay for buyable items in the Vending Machine.

# 5, 10 or 20 gold for "Fizzy Soda", "Random Potion" or "Potion of Relief" respectively.

[code]

  File "game/dungeon_room.rpy", lines 7342-7436, inside room_vending_machine label:

        python:

            # This block is now ONLY for displaying the gold and setting button sensitivity.

            store.vending_machine_current_gold = get_current_gold_amount()

        

        menu:

            "Buy Fizzy Soda (Cost: 5 Gold)":

                if store.vending_machine_current_gold >= 5:

                    y "A fizzy soda sounds... bubbly right now. Yuki will try it."

                    python:

                        # --- THIS IS THE FIX ---

                        lose_item("gold_coins", 5) # Should return 5

                        renpy.restart_interaction()

                        store._turn_notification_counter += 1

                        add_chat_notification("Spent 5 Gold on a Fizzy Soda.", store._turn_notification_counter)

                        gain_item("food_fizzy_soda")

                        # --- END OF FIX ---

                    y "Clunk! The machine dispenses a can of Fizzy Soda."

                else:

                    y "Yuki doesn't have enough gold for that. This machine is clearly not recognizing her status. (Need 5 Gold)"

                pause 0.5

                jump room_vending_machine_menu

            

            "Buy Random Potion (Cost: 10 Gold)":

                if store.vending_machine_current_gold >= 10:

                    y "A random potion? Sounds like a gamble, but Yuki is totally lucky!"

                    python:

                        # --- THIS IS THE FIX ---

                        lose_item("gold_coins", 10) # Should return 10

                        renpy.restart_interaction()

                        store._turn_notification_counter += 1

                        add_chat_notification("Spent 10 Gold on a Random Potion.", store._turn_notification_counter)

                        potion_pool_vm = [item_id for item_id, data in store.item_definitions.items() if data.get("type") == "potion"]

                        if potion_pool_vm:

                            chosen_potion_id_vm = renpy.random.choice(potion_pool_vm)

                            gain_item(chosen_potion_id_vm)

                            store.vending_machine_potion_name = store.item_definitions.get(chosen_potion_id_vm, {}).get("name", "a mysterious potion")

                        else:

                            store.vending_machine_potion_name = "an empty slot... how unlucky"

                        # --- END OF FIX ---

                    y "The machine whirs and drops... a [store.vending_machine_potion_name]! Yuki hopes it's not too disappointing."

                else:

                    y "Not enough gold for a mystery potion. (Need 10 Gold)"

                pause 0.5

                jump room_vending_machine_menu

            

            "Buy Potion of Relief (Cost: 20 Gold)":

                if store.vending_machine_current_gold >= 20:

                    y "A Potion of Relief sounds very useful for Yuki right about now. She might consider it."

                    python:

                        # --- THIS IS THE FIX ---

                        lose_item("gold_coins", 20) # Should return 20

                        renpy.restart_interaction()

                        store._turn_notification_counter += 1

                        add_chat_notification("Spent 20 Gold on a Potion of Relief.", store._turn_notification_counter)

                        gain_item("potion_relief")

                        # --- END OF FIX ---

                    y "The machine dispenses a Potion of Relief. Ah, just what Yuki might need, if she were lesser."

                else:

                    y "Yuki can't afford that right now. This machine is clearly overpriced. (Need 20 Gold)"

                pause 0.5

                jump room_vending_machine_menu

[/code]

 

3.3) Implement the new functions in the Barter Peddler room:

# pay for trades with the Barter Peddler.

# 10 gold per trade (upon first trade).

[code]

  File "game/dungeon_room.rpy", lines 6489-6556, inside room_barter_peddler label:

                if offered_item_index is not None and 0 <= offered_item_index < len(persistent.player_inventory):

                    python:

                        item_to_be_traded = persistent.player_inventory[offered_item_index]

                        

                        cost_of_this_trade = 10 if peddler_trade_count_this_encounter > 0 else 0

                        can_afford_trade = True

                        

                        if cost_of_this_trade > 0:

                            can_afford_trade = get_current_gold_amount() >= cost_of_this_trade

                    ...

                    if store.trade_outcome == "success":

                        if cost_of_this_trade > 0:

                            # FIX: Using a temp variable for the f-string

                            $ _peddler_dialogue_line = f"Very well. That'll be {cost_of_this_trade} Gold Coins."

                            p "[_peddler_dialogue_line]"

                            python:

                                lose_item("gold_coins", cost_of_this_trade) # Should return cost_of_this_trade

                            y "(I hand over [cost_of_this_trade] Gold Coins.)"

                            $ renpy.restart_interaction()

                        

                        $ persistent.player_inventory.remove(item_to_be_traded)

                        $ renpy.restart_interaction()

                        

                        $ gain_item(store.new_item_id_from_trade)

[/code]

 

3.4) Implement the new functions in the Gamble room:

# pay for the "Spin with Gold" in the Gamble room.

# 10 gold per spin.

[code]

  File "game/dungeon_room.rpy", lines 1887-1894, inside gamble_unified_actions_screen screen:

                        python:

                            _can_afford_gold_spin_screen = get_current_gold_amount() >= 10

                        textbutton "Spin with Gold":

[/code]

[code]

  File "game/dungeon_room.rpy", lines 1943-1955, inside gamble_action_spin_inflation label:

    label gamble_action_spin_gold:

        y "A little spending couldn't hurt~"

        python:

            lose_item("gold_coins", 10) # Should return 10

            renpy.restart_interaction()

[/code]

 

3.5) Implement the new functions in the Curse Cleansing Pool room:

# toss coins in the Curse Cleansing Pool room ('The Font of Fickle Fortunes').

# 10 gold for tossing action.

[code]

  File "game/dungeon_room.rpy", lines 6380-6401, inside room_curse_cleansing_pool label:

            "Toss 10 Gold Coins into the pool." if "toss_coin" not in chosen_options_this_visit:

                python:

                    # This block re-checks affordability right before action, which is good practice.

                    _can_afford_pool_toss = get_current_gold_amount() >= 10

                

                if _can_afford_pool_toss:

                    python:

                        # Deduct 10 gold coins from stacks in inventory

                        lose_item("gold_coins", 10) #  Should return 10

                        renpy.restart_interaction() # Update HUD if inventory changed

[/code]

 

3.6) Implement the new functions in the MAX MIN Lever room:

# pay for 'MIN' Lever deflation in the MAX MIN Lever room.

# gold cost based on current total inflation level.

[code]

  File "game/dungeon_room.rpy", lines 6041-6058, inside room_lever_max_min label:

        "Pull 'MIN' Lever (Cost: [_dynamic_cost_min_lever_menu] Gold).":

            y "Let's reduce the pressure."

            python:

                _can_afford_min_lever = get_current_gold_amount() >= _dynamic_cost_min_lever_menu

                

                if _can_afford_min_lever:

                    # Deduct gold

                    lose_item("gold_coins", _dynamic_cost_min_lever_menu) # Should return _dynamic_cost_min_lever_menu

                    renpy.restart_interaction()

[/code]

;)

On version 0.2.1, all actions centered on spending "Gold Coins" from player's inventory may fail to detect payability from the total gold amount and only will take into account the first "gold_coins" stack item in inventory. This is not allways a problem, if the player has a "gold_coins" stack item equal or above the required amount, he can use the inventory menu (right-click) to move the higher stack to be first one.

But in cases where the required amount is only reached using 2 gold stacks (each gold stacks been bellow the required amount), the player will not be able to perform that action. The most extreme case of this behavior if getting a debt to Cubi of 110 or more (11 services), making impossible for the player to pay the debt.

Solution to avoid these scenarios is:

  1. Use a function to calculate the total collective gold quantity from all the player's inventory,
  2. Use another function to substract a given amount from multiple "gold_coins" items (or any other item quantity), and
  3. Adapt the gold-spending-centered mechanics to use these functions.

The following list show all the places affected by the minor bug, and the quick fixes I applied (all code line numbers based on original v0.2.1 unaltered files):

 

1) Refactor the (unused) get_current_gold_amount Function to return the total quantity of gold from all "gold_coins" items in the inventory.

[code]

  File "game/init.rpy", lines 1926-1937, inside get_current_gold_amount:

    def get_current_gold_amount():

        gold_qty = 0

        # Ensure persistent.player_inventory is a list before iterating

        if not isinstance(store.persistent.player_inventory, list):

            renpy.log("Warning: persistent.player_inventory is not a list in get_current_gold_amount.")

            return 0 # Or handle as an error

        for item_dict in store.persistent.player_inventory:

            if item_dict.get("id") == "gold_coins":

                gold_qty += item_dict.get("quantity", 0) # <-- Collective gold quantity

                #break # <-- Continue with all the items

        return gold_qty

[/code]

 

2) Add a "lose_item" Function to remove the stackable (and maybe non-stackable) items, like "gold_coins". This function does the opposite of the gain_item Function (File "game/init.rpy", lines 1449-1616) for stackable items on the "# INVENTORY MANAGEMENT #" python block (lines 1448-1945).

# (!) This fix doesnt handle "cursed" equipment that cannot be discarted, or any other special item (that is handled by the discard_item Function, invoked by player).

[code]

  File "game/init.rpy", lines 1939-1948, appended after yuki_has_item:

    def yuki_has_item(item_id_to_check):

        ...

        return False

 

    def lose_item(item_id, amount=1): # Removed amount parameter, defaults to 1

        if item_id not in item_definitions:

            renpy.log(f"Error: Tried to lose unknown item '{item_id}'")

            return 0

        if amount <= 0:

            return 0

        

        item_data = item_definitions[item_id]

        item_name = item_data.get("name", item_id)

        

        amount_to_remove = amount # Start with the full amount requested

        # --- Handle Stackable and Non-Stackable Items (like Gold Coins) ---

        i = 0

        while i <= len(persistent.player_inventory) and amount_to_remove > 0:

            index_in_list = i

            item_dict = persistent.player_inventory[index_in_list]

            if item_dict.get("id") == item_id:

                current_quantity = item_dict.get("quantity", 0)

                can_remove_from_stack = current_quantity

                if can_remove_from_stack > 0:

                    remove_now = min(amount_to_remove, can_remove_from_stack)

                    item_dict["quantity"] = current_quantity - remove_now

                    amount_to_remove -= remove_now

                

                if item_dict["quantity"] <= 0:

                    # Continue next iteration using the same index after removing current item

                    store.persistent.player_inventory.pop(index_in_list)

                    message_text = f"Subtracted {remove_now} {item_name}."

                else:

                    # Continue next iteration using the next index

                    i += 1

                    message_text = f"Subtracted {remove_now} {item_name} (Total: {item_dict['quantity']})."

                store._turn_notification_counter += 1 # Increment counter regardless of context

                add_chat_notification(message_text, store._turn_notification_counter)

                if not renpy.get_screen("chat_notify_display"): # Ensure chat display is shown

                    renpy.show_screen("chat_notify_display")

                continue

            

            i += 1 # Continue next iteration using the next index

        

        return amount - amount_to_remove # The effectively removed item quantity

 

init -1 python: # QTE FUNCTIONS #

[/code]

;)

(4 edits)

On version 0.2.1, there is an unintuitive and rather unexpected behavior on all the skills that take some inflation from one bodypart and then transfers it to other bodyparts or transforms it into other things. These skills that deflates from a part, and then inflates to another part or convert it into other things, doesnt take into account how much change is actually applied after clamping (mainly due to Rank base minimum, spirit-induced minimum or "Lactation Lock" status).

For example, the "Redirect Presure" Skill says that it "Takes half of the most inflated part and redirects it to the least inflated part".

  • Expected behavior 1: takes half of the current inflation level from the most inflated part, from the minimum level to the current level, and redirects it to the least inflated part.
  • Expected behavior 2: takes half of the current inflation level from the most inflated part, from 0 to the current level, takes that amount from the inflation level between minimum level and current level, and redirects it to the least inflated part.
  • Actual behavior: tries to take half of the current inflation level from the most inflated part, from 0 to the current level, clamps the most inflated part to the minimum level, and adds the mistaken amount to the least inflated part, creating more unbalanced inflation than before.

This makes the skill unviable at any other Rank than "B Rank", while inhabited by Helium Spirits or under the "Lactation Lock" status (that is, with all minimums at 0 and no deflation-locking status). Other skills that deflate and inflate specific amounts of inflation also could lead to creating more inflation than before.

 

The Solution I came out with was to add a returning feature to the modify_inflation funtion, to make it return the total effective amount of inflation/deflation dealt (absolute value). And then, update the code in all these inflation-transfer/transform skills to take into account the effective inflated/deflated amount.

The following list show all the places affected by the minor bug, and the quick fixes I applied (all code line numbers based on original v0.2.1 unaltered files):

 

1.1) Take into account the modified amount in the "Air Control" Skill.

# 5 inflation from most-part to least-part.

[code]

  File "game/init.rpy", lines 3537-3551, inside skill_use_air_control:

    def skill_use_air_control():

        amount_to_shift = 5

        part_from = get_most_likely_to_pop_part()

        if not part_from:

            notify("Air Control: No parts have inflation to shift from.")

            return False # <--

        part_to = get_least_likely_to_pop_part(exclude_part=part_from)  # <-- Get the part to inflate excluding the part to deflate.

        if not part_to:

            notify("Air Control: No parts have room to shift inflation to.")

            return False # <--

        initial_context = renpy.game.context().current

        modified_amount = modify_inflation(part_from, -amount_to_shift) # <-- Get the effectively deflated amount.

        if modified_amount == 0: # <-- When the most likely to pop cannot be deflated.

            notify("Air Control: Cannot deflate part to shift from.") # <--

            return False # <-- 

        if renpy.game.context().current is initial_context:

            modify_inflation(part_to, modified_amount) # <-- Inflates the effectively deflated amount.

            notify(f"Air Control: Shifted {modified_amount} inflation from {part_from} to {part_to}.")# <--

        return True # <--

[/code]

 

1.2) Take into account the modified amount in the "Pressure Shift" Skill.

 # 10 inflation from most-part to least-part.

[code]

  File "game/init.rpy", lines 3602-279, inside skill_use_pressure_shift:

    def skill_use_pressure_shift():

        amount_to_shift = 10 

        part_from = get_most_likely_to_pop_part()

        part_to = get_least_likely_to_pop_part(exclude_part=part_from)  # <-- Get the part to inflate excluding the part to deflate.

        if not part_from or not part_to: # <--

            notify("Cannot effectively shift pressure.")

            return False # <--

        initial_context = renpy.game.context().current

        modified_amount = modify_inflation(part_from, -amount_to_shift) # <-- Get the effectively deflated amount.

        if modified_amount == 0: # <-- When the most likely to pop cannot be deflated.

            notify("Cannot effectively shift pressure.") # <--

            return False # <-- 

        if renpy.game.context().current is initial_context:

            modify_inflation(part_to, modified_amount) # <-- Inflates the effectively deflated amount.

            notify(f"Shifted {modified_amount} inflation from {part_from} to {part_to}.")# <--

        return True # <--

[/code]

 

1.3) Update legacy code for most/least parts and take into account the modified amount in the "Redirect Pressure" Skill.

# halft inflation from most-part to least-part.

#For the Expected behavior 1, use "net_level" to calc the dynamic half (minimum to current level).

#For the Expected behavior 2, use "gross_level" to calc the total half (zero to current level).

[code]

  File "game/init.rpy", lines 3511-3535, inside skill_use_redirect_pressure:

    def skill_use_redirect_pressure():

        part_from = get_most_likely_to_pop_part() # <--

        if not part_from: # <--

            notify("Cannot redirect pressure: No valid parts to shift from.") # <--

            return False # <--

        part_to = get_least_likely_to_pop_part(exclude_part=part_from)  # <-- Get the part to inflate excluding the part to deflate.

        if not part_to: # <--

            no_part_to_message = "Cannot redirect pressure effectively: Target and source are the same." if get_least_likely_to_pop_part() is not None else "Cannot redirect pressure: No valid parts to shift to." # <-- If there is a part_to target when not using exclusion, then it is the excluded one

            notify(no_part_to_message) # <--

            return False # <--

        gross_level = getattr(store, f"yuki_{part_from}_level") # <-- Total current level (including the Rank base minimum and/or the spirit-induced minimum)

        net_level = gross_level - _clamp_level_minimum(0, part_from) # <-- Dynamic current level (excluding the Rank base minimum and/or the spirit-induced minimum)

        amount_to_redirect = int(round(net_level / 2.0)) # <-- Choose espected behavior (total level or dynamic level)

        if amount_to_redirect <= 0:

            notify("Not enough inflation in the largest part to redirect.")

            return False

        initial_context = renpy.game.context().current

        modified_amount = modify_inflation(part_from, -amount_to_redirect) # <-- Get the effectively deflated amount.

        if modified_amount == 0: # <-- When the most likely to pop cannot be deflated.

            notify("Cannot deflate the largest part to redirect.") # <--

            return False # <-- 

        if renpy.game.context().current is initial_context:

            modify_inflation(part_to, modified_amount) # <-- Inflates the effectively deflated amount.

            notify(f"Shifted {modified_amount} inflation from {part_from} to {part_to}.")# <--

        return True

[/code]

 

1.4) Take into account the modified amount in the "Udderly Balanced" Skill.

# 15 deflation from chest, 7 inflation to belly and 8 inflation to butt.

[code]

  File "game/init.rpy", lines 3472-3478, inside skill_use_udderly_balanced:

    def skill_use_udderly_balanced():

        initial_context = renpy.game.context().current

        modified_amount = modify_inflation("chest", -15) # <-- Effective amount deflated from "chest"

        if modified_amount == 0: return False # <-- Cannot deflate chest (maybe lactation lock is active)

        if renpy.game.context().current is initial_context: # Check if pop occurred

            modify_inflation("belly", (modified_amount // 2)) # <-- The floor half

            if renpy.game.context().current is initial_context: # Check if pop occurred again

                modify_inflation("butt", (modified_amount // 2) + (modified_amount % 2)) # <-- The tie breaking half (odd case)

        return True # <--

[/code]

 

1.5) Take into account the modified amount in "Milk Conversion" Skill.

# 10 deflation from chest and +2 lactation stacks.

[code]

  File "game/init.rpy", lines 3499-3509, inside skill_use_milk_conversion:

    def skill_use_milk_conversion():

        if store.yuki_chest_level >= 10:

            initial_context = renpy.game.context().current

            modified_amount = modify_inflation("chest", -10) # <-- Effective amount deflated from "chest"

            if modified_amount != 10: # <-- Cannot deflate enough of chest

                notify("Cannot convert 10 chest inflation to milk!") # <-- (maybe lactation lock is active)

                return False # <--

            if renpy.game.context().current is initial_context:

                store.lactation_stacks += 2

                notify(f"Converted 10 chest inflation into 2 Lactation Stacks! (Total: {store.lactation_stacks})")

                return True

        else:

            notify("Not enough chest inflation to convert to milk!")

        return False

[/code]

 

2) Refactor the return behavior of the modify_inflation Funtion, to return the absolute value of the total inflation levels modified.

[code]

  File "game/init.rpy", lines 306-380, inside modify_inflation:

    def modify_inflation(part, amount, _is_recursive_call=False):

        if part not in ["chest", "belly", "butt"]:

            renpy.log(f"Error: Invalid part '{part}' passed to modify_inflation.")

            return 0 # <-- No inflation level was modified.

        ...

        current_level = getattr(store, current_level_attr, 0)

        new_level_unclamped = current_level + amount

        

        current_total_modified_amount = 0 # <-- Modified amount on this call.

        ...

        if part == "belly" and amount > 0 and yuki_has_item("equip_tight_rubber_belt"):

            ...

            if new_level_unclamped > effective_belly_cap_with_belt:

                excess_belly_inflation_to_redirect = new_level_unclamped - effective_belly_cap_with_belt

                new_level_unclamped = effective_belly_cap_with_belt

                current_total_modified_amount += abs(effective_belly_cap_with_belt - current_level) # <-- Modified on belly from current_level to effective_belly_cap_with_belt (clamped).

                ...

        if amount > 0: # Inflation

            shake_duration_scale = max(min_shake_scale, min(float(abs(amount)) / base_inflation_for_shake, max_shake_scale))

            actual_change_this_part = new_level_unclamped - current_level

            current_total_modified_amount += abs(actual_change_this_part) # <-- Modified on part from current_level to new_level_unclamped (unclamped).

            ...

        elif amount < 0: # Deflation

            if part == "chest" and has_status_effect("status_lactation_lock"):

                if store.in_dungeon: notify("Lactation Lock prevents chest deflation!")

            else:

                new_level_clamped_deflate = _clamp_level_minimum(new_level_unclamped, part)

                actual_decrease_this_part = current_level - new_level_clamped_deflate

                current_total_modified_amount += abs(actual_decrease_this_part) # <-- Modified on part from current_level to new_level_clamped_deflate (clamped)

                ...

        if excess_belly_inflation_to_redirect > 0 and redirection_target_part:

            current_total_modified_amount += modify_inflation(redirection_target_part, excess_belly_inflation_to_redirect, _is_recursive_call=True) # <-- Modified on redirection_target_part recursively.

    

        if not _is_recursive_call:

            check_game_state_after_inflation()

        return current_total_modified_amount # <-- Total absolute-value modified amount (including recursion)

[/code]

;)

After boughting a skill, you have to "enable" it with the green button next to the skill name on the Aether Upgrades > Starting Upgrades menu. The skills with the "disable" red button are the active ones.

(2 edits)

On version 0.2.1, I got an error when using the Air Control Skill:

[code]

I'm sorry, but an uncaught exception occurred.

While running game code:

  File "game/dungeon_rooms.rpy", line 337, in script call

    call expression _current_room_label from _call_expression_1

  File "game/dungeon_rooms.rpy", line 5957, in script

    menu:

  File "renpy/common/00action_other.rpy", line 619, in __call__

    rv = self.callable(*self.args, **self.kwargs)

  File "game/init.rpy", line 1667, in use_item

    effect_result = effect_func(*effect_args)

  File "game/init.rpy", line 3543, in skill_use_air_control

    part_to = get_least_likely_to_pop_part(exclude_part=part_from)

TypeError: get_least_likely_to_pop_part() got an unexpected keyword argument 'exclude_part'

-- Full Traceback ------------------------------------------------------------

Full traceback:

  File "game/dungeon_rooms.rpy", line 337, in script call

    call expression _current_room_label from _call_expression_1

  File "game/dungeon_rooms.rpy", line 5957, in script

    menu:

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\ast.py", line 1632, in execute

    choice = renpy.exports.menu(choices, self.set, args, kwargs, item_arguments)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\exports\menuexports.py", line 134, in menu

    rv = renpy.store.menu(new_items)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\exports\menuexports.py", line 424, in display_menu

    rv = renpy.ui.interact(mouse='menu', type=type, roll_forward=roll_forward)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\ui.py", line 301, in interact

    rv = renpy.game.interface.interact(roll_forward=roll_forward, **kwargs)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\core.py", line 2218, in interact

    repeat, rv = self.interact_core(preloads=preloads, trans_pause=trans_pause, pause=pause, pause_start=pause_start, pause_modal=pause_modal, **kwargs) # type: ignore

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\core.py", line 3289, in interact_core

    rv = root_widget.event(ev, x, y, 0)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 1297, in event

    rv = i.event(ev, x - xo, y - yo, cst)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 1297, in event

    rv = i.event(ev, x - xo, y - yo, cst)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 1297, in event

    rv = i.event(ev, x - xo, y - yo, cst)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\screen.py", line 794, in event

    rv = self.child.event(ev, x, y, st)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 1297, in event

    rv = i.event(ev, x - xo, y - yo, cst)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 1297, in event

    rv = i.event(ev, x - xo, y - yo, cst)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\behavior.py", line 1119, in event

    rv = super(Button, self).event(ev, x, y, st)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 1526, in event

    rv = super(Window, self).event(ev, x, y, st)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 285, in event

    rv = d.event(ev, x - xo, y - yo, st)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 1297, in event

    rv = i.event(ev, x - xo, y - yo, cst)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 285, in event

    rv = d.event(ev, x - xo, y - yo, st)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 1526, in event

    rv = super(Window, self).event(ev, x, y, st)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\layout.py", line 285, in event

    rv = d.event(ev, x - xo, y - yo, st)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\behavior.py", line 1182, in event

    return handle_click(self.clicked)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\behavior.py", line 1103, in handle_click

    rv = run(action)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\behavior.py", line 394, in run

    new_rv = run(i, *args, **kwargs)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\display\behavior.py", line 401, in run

    return action(*args, **kwargs)

  File "renpy/common/00action_other.rpy", line 619, in __call__

    rv = self.callable(*self.args, **self.kwargs)

  File "game/init.rpy", line 1667, in use_item

    effect_result = effect_func(*effect_args)

  File "game/init.rpy", line 3543, in skill_use_air_control

    part_to = get_least_likely_to_pop_part(exclude_part=part_from)

TypeError: get_least_likely_to_pop_part() got an unexpected keyword argument 'exclude_part'

Windows-10-10.0.19041 AMD64

Ren'Py 8.3.8.25060602+nightly

Yuki, Pop, Repeat 0.2.1

Wed Nov 12 13:16:20 2025

[/code]

 

The get_least_likely_to_pop_part function doesnt take arguments (as the exclude_part). A quick fix can be to implement the behavior of exclusion on the get_least_likely_to_pop_part function, and additionally implement it on the get_most_likely_to_pop_part function for future skills. The fix is (all code line numbers based on original v0.2.1 unaltered files):

[code]

  File "game/init.rpy", lines 796-818, in get_least_likely_to_pop_part:

    def get_least_likely_to_pop_part(exclude_part=""): # Consider excluded part

        ...

        parts_with_remaining_capacity.pop(exclude_part, None) # Remove the excluded part from options

        if not parts_with_remaining_capacity: # All parts are at/over cap or no valid parts

            default_parts = ["chest", "belly", "butt"] # Default options for Fallback

            default_parts.remove(exclude_part) # Remove the excluded part from fallback options

            return renpy.random.choice(default_parts) # Fallback: pick a random part

        return max(parts_with_remaining_capacity, key=parts_with_remaining_capacity.get)

[/code]

[code]

  File "game/init.rpy", lines 820-849, in get_most_likely_to_pop_part:

    def get_most_likely_to_pop_part(exclude_part=""): Consider excluded part

        ...

        parts_with_remaining_capacity.pop(exclude_part, None) # Remove the excluded part from options

        if not parts_with_remaining_capacity: # All parts are at/below minimum or no valid parts

            return None # No part can be siphoned

        # Return the part with the smallest remaining capacity (closest to pop)

        # This includes negative remaining capacity (overinflated parts are most likely to pop)

        return min(parts_with_remaining_capacity, key=parts_with_remaining_capacity.get)

[/code]

;)

On version 0.2.1,  there is a minor bug when displaying the Max Inflation Capacity and Base Inflation Capacity on the Upgrade screen. No matter what, the Max Inflation Capacity (effective) is lock at 30, and the Base Inflation Capacity is decreased by each max inflation upgrade (been allways Base Bonus = 30).

quick fix that solved the problem for me was to add the "general_base_max" inflation capacity value for each Tier/Rank on the rank_definitions, then get that data as the Base for the current Tier/Rank and finally calc the effective Max Inflation Capacity.

[code]

  File "game/item_definitions.rpy", assignation at lines 1010-1211 (rank_definitions):

    rank_definitions = {

        "B": {

            "name": "Tier B",

            "base_min": (0, 0, 0),

            "base_max": (30, 30, 30),

            "general_base_max": 30, # (from v0.1.6: 30, average v0.2.1: 30)

            "rank_down_threshold": 0, # Cannot rank down from B

            ...

        },

        "A": {

            "name": "Tier A",

            "base_min": (30, 30, 30),

            "base_max": (60, 60, 60),

            "general_base_max": 60, # (from v0.1.6: 60, average v0.2.1: 60)

            "rank_down_threshold": 90, # 30 * 3

            ...

        },

        "Hourglass": {

            "name": "Hourglass",

            "base_min": (30, 0, 30),

            "base_max": (60, 30, 60),

            "general_base_max": 50, # (from v0.1.6: 60, average v0.2.1: 50)

            "rank_down_threshold": 60, # 30 + 0 + 30

            ...

        },

        "Bloated": {

            "name": "Bloated",

            "base_min": (0, 30, 0),

            "base_max": (30, 60, 30),

            "general_base_max": 40, # (from v0.1.6: 70, average v0.2.1: 40)

            "rank_down_threshold": 30, # 0 + 30 + 0

            ...

        },

        "Top Heavy": {

            "name": "Top Heavy",

            "base_min": (30, 0, 0),

            "base_max": (60, 30, 30),

            "general_base_max": 40, # (average v0.2.1: 40)

            "rank_down_threshold": 30, # 30 + 0 + 0

            ...

        },

        "Bottom Heavy": {

            "name": "Bottom Heavy",

            "base_min": (0, 0, 30),

            "base_max": (30, 30, 60),

            "general_base_max": 40, # (average v0.2.1: 40)

            "rank_down_threshold": 30, # 0 + 0 + 30

            ...

        },

        "Cow Loon": {

            "name": "Cow Loon",

            "base_min": (40, 40, 40),

            "base_max": (90, 60, 60),

            "general_base_max": 70, # (from v0.1.6: 60, average v0.2.1: 70)

            "rank_down_threshold": 120, # 40 * 3

            ...

        },

        "Mana Tank": {

            "name": "Mana Tank",

            "base_min": (50, 50, 50),

            "base_max": (80, 80, 80),

            "general_base_max": 80, # (average v0.2.1: 80)

            "rank_down_threshold": 150, # 50 * 3

            ...

        },

        "Gas Balloon": {

            "name": "Gas Balloon",

            "base_min": (50, 50, 50),

            "base_max": (70, 90, 70),

            "general_base_max": 80, # (average v0.2.1: 80)

            "rank_down_threshold": 150, # 50 * 3

            ...

        },

        "Absolute Blimp": {

            "name": "Absolute Blimp",

            "base_min": (0, 0, 0),

            "base_max": (100, 100, 100),

            "general_base_max": 100, # (from v0.1.6: 100, average v0.2.1: 100)

            "rank_down_threshold": 0,

            ...

        }

    }

[/code]

 

[code]

  File "game/screens.rpy", function at lines 2638-2640, inside "Inflation Capacity:" label:

                            # The general base (e.g., 30 for Tier B, 60 for Tier A) is now part of the rank_definitions (v0.2.1).

                            _base_tier_max_display = get_rank_data().get("general_base_max", 30) # General base from the current Tier/Rank

                            _effective_max_level = _base_tier_max_display + _current_max_bonus # Calculate base + bonus for display

[/code]

;)

On version 0.2.1, I got an error on the Dungeon Trader room:

[code]

I'm sorry, but an uncaught exception occurred.

While running game code:

  File "game/dungeon_rooms.rpy", line 337, in script call

    call expression _current_room_label from _call_expression_1

  File "game/dungeon_rooms.rpy", line 6487, in script

    $ offered_item_index = store.temp_selected_item_index

  File "game/dungeon_rooms.rpy", line 6487, in <module>

    $ offered_item_index = store.temp_selected_item_index

AttributeError: 'StoreModule' object has no attribute 'temp_selected_item_index'

-- Full Traceback ------------------------------------------------------------

Full traceback:

  File "game/dungeon_rooms.rpy", line 337, in script call

    call expression _current_room_label from _call_expression_1

  File "game/dungeon_rooms.rpy", line 6487, in script

    $ offered_item_index = store.temp_selected_item_index

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\ast.py", line 834, in execute

    renpy.python.py_exec_bytecode(self.code.bytecode, self.hide, store=self.store)

  File "C:\Users\Anom\Games\Yuki Pop Repeat\v0.2.1\renpy\python.py", line 1187, in py_exec_bytecode

    exec(bytecode, globals, locals)

  File "game/dungeon_rooms.rpy", line 6487, in <module>

    $ offered_item_index = store.temp_selected_item_index

AttributeError: 'StoreModule' object has no attribute 'temp_selected_item_index'

Windows-10-10.0.19041 AMD64

Ren'Py 8.3.8.25060602+nightly

Yuki, Pop, Repeat 0.2.1

Tue Nov 11 14:10:16 2025

[/code]

After viewing the dungeon_rooms.rpy file, and checking what changes from version 0.1.6, I made a quick fix on lines 6487 and 7001. This should work for the Trader Room and the Hungry Idol, and solves the error when a Trader or Hungry Idol room is encountered before any Cauldron room (the one where you drop a food or potion item), or when you choose the option to give an item but attempt to exit the item selection menu without picking any items. Since the store.temp_selected_item_index variable is never initialized by a "call screen offer_item_generic(..., cancel_jump_target="cauldron_action_canceled") _result" or  "call screen offer_item_generic(...)" action menu, accessing that undefined attribute directly will rise the AttributeError. The solution is to use a method that returns a default if the value is undefined:

[code]

  File "game/dungeon_rooms.rpy", line 6487 (trader):

    $ offered_item_index = getattr(store, 'temp_selected_item_index', None)

  File "game/dungeon_rooms.rpy", line 7001 (hungry idol):

    $ offered_item_index = getattr(store, 'temp_selected_item_index', None)

[/code]

;)

On version 0.2.1, I have been dealing with big missbehavior of multiple features of the dungeon rooms and equipment, some of them that worked on version 0.1.6. After checking and trying to solve the annoying not-working features, I got the mayor bug that has been causing ALL these troubles.

The Bug I found is an incorrect use of hasattr to check the existance of the player's inventory. All statements condition checks with "hasattr(store, 'persistent.player_inventory')" will result into False.

The Cause of this is that "hasattr(object, 'attribute_name')" only works with direct attributes of the object, not recursively with multiple nesting levels and dot notation. See https://stackoverflow.com/questions/4342168/can-hasattr-go-multiple-children-dee...

There are at least 3 Solutions to fix it.

  • Remove all ocurrences of "hasattr(store, 'persistent.player_inventory')" (replace with empty line or the rest of the current sentence). (what i did)
  • Use "hasattr(store, 'persistent') and hasattr(store.persistent, 'player_inventory)" instead.
  • Use a custom multi-level-hasattr function to be defined in init.rpy to perform "hasattr" recursively, following dot notation for the second argument (as shown in https://stackoverflow.com/questions/4342168/can-hasattr-go-multiple-children-dee...).

 

The following list show all the places affected by the bug, and the quick fixes I applied (all code line numbers based on original v0.2.1 unaltered files):

 

1) No detection of gold coins for spins at the Gamble room ("Gamble Room Unified Action Screen"). (remove and deindent)

[code]

  File "game/dungeon_rooms.rpy", line 1889, inside gamble_unified_actions_screen.

[/code] 

2) No detection of gold coins for purchases at the Vending Machine room. (remove and deindent)

[code]

  File "game/dungeon_rooms.rpy", line 7345, inside room_vending_machine.

[/code]

3) Tight Rubber Belt equipment not breaking at Rank Up. (changed)

  File "game/init.rpy", lines 1328-1334, inside _perform_rank_up:

[code]

        belt_idx_to_remove = -1

        

        for i, item_dict in enumerate(store.persistent.player_inventory):

            if item_dict.get("id") == "equip_tight_rubber_belt":

                belt_idx_to_remove = i

                belt_snapped_this_event = True

                break

[/code]

4) Current amount of gold calculated as 0. (removed and deindent)

[code]

  File "game/init.rpy", line 1928, inside get_current_gold_amount.

[/code]

    # if the behavior of this function is to calculate the total amount of gold, then line "gold_qty = item_dict.get("quantity", 0)" has to be replaced with "gold_qty += item_dict.get("quantity", 0)". Useful for Vending Machine room, the Cubi room and any other gold-centered mechanic inside the Dungeon.

5) Check of item in Yuki's inventory always returning False. (changed)

[code]

  File "game/init.rpy", line 1940, inside yuki_has_item:

        if not isinstance(store.persistent.player_inventory, list):

[/code]

6) Passive Equipment Effects not been applied. (changed)

[code]

  File "game/init.rpy", line 2673, inside process_passive_equipment_effects:

        if not isinstance(store.persistent.player_inventory, list): return []

[/code]

7) Inventory items not been displayed/selectable in the Barter Peddler room menu (offer item to trade with the peddler) and the Hungry Idol room menu (offer item to please the idol). (changed based on the code for the select_potion_for_throw_screen screen)

[code]

  File "game/screens.rpy", lines 4016-4049, inside offer_item_generic:

            python:

                # Create a new list of indices of items to display based on the filter

                _items_to_display_indices = []

                for i, item_dict in enumerate(store.persistent.player_inventory):

                    if item_type_filter:

                        _item_id_check = item_dict.get("id")

                        _item_data_check = store.item_definitions.get(_item_id_check, {})

                        if _item_data_check.get("type") == item_type_filter:

                            _items_to_display_indices.append(i) # Store single value as the original_index

                    else:

                        _items_to_display_indices.append(i) # No filter, show all items

            

            if not _items_to_display_indices:

            ...

                        # Loop through the filtered list

                        for original_index in _items_to_display_indices: # Iterate using the filtered indices

                            python:

                                # Fetch item details using index 'original_index' from original persistent.player_inventory

                                item_dict_offer = store.persistent.player_inventory[original_index]

                                _item_id_offer = item_dict_offer.get("id")

[/code]

;)