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.
;)
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]
;)
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.
A Solution to avoid these scenarios is:
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]
;)
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".
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]
;)
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).
A 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.
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]
;)