Advertisement
YaelBashan6

CraftingUI.rb v2

Jun 18th, 2025
24
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Ruby 32.86 KB | Software | 0 0
  1. # ===============================================================================
  2. # CRAFTING UI - SIMPLIFIED AND CONFIGURABLE VERSION (FIXED)
  3. # File: Plugins/CraftingSystem/CraftingUI.rb
  4. # ===============================================================================
  5.  
  6. #===============================================================================
  7. # CUSTOMIZATION CONSTANTS - MODIFY THESE TO CHANGE UI LAYOUT
  8. #===============================================================================
  9. module CraftingUIConfig
  10.   # === RECIPE LIST WINDOW CONFIGURATION ===
  11.  
  12.   RECIPE_WINDOW_X = 228        # X position of recipe list window
  13.   RECIPE_WINDOW_Y = 32         # Y position of recipe list window
  14.   RECIPE_WINDOW_WIDTH = 250    # Width of recipe list window
  15.   RECIPE_WINDOW_HEIGHT = 200   # Height of recipe list window
  16.   RECIPE_WINDOW_PADDING = 0    # Internal padding for recipe window
  17.   RECIPE_ROW_HEIGHT = 32       # Height of each recipe row
  18.   RECIPES_PER_PAGE = 6         # Number of recipes visible at once
  19.   RECIPE_WINDOW_Y_OFFSET = 16  # Y offset from background graphic to actual window
  20.   RECIPE_WINDOW_HEIGHT_OFFSET = 24  # Height reduction from background graphic
  21.  
  22.   # === RECIPE LIST CONFIGURATION ===
  23.   RECIPE_LIST_Y_OFFSET = 0     # FIXED: Remove negative offset that causes clipping
  24.  
  25.   # === RECIPE TEXT AREA MARGINS ===
  26.   TEXT_AREA_LEFT_MARGIN = 0    # Extra space on left side for text positioning
  27.   TEXT_AREA_RIGHT_MARGIN = 0   # Extra space on right side
  28.   TEXT_AREA_TOP_MARGIN = 0     # FIXED: Remove negative offset that causes clipping
  29.   TEXT_AREA_BOTTOM_MARGIN = 0  # Extra space on bottom for text positioning
  30.  
  31.   # === RECIPE TEXT POSITIONING ===
  32.   RECIPE_TEXT_X_OFFSET = 8     # Position relative to the margin
  33.   RECIPE_TEXT_Y_OFFSET = 8     # FIXED: Add positive offset to move text down from top edge
  34.   RECIPE_TEXT_MAX_CHAR_CALC = 40  # Used in max_chars calculation (rect.width - this) / 8
  35.   QUANTITY_TEXT_RIGHT_MARGIN = 8   # Right margin for quantity text (x2, x3, etc.)
  36.  
  37.   # === CURSOR CONFIGURATION ===
  38.   CURSOR_X_OFFSET = 0         # How far left/right to move cursor from text area
  39.   CURSOR_Y_OFFSET = 0          # How far up/down to move cursor
  40.  
  41.   # === INGREDIENTS DISPLAY CONFIGURATION ===
  42.   INGREDIENTS_WINDOW_X = 22                # X position of ingredients window
  43.   INGREDIENTS_WINDOW_Y = 34                # Y position of ingredients window
  44.   INGREDIENTS_WINDOW_PADDING = 8           # Internal padding for ingredients window only
  45.   INGREDIENTS_TITLE_Y_OFFSET = 16          # Y offset for "Ingredients" title from window top
  46.   INGREDIENTS_LIST_Y_OFFSET = 42           # Y offset for ingredient list from window top
  47.   INGREDIENT_SPACING = 28                  # Vertical spacing between ingredients
  48.   INGREDIENT_ICON_SIZE = 24                # Size of ingredient icons (24x24)
  49.   INGREDIENT_TEXT_X_OFFSET = 28            # X offset for ingredient text from icon
  50.   INGREDIENT_TEXT_Y_OFFSET = 4             # Y offset for ingredient text from icon top
  51.   INGREDIENT_FONT_SIZE_REDUCTION = 2       # How much smaller ingredient font should be
  52.  
  53.   # === RESULT ITEM DISPLAY CONFIGURATION ===
  54.   RESULT_ITEM_PANEL_X = 24             # X position of result item icon
  55.   RESULT_ITEM_PANEL_Y = 313            # Y position of result item icon
  56.   RESULT_ITEM_ICON_SIZE = 48           # Size of result item icon
  57.   RESULT_TEXT_X_OFFSET = 80            # X offset for result text from icon
  58.   RESULT_TEXT_Y = 300                  # Y position for result item name
  59.   RESULT_DESC_Y_OFFSET = 28            # Y offset for description from name
  60.   RESULT_DESC_LINE_HEIGHT = 25         # Line height for description text
  61.   RESULT_DESC_MAX_CHARS = 35           # Max characters per description line
  62.   RESULT_DESC_WRAP_THRESHOLD = 40      # Description length before word wrapping
  63.  
  64.   # === CATEGORY WINDOW CONFIGURATION ===
  65.   CATEGORY_WINDOW_Y = 220              # Y position of category window
  66.   CATEGORY_TEXT_Y_OFFSET = -8          # Y offset for category text from center
  67.   ARROW_LEFT_X = 8                     # X position of left arrow
  68.   ARROW_RIGHT_X = 188                  # X position of right arrow
  69.   ARROW_UP_X = 466                     # X position of up arrow
  70.   ARROW_UP_Y = 20                      # Y position of up arrow
  71.   ARROW_OPACITY_DIM = 128              # Opacity for dimmed arrows
  72.   ARROW_OPACITY_BRIGHT = 255           # Opacity for active arrows
  73.  
  74.   # === WINDOW BACKGROUND POSITIONS ===
  75.   DESC_WINDOW_Y = 280                  # Y position of description background window
  76.  
  77.   # === FONT CONFIGURATION ===
  78.   BASE_FONT_SIZE = 0                   # Offset from system font size (0 = use system default)
  79.   INGREDIENT_MIN_FONT_SIZE = 16        # Minimum font size for ingredients
  80.  
  81.   # === COLOR CONFIGURATION ===
  82.   COLOR_CRAFTABLE = Color.new(0, 120, 0)        # Green - can craft
  83.   COLOR_LOCKED = Color.new(80, 80, 80)          # Gray - unlocked but missing materials
  84.   COLOR_UNAVAILABLE = Color.new(150, 150, 150)  # Light gray - locked
  85.   COLOR_AVAILABLE = Color.new(0, 150, 0)        # Green for available ingredients
  86.   COLOR_MISSING = Color.new(150, 50, 50)        # Red for missing ingredients
  87.   COLOR_TEXT = Color.new(248, 248, 248)         # White text
  88.   COLOR_SHADOW = Color.new(0, 0, 0)             # Black shadow/outline
  89.   COLOR_CANCEL = Color.new(60, 60, 60)          # Cancel option color
  90.   COLOR_TITLE = Color.new(0, 0, 0)              # Title text color (ingredients)
  91.  
  92.   # === GRAPHICS PATHS - CHANGE THESE TO USE DIFFERENT GRAPHICS ===
  93.   CURSOR_GRAPHIC = "Graphics/UI/Crafting/cursor"
  94.   BACKGROUND_GRAPHIC = "Graphics/UI/Crafting/background.png"
  95.   INGREDIENTS_BG_GRAPHIC = "Graphics/UI/Crafting/ingredients_window.png"
  96.   RECIPE_BG_GRAPHIC = "Graphics/UI/Crafting/recipe_window.png"
  97.   CATEGORY_BG_GRAPHIC = "Graphics/UI/Crafting/category_window.png"
  98.   DESC_BG_GRAPHIC = "Graphics/UI/Crafting/craftable_description.png"
  99.   ARROW_LEFT_GRAPHIC = "Graphics/UI/Crafting/arrow_left.png"
  100.   ARROW_RIGHT_GRAPHIC = "Graphics/UI/Crafting/arrow_right.png"
  101.   ARROW_UP_GRAPHIC = "Graphics/UI/Crafting/arrow_vertical.png"
  102.   ITEM_GRAPHICS_PATH = "Graphics/Items/"  # Path to item graphics folder
  103.  
  104.   # === PERFORMANCE CONFIGURATION ===
  105.   REFRESH_ON_INDEX_CHANGE = true       # Whether to refresh ingredients when selection changes
  106. end
  107.  
  108. #===============================================================================
  109. # Recipe List Window (Using Built-in Cursor Like Pokemon Essentials Bag)
  110. #===============================================================================
  111. class Window_CraftingRecipeList < Window_DrawableCommand
  112.   include CraftingUIConfig
  113.  
  114.   attr_reader :current_category
  115.   attr_accessor :recipes
  116.  
  117.   def initialize(x, y, width, height)
  118.     @current_category = "All Items"
  119.     @recipes = []
  120.     @navigating = false  # Flag to prevent refresh interference
  121.    
  122.     super(x, y, width, height)
  123.     @selarrow = AnimatedBitmap.new(CURSOR_GRAPHIC)
  124.     self.windowskin = nil
  125.     @row_height = RECIPE_ROW_HEIGHT
  126.    
  127.     # Initialize index explicitly
  128.     @index = 0
  129.    
  130.     # CRITICAL: Disable built-in navigation completely
  131.     self.active = false
  132.    
  133.     # Create cursor as separate sprite
  134.     @cursor_sprite = nil
  135.    
  136.     update_recipe_list
  137.   end
  138.  
  139.   def dispose
  140.     @cursor_sprite&.dispose
  141.     @selarrow&.dispose
  142.     super
  143.   end
  144.  
  145.   # FIXED: Simplified setup_contents method
  146.   def setup_contents
  147.     dwidth = self.width - self.borderX
  148.     dheight = self.height - self.borderY
  149.     self.contents = pbDoEnsureBitmap(self.contents, dwidth, dheight)
  150.     self.contents.clear
  151.     setup_font(self.contents)
  152.     # Removed all the complex offset calculations that were causing issues
  153.   end
  154.  
  155.   # === CORE WINDOW METHODS ===
  156.   def item
  157.     return nil if @recipes.empty? && self.index != 0
  158.     return :cancel if self.index == @recipes.length
  159.     return :cancel if @recipes.empty? && self.index == 0
  160.     return nil if self.index >= @recipes.length
  161.     return @recipes[self.index]
  162.   end
  163.  
  164.   def itemCount
  165.     @recipes.empty? ? 1 : @recipes.length + 1
  166.   end
  167.  
  168.   def page_row_max
  169.     RECIPES_PER_PAGE
  170.   end
  171.  
  172.   def page_item_max
  173.     RECIPES_PER_PAGE
  174.   end
  175.  
  176.   # FIXED: Simplified itemRect method
  177.   def itemRect(item)
  178.     return Rect.new(0, 0, 0, 0) if item < 0 || item >= @item_max
  179.     return Rect.new(0, 0, 0, 0) if item < self.top_item || item >= self.top_item + self.page_item_max
  180.    
  181.     cursor_width = (self.width - self.borderX - ((@column_max - 1) * @column_spacing)) / @column_max
  182.     x = (item % @column_max * (cursor_width + @column_spacing)) + TEXT_AREA_LEFT_MARGIN
  183.     y = (item - self.top_item) * @row_height + TEXT_AREA_TOP_MARGIN  # Simplified Y calculation
  184.    
  185.     # Apply all margins to the rect dimensions
  186.     width = cursor_width - TEXT_AREA_LEFT_MARGIN - TEXT_AREA_RIGHT_MARGIN
  187.     height = @row_height - TEXT_AREA_TOP_MARGIN - TEXT_AREA_BOTTOM_MARGIN
  188.    
  189.     return Rect.new(x, y, width, height)
  190.   end
  191.  
  192.   # === DRAWING METHODS ===
  193.   def drawCursor(index, rect)
  194.     # Empty - cursor is handled by separate sprite
  195.   end
  196.  
  197.   def drawItem(index, _count, rect)
  198.     # Handle empty recipe list or cancel option
  199.     if @recipes.empty? || index == @recipes.length
  200.       draw_cancel_option(rect)
  201.       return
  202.     end
  203.    
  204.     return if index >= @recipes.length
  205.     recipe = @recipes[index]
  206.     return unless recipe
  207.    
  208.     draw_recipe_item(recipe, rect)
  209.   end
  210.  
  211.   # === PRIVATE DRAWING HELPERS ===
  212.   private
  213.  
  214.   def draw_cancel_option(rect)
  215.     textpos = [["Cancel", rect.x + RECIPE_TEXT_X_OFFSET, rect.y + RECIPE_TEXT_Y_OFFSET, :left, COLOR_CANCEL, COLOR_TEXT]]
  216.     pbDrawTextPositions(self.contents, textpos)
  217.   end
  218.  
  219.   def draw_recipe_item(recipe, rect)
  220.     color = get_recipe_color(recipe)
  221.     max_chars = ((rect.width - RECIPE_TEXT_MAX_CHAR_CALC) / 8).to_i
  222.     name = recipe.name.length > max_chars ? recipe.name[0..max_chars-3] + "..." : recipe.name
  223.    
  224.     textpos = [[name, rect.x + RECIPE_TEXT_X_OFFSET, rect.y + RECIPE_TEXT_Y_OFFSET, :left, color, COLOR_TEXT]]
  225.    
  226.     if recipe.result_quantity > 1
  227.       qty_text = "x#{recipe.result_quantity}"
  228.       qty_x = rect.x + rect.width - self.contents.text_size(qty_text).width - QUANTITY_TEXT_RIGHT_MARGIN
  229.       textpos.push([qty_text, qty_x, rect.y + RECIPE_TEXT_Y_OFFSET, :left, color, COLOR_TEXT])
  230.     end
  231.    
  232.     pbDrawTextPositions(self.contents, textpos)
  233.   end
  234.  
  235.   def get_recipe_color(recipe)
  236.     if recipe.can_craft?
  237.       COLOR_CRAFTABLE
  238.     elsif recipe.unlocked?
  239.       COLOR_LOCKED
  240.     else
  241.       COLOR_UNAVAILABLE
  242.     end
  243.   end
  244.  
  245.   # === PUBLIC METHODS ===
  246.   public
  247.  
  248.   def refresh
  249.     @item_max = itemCount
  250.     self.update_cursor_rect
  251.     setup_contents
  252.    
  253.     # Draw items and update cursor sprite
  254.     draw_all_visible_items
  255.     update_cursor_sprite  # Update separate cursor sprite
  256.   end
  257.  
  258.   # FIXED: Make cursor position exactly match text position
  259.   def update_cursor_sprite
  260.     @cursor_sprite&.dispose
  261.     return if @index < 0
  262.    
  263.     # Get the item rectangle
  264.     rect = itemRect(@index)
  265.    
  266.     # Position cursor exactly where the text is drawn
  267.     cursor_x = self.x + self.borderX + rect.x + RECIPE_TEXT_X_OFFSET  
  268.     cursor_y = self.y + self.borderY + rect.y + RECIPE_TEXT_Y_OFFSET
  269.     cursor_width = rect.width - RECIPE_TEXT_X_OFFSET  
  270.     cursor_height = rect.height - RECIPE_TEXT_Y_OFFSET  
  271.    
  272.     # Create cursor sprite that matches the text position exactly
  273.     @cursor_sprite = IconSprite.new(cursor_x, cursor_y, self.viewport)
  274.    
  275.     cursor_bitmap = Bitmap.new(cursor_width, cursor_height)
  276.     cursor_bitmap.stretch_blt(
  277.       Rect.new(0, 0, cursor_width, cursor_height),
  278.       @selarrow.bitmap,
  279.       Rect.new(0, 0, @selarrow.bitmap.width, @selarrow.bitmap.height)
  280.     )
  281.    
  282.     @cursor_sprite.bitmap = cursor_bitmap
  283.     @cursor_sprite.z = self.z + 1  # Draw above window
  284.     @cursor_sprite.visible = true  # Ensure it's visible
  285.   end
  286.  
  287.   # Override update to prevent built-in navigation
  288.   def update
  289.     # Don't call super - this prevents built-in input handling
  290.     # Only update the animated cursor bitmap
  291.     if @selarrow
  292.       @selarrow.update
  293.     end
  294.   end
  295.  
  296.   def setup_font(bitmap)
  297.     pbSetSystemFont(bitmap)
  298.     if BASE_FONT_SIZE != 0
  299.       bitmap.font.size += BASE_FONT_SIZE
  300.     end
  301.   end
  302.  
  303.   # FIXED: Account for text offset in cursor positioning
  304.   def update_cursor_sprite
  305.     @cursor_sprite&.dispose
  306.     return if @index < 0
  307.    
  308.     # Use the same calculation as itemRect for consistency
  309.     rect = itemRect(@index)
  310.     cursor_x = self.x + rect.x
  311.     cursor_y = self.y + rect.y + self.borderY + RECIPE_TEXT_Y_OFFSET  # Add text offset
  312.     cursor_width = rect.width
  313.     cursor_height = rect.height
  314.    
  315.     # DEBUG: Print detailed positioning info
  316.     puts "=== CURSOR DEBUG ==="
  317.     puts "Index: #{@index}"
  318.     puts "Window position: (#{self.x}, #{self.y})"
  319.     puts "ItemRect: #{rect.x}, #{rect.y}, #{rect.width}, #{rect.height}"
  320.     puts "BorderY: #{self.borderY}"
  321.     puts "Text Y offset: #{RECIPE_TEXT_Y_OFFSET}"
  322.     puts "Final cursor position: (#{cursor_x}, #{cursor_y})"
  323.     puts "Top item: #{self.top_item}"
  324.     puts "Row height: #{@row_height}"
  325.     puts "===================="
  326.    
  327.     # Create cursor sprite that matches the item rectangle exactly
  328.     @cursor_sprite = IconSprite.new(cursor_x, cursor_y, self.viewport)
  329.    
  330.     cursor_bitmap = Bitmap.new(cursor_width, cursor_height)
  331.     cursor_bitmap.stretch_blt(
  332.       Rect.new(0, 0, cursor_width, cursor_height),
  333.       @selarrow.bitmap,
  334.       Rect.new(0, 0, @selarrow.bitmap.width, @selarrow.bitmap.height)
  335.     )
  336.    
  337.     @cursor_sprite.bitmap = cursor_bitmap
  338.     @cursor_sprite.z = self.z + 1  # Draw above window
  339.     @cursor_sprite.visible = true  # Ensure it's visible
  340.   end
  341.  
  342.   # FIXED: Match Pokemon Essentials bag logic exactly
  343.   def draw_all_visible_items
  344.     @item_max.times do |i|
  345.       next if i < self.top_item - 1 || i > self.top_item + self.page_item_max  # FIXED: Match bag logic
  346.       drawItem(i, @item_max, itemRect(i))
  347.     end
  348.   end
  349.  
  350.   def update_recipe_list
  351.     @recipes = CraftingRecipeManager.recipes_by_category(@current_category) || []
  352.     @item_max = itemCount
  353.    
  354.     # Ensure index stays within bounds
  355.     max_index = [itemCount - 1, 0].max
  356.     self.index = [self.index, max_index].min
  357.     self.index = [self.index, 0].max
  358.    
  359.     refresh
  360.   end
  361.  
  362.   def index=(new_index)
  363.     old_index = @index
  364.    
  365.     # Only allow changes from specific safe sources
  366.     caller_info = caller[0..1].join(" <- ")
  367.     puts "INDEX SETTER: #{old_index} → #{new_index} called by: #{caller_info}"
  368.    
  369.     super(new_index)
  370.     if @index != old_index
  371.       puts "INDEX: Actually changed from #{old_index} to #{@index}"
  372.       update_cursor_sprite
  373.     end
  374.   end
  375.  
  376.   def current_category=(new_category)
  377.     return if @current_category == new_category
  378.     @current_category = new_category
  379.     update_recipe_list
  380.   end
  381. end
  382.  
  383. #===============================================================================
  384. # Crafting Scene (Visual Management) - Enhanced with Ingredients Display
  385. #===============================================================================
  386. class CraftingScene
  387.   include CraftingUIConfig
  388.  
  389.   def initialize
  390.     @current_category = "All Items"
  391.     @cached_graphics = {}  # Cache for loaded graphics
  392.   end
  393.  
  394.   def pbStartScene
  395.     create_viewport
  396.     create_background_sprites
  397.     create_windows
  398.     create_overlays
  399.    
  400.     update_arrow_visibility
  401.     pbRefresh
  402.     pbFadeInAndShow(@sprites)
  403.   end
  404.  
  405.   def pbEndScene
  406.     pbFadeOutAndHide(@sprites)
  407.     dispose
  408.   end
  409.  
  410.   def dispose
  411.     dispose_cached_graphics
  412.     pbDisposeSpriteHash(@sprites)
  413.     @viewport&.dispose
  414.   end
  415.  
  416.   # === INITIALIZATION METHODS ===
  417.   private
  418.  
  419.   def create_viewport
  420.     @viewport = Viewport.new(0, 0, Graphics.width, Graphics.height)
  421.     @viewport.z = 99999
  422.     @sprites = {}
  423.   end
  424.  
  425.   def create_background_sprites
  426.     # Main background
  427.     @sprites["background"] = IconSprite.new(0, 0, @viewport)
  428.     @sprites["background"].setBitmap(BACKGROUND_GRAPHIC)
  429.     @sprites["background"].z = 0
  430.    
  431.     # Window backgrounds
  432.     create_window_backgrounds
  433.     create_navigation_arrows
  434.   end
  435.  
  436.   def create_window_backgrounds
  437.     @sprites["ingredients_bg"] = IconSprite.new(INGREDIENTS_WINDOW_X, INGREDIENTS_WINDOW_Y, @viewport)
  438.     @sprites["ingredients_bg"].setBitmap(INGREDIENTS_BG_GRAPHIC)
  439.     @sprites["ingredients_bg"].z = 1
  440.    
  441.     @sprites["recipe_bg"] = IconSprite.new(RECIPE_WINDOW_X, RECIPE_WINDOW_Y, @viewport)
  442.     @sprites["recipe_bg"].setBitmap(RECIPE_BG_GRAPHIC)
  443.     @sprites["recipe_bg"].z = 1
  444.    
  445.     @sprites["category_bg"] = IconSprite.new(0, CATEGORY_WINDOW_Y, @viewport)
  446.     @sprites["category_bg"].setBitmap(CATEGORY_BG_GRAPHIC)
  447.     @sprites["category_bg"].z = 1
  448.    
  449.     @sprites["description_bg"] = IconSprite.new(0, DESC_WINDOW_Y, @viewport)
  450.     @sprites["description_bg"].setBitmap(DESC_BG_GRAPHIC)
  451.     @sprites["description_bg"].z = 1
  452.    
  453.     center_category_window
  454.   end
  455.  
  456.   def create_navigation_arrows
  457.     @sprites["arrow_left"] = IconSprite.new(ARROW_LEFT_X, CATEGORY_WINDOW_Y, @viewport)
  458.     @sprites["arrow_left"].setBitmap(ARROW_LEFT_GRAPHIC)
  459.     @sprites["arrow_left"].z = 2
  460.    
  461.     @sprites["arrow_right"] = IconSprite.new(ARROW_RIGHT_X, CATEGORY_WINDOW_Y, @viewport)
  462.     @sprites["arrow_right"].setBitmap(ARROW_RIGHT_GRAPHIC)
  463.     @sprites["arrow_right"].z = 2
  464.    
  465.     @sprites["arrow_up"] = IconSprite.new(ARROW_UP_X, ARROW_UP_Y, @viewport)
  466.     @sprites["arrow_up"].setBitmap(ARROW_UP_GRAPHIC)
  467.     @sprites["arrow_up"].z = 2
  468.   end
  469.  
  470.   def create_windows
  471.     # Recipe list window with configurable positioning
  472.     window_x = RECIPE_WINDOW_X + RECIPE_WINDOW_PADDING
  473.     window_y = RECIPE_WINDOW_Y + RECIPE_WINDOW_Y_OFFSET
  474.     window_w = RECIPE_WINDOW_WIDTH - RECIPE_WINDOW_PADDING * 2
  475.     window_h = RECIPE_WINDOW_HEIGHT - RECIPE_WINDOW_HEIGHT_OFFSET
  476.    
  477.     @sprites["recipelist"] = Window_CraftingRecipeList.new(window_x, window_y, window_w, window_h)
  478.     @sprites["recipelist"].viewport = @viewport
  479.     @sprites["recipelist"].z = 2
  480.     @sprites["recipelist"].current_category = @current_category
  481.    
  482.     # Connect the scene to the window for ingredient updates
  483.     @sprites["recipelist"].instance_variable_set(:@scene, self)
  484.   end
  485.  
  486.   def create_overlays
  487.     # Create overlays with proper font setup
  488.     ["ingredients_overlay", "result_item_overlay", "overlay"].each do |overlay_name|
  489.       @sprites[overlay_name] = BitmapSprite.new(Graphics.width, Graphics.height, @viewport)
  490.       setup_overlay_font(@sprites[overlay_name].bitmap)
  491.       @sprites[overlay_name].z = 3
  492.     end
  493.   end
  494.  
  495.   def setup_overlay_font(bitmap)
  496.     pbSetSystemFont(bitmap)
  497.     if BASE_FONT_SIZE != 0
  498.       bitmap.font.size += BASE_FONT_SIZE
  499.     end
  500.   end
  501.  
  502.   # === GRAPHICS MANAGEMENT ===
  503.   def load_item_graphic(item_symbol)
  504.     # Return cached graphic if available
  505.     return @cached_graphics[item_symbol] if @cached_graphics[item_symbol]
  506.    
  507.     item_name = item_symbol.to_s
  508.     graphic_paths = generate_graphic_paths(item_name)
  509.    
  510.     # Try to load from available paths
  511.     graphic_paths.each do |path|
  512.       if File.exist?(path)
  513.         @cached_graphics[item_symbol] = Bitmap.new(path)
  514.         return @cached_graphics[item_symbol]
  515.       end
  516.     end
  517.    
  518.     # Create simple fallback graphic
  519.     @cached_graphics[item_symbol] = create_fallback_graphic
  520.     return @cached_graphics[item_symbol]
  521.   end
  522.  
  523.   def generate_graphic_paths(item_name)
  524.     base_path = ITEM_GRAPHICS_PATH
  525.     [
  526.       "#{base_path}#{item_name}.png",
  527.       "#{base_path}#{item_name.downcase}.png",
  528.       "#{base_path}#{item_name.capitalize}.png",
  529.       "#{base_path}#{item_name}.PNG"
  530.     ]
  531.   end
  532.  
  533.   def create_fallback_graphic
  534.     fallback = Bitmap.new(32, 32)
  535.     fallback.fill_rect(0, 0, 32, 32, Color.new(200, 200, 200))
  536.     fallback
  537.   end
  538.  
  539.   def dispose_cached_graphics
  540.     return unless @cached_graphics
  541.     @cached_graphics.each_value(&:dispose)
  542.     @cached_graphics.clear
  543.   end
  544.  
  545.   # === DISPLAY UPDATE METHODS ===
  546.   public
  547.  
  548.   def update_ingredients_display
  549.     clear_overlays
  550.    
  551.     recipe = current_recipe
  552.     draw_ingredients_title
  553.    
  554.     return unless recipe && recipe != :cancel && recipe.is_a?(CraftingRecipe)
  555.    
  556.     draw_ingredients_list(recipe)
  557.     draw_result_item(recipe)
  558.   end
  559.  
  560.   private
  561.  
  562.   def clear_overlays
  563.     @sprites["ingredients_overlay"].bitmap.clear
  564.     @sprites["result_item_overlay"].bitmap.clear
  565.    
  566.     # Ensure fonts are properly set
  567.     setup_overlay_font(@sprites["ingredients_overlay"].bitmap)
  568.     setup_overlay_font(@sprites["result_item_overlay"].bitmap)
  569.   end
  570.  
  571.   def draw_ingredients_title
  572.     # Calculate centered position for title
  573.     ingredients_window_width = get_ingredients_window_width
  574.     title_x = INGREDIENTS_WINDOW_X + (ingredients_window_width / 2)
  575.     title_y = INGREDIENTS_WINDOW_Y + INGREDIENTS_TITLE_Y_OFFSET
  576.    
  577.     textpos = [["Ingredients", title_x, title_y, :center, COLOR_TITLE, COLOR_TEXT]]
  578.     pbDrawTextPositions(@sprites["ingredients_overlay"].bitmap, textpos)
  579.   end
  580.  
  581.   def get_ingredients_window_width
  582.     begin
  583.       temp_bitmap = Bitmap.new(INGREDIENTS_BG_GRAPHIC)
  584.       width = temp_bitmap.width
  585.       temp_bitmap.dispose
  586.       width
  587.     rescue
  588.       180  # Fallback width
  589.     end
  590.   end
  591.  
  592.   def draw_ingredients_list(recipe)
  593.     ingredients_x = INGREDIENTS_WINDOW_X + INGREDIENTS_WINDOW_PADDING  
  594.     ingredients_y = INGREDIENTS_WINDOW_Y + INGREDIENTS_LIST_Y_OFFSET
  595.    
  596.     recipe.ingredients.each_with_index do |(item_symbol, quantity), index|
  597.       y_pos = ingredients_y + (index * INGREDIENT_SPACING)
  598.       draw_single_ingredient(item_symbol, quantity, ingredients_x, y_pos)
  599.     end
  600.   end
  601.  
  602.   def draw_single_ingredient(item_symbol, quantity, x, y)
  603.     # Draw ingredient icon (scaled and positioned)
  604.     draw_ingredient_icon(item_symbol, x, y)
  605.    
  606.     # Draw ingredient text with availability coloring
  607.     draw_ingredient_text(item_symbol, quantity, x + INGREDIENT_TEXT_X_OFFSET, y)
  608.   end
  609.  
  610.   def draw_ingredient_icon(item_symbol, x, y)
  611.     item_graphic = load_item_graphic(item_symbol)
  612.     return unless item_graphic
  613.    
  614.     scaled_graphic = create_scaled_bitmap(item_graphic, INGREDIENT_ICON_SIZE, INGREDIENT_ICON_SIZE)
  615.    
  616.     @sprites["ingredients_overlay"].bitmap.blt(x, y, scaled_graphic,
  617.       Rect.new(0, 0, INGREDIENT_ICON_SIZE, INGREDIENT_ICON_SIZE))
  618.    
  619.     scaled_graphic.dispose  # Only dispose the scaled version
  620.   end
  621.  
  622.   def draw_ingredient_text(item_symbol, quantity, x, y)
  623.     item_name = get_item_name(item_symbol)
  624.     player_quantity = get_player_quantity(item_symbol)
  625.    
  626.     # Determine text color based on availability
  627.     text_color = player_quantity >= quantity ? COLOR_AVAILABLE : COLOR_MISSING
  628.     ingredient_text = "#{item_name} x#{quantity}"
  629.    
  630.     # Temporarily adjust font size
  631.     with_smaller_font do
  632.       textpos = [[ingredient_text, x, y + INGREDIENT_TEXT_Y_OFFSET, :left, text_color, COLOR_TEXT]]
  633.       pbDrawTextPositions(@sprites["ingredients_overlay"].bitmap, textpos)
  634.     end
  635.   end
  636.  
  637.   def draw_result_item(recipe)
  638.     # Draw result item icon
  639.     result_graphic = load_item_graphic(recipe.result_item)
  640.     return unless result_graphic
  641.    
  642.     scaled_result = create_scaled_bitmap(result_graphic, RESULT_ITEM_ICON_SIZE, RESULT_ITEM_ICON_SIZE)
  643.    
  644.     @sprites["result_item_overlay"].bitmap.blt(RESULT_ITEM_PANEL_X, RESULT_ITEM_PANEL_Y,
  645.       scaled_result, Rect.new(0, 0, RESULT_ITEM_ICON_SIZE, RESULT_ITEM_ICON_SIZE))
  646.    
  647.     scaled_result.dispose
  648.    
  649.     # Draw result item text and description
  650.     draw_result_item_text(recipe)
  651.   end
  652.  
  653.   def draw_result_item_text(recipe)
  654.     result_name = get_item_name(recipe.result_item)
  655.     result_desc = get_item_description(recipe.result_item)
  656.    
  657.     # Position text relative to icon
  658.     text_x = RESULT_ITEM_PANEL_X + RESULT_TEXT_X_OFFSET
  659.     name_and_qty = "#{result_name} x#{recipe.result_quantity}"
  660.    
  661.     textpos = [[name_and_qty, text_x, RESULT_TEXT_Y, :left, COLOR_TEXT, COLOR_SHADOW]]
  662.    
  663.     # Add word-wrapped description
  664.     desc_y = RESULT_TEXT_Y + RESULT_DESC_Y_OFFSET
  665.     add_wrapped_description(textpos, result_desc, text_x, desc_y)
  666.    
  667.     pbDrawTextPositions(@sprites["result_item_overlay"].bitmap, textpos)
  668.   end
  669.  
  670.   def add_wrapped_description(textpos, description, x, start_y)
  671.     if description.length > RESULT_DESC_WRAP_THRESHOLD
  672.       desc_lines = wrap_text(description, RESULT_DESC_MAX_CHARS)
  673.       desc_lines.each_with_index do |line, idx|
  674.         y_pos = start_y + (idx * RESULT_DESC_LINE_HEIGHT)
  675.         textpos << [line, x, y_pos, :left, COLOR_TEXT, COLOR_SHADOW]
  676.       end
  677.     else
  678.       textpos << [description, x, start_y, :left, COLOR_TEXT, COLOR_SHADOW]
  679.     end
  680.   end
  681.  
  682.   # === UTILITY METHODS ===
  683.   def create_scaled_bitmap(source, width, height)
  684.     scaled = Bitmap.new(width, height)
  685.     scaled.stretch_blt(Rect.new(0, 0, width, height), source,
  686.       Rect.new(0, 0, source.width, source.height))
  687.     scaled
  688.   end
  689.  
  690.   def with_smaller_font
  691.     overlay = @sprites["ingredients_overlay"].bitmap
  692.     old_size = overlay.font.size
  693.     new_size = [old_size - INGREDIENT_FONT_SIZE_REDUCTION, INGREDIENT_MIN_FONT_SIZE].max
  694.     overlay.font.size = new_size
  695.     yield
  696.     overlay.font.size = old_size
  697.   end
  698.  
  699.   def wrap_text(text, max_chars)
  700.     words = text.split(' ')
  701.     lines = []
  702.     current_line = ""
  703.    
  704.     words.each do |word|
  705.       if (current_line + word).length > max_chars
  706.         lines << current_line.strip unless current_line.strip.empty?
  707.         current_line = word + " "
  708.       else
  709.         current_line += word + " "
  710.       end
  711.     end
  712.     lines << current_line.strip unless current_line.strip.empty?
  713.     lines
  714.   end
  715.  
  716.   def get_item_name(item_symbol)
  717.     GameData::Item.get(item_symbol).name
  718.   rescue
  719.     item_symbol.to_s.capitalize
  720.   end
  721.  
  722.   def get_item_description(item_symbol)
  723.     GameData::Item.get(item_symbol).description || "A crafted item."
  724.   rescue
  725.     "A crafted item."
  726.   end
  727.  
  728.   def get_player_quantity(item_symbol)
  729.     $bag.quantity(item_symbol)
  730.   rescue
  731.     0
  732.   end
  733.  
  734.   # === PUBLIC INTERFACE METHODS ===
  735.   public
  736.  
  737.   def pbRefresh
  738.     # FIXED: Only refresh the recipe list if it's not currently being navigated
  739.     @sprites["recipelist"]&.refresh
  740.     update_arrow_visibility
  741.     update_category_text
  742.     update_ingredients_display
  743.   end
  744.  
  745.   def update_category_text
  746.     return unless @sprites["overlay"]
  747.    
  748.     @sprites["overlay"].bitmap.clear
  749.     setup_overlay_font(@sprites["overlay"].bitmap)
  750.    
  751.     # Center category text in window with configurable offset
  752.     category_x, category_y = get_category_position
  753.     category_width, category_height = get_category_dimensions
  754.    
  755.     text_x = category_x + (category_width / 2)
  756.     text_y = category_y + (category_height / 2) + CATEGORY_TEXT_Y_OFFSET
  757.    
  758.     textpos = [[@current_category, text_x, text_y, :center, COLOR_TEXT, COLOR_SHADOW]]
  759.     pbDrawTextPositions(@sprites["overlay"].bitmap, textpos)
  760.   end
  761.  
  762.   def center_category_window
  763.     ingredients_center = INGREDIENTS_WINDOW_X + (get_ingredients_window_width / 2)
  764.     category_width = get_category_dimensions[0]
  765.     @sprites["category_bg"].x = (ingredients_center - (category_width / 2)).to_i
  766.   end
  767.  
  768.   def update_arrow_visibility
  769.     # Set arrow opacity based on current category position using configurable values
  770.     case @current_category
  771.     when "All Items"
  772.       set_arrow_opacity(ARROW_OPACITY_DIM, ARROW_OPACITY_BRIGHT)  # Left dim, right bright
  773.     when "Pokéballs"
  774.       set_arrow_opacity(ARROW_OPACITY_BRIGHT, ARROW_OPACITY_DIM)  # Left bright, right dim
  775.     else
  776.       set_arrow_opacity(ARROW_OPACITY_BRIGHT, ARROW_OPACITY_BRIGHT)  # Both bright
  777.     end
  778.    
  779.     @sprites["arrow_up"].opacity = ARROW_OPACITY_BRIGHT
  780.   end
  781.  
  782.   def switch_category(direction)
  783.     old_category = @current_category
  784.    
  785.     @current_category = case direction
  786.     when :left
  787.       case @current_category
  788.       when "Candies" then "All Items"
  789.       when "Potions" then "Candies"
  790.       when "Pokéballs" then "Potions"
  791.       else @current_category
  792.       end
  793.     when :right
  794.       case @current_category
  795.       when "All Items" then "Candies"
  796.       when "Candies" then "Potions"
  797.       when "Potions" then "Pokéballs"
  798.       else @current_category
  799.       end
  800.     else
  801.       @current_category
  802.     end
  803.    
  804.     # Only update if category actually changed
  805.     if @current_category != old_category
  806.       # FIXED: Don't call pbRefresh during category switch - it interferes with navigation
  807.       @sprites["recipelist"].current_category = @current_category
  808.       update_arrow_visibility
  809.       update_category_text
  810.       update_ingredients_display  # Only update ingredients, not full refresh
  811.     end
  812.   end
  813.  
  814.   def pbUpdate
  815.     pbUpdateSpriteHash(@sprites)
  816.   end
  817.  
  818.   def current_recipe
  819.     @sprites["recipelist"].item
  820.   end
  821.  
  822.   def current_category
  823.     @current_category
  824.   end
  825.  
  826.   def sprites
  827.     @sprites
  828.   end
  829.  
  830.   # === PRIVATE HELPER METHODS ===
  831.   private
  832.  
  833.   def get_category_position
  834.     [@sprites["category_bg"].x, @sprites["category_bg"].y]
  835.   end
  836.  
  837.   def get_category_dimensions
  838.     begin
  839.       temp_bitmap = Bitmap.new(CATEGORY_BG_GRAPHIC)
  840.       width, height = temp_bitmap.width, temp_bitmap.height
  841.       temp_bitmap.dispose
  842.       [width, height]
  843.     rescue
  844.       [80, 32]  # Fallback dimensions
  845.     end
  846.   end
  847.  
  848.   def set_arrow_opacity(left_opacity, right_opacity)
  849.     @sprites["arrow_left"].opacity = left_opacity
  850.     @sprites["arrow_right"].opacity = right_opacity
  851.   end
  852. end
  853.  
  854. #===============================================================================
  855. # Crafting Screen (Input Handling) - Optimized
  856. #===============================================================================
  857. class CraftingScreen
  858.   include CraftingUIConfig
  859.  
  860.   def initialize(scene)
  861.     @scene = scene
  862.   end
  863.  
  864.   def pbStartScreen
  865.     @scene.pbStartScene
  866.     @recipe_window = @scene.sprites["recipelist"]
  867.    
  868.     handle_input_loop
  869.     @scene.pbEndScene
  870.   end
  871.  
  872.   private
  873.  
  874.   def handle_input_loop
  875.     loop do
  876.       Graphics.update
  877.       Input.update
  878.       @scene.pbUpdate
  879.      
  880.       break if process_input
  881.     end
  882.   end
  883.  
  884.   def process_input
  885.     if Input.trigger?(Input::UP)
  886.       navigate_recipes(-1)
  887.     elsif Input.trigger?(Input::DOWN)
  888.       navigate_recipes(1)
  889.     elsif Input.trigger?(Input::LEFT)
  890.       @scene.switch_category(:left)
  891.       pbPlayCursorSE
  892.     elsif Input.trigger?(Input::RIGHT)
  893.       @scene.switch_category(:right)
  894.       pbPlayCursorSE
  895.     elsif Input.trigger?(Input::USE)
  896.       return true if handle_selection
  897.     elsif Input.trigger?(Input::BACK)
  898.       pbPlayCloseMenuSE
  899.       return true
  900.     end
  901.    
  902.     false  # Continue loop
  903.   end
  904.  
  905.   def navigate_recipes(direction)
  906.     old_index = @recipe_window.index
  907.     new_index = old_index + direction
  908.     max_index = @recipe_window.itemCount - 1
  909.    
  910.     # Ensure new_index stays within bounds
  911.     if new_index >= 0 && new_index <= max_index
  912.       puts "NAVIGATION: Moving from #{old_index} to #{new_index}"
  913.      
  914.       # Set index directly without triggering the setter's protection
  915.       @recipe_window.instance_variable_set(:@index, new_index)
  916.      
  917.       # Update cursor manually since we bypassed the setter
  918.       @recipe_window.update_cursor_sprite
  919.      
  920.       # Update ingredients display
  921.       @scene.update_ingredients_display
  922.      
  923.       pbPlayCursorSE
  924.     else
  925.       puts "NAVIGATION: Can't move to #{new_index} (out of bounds)"
  926.     end
  927.   end
  928.  
  929.   def handle_selection
  930.     current_item = @scene.current_recipe
  931.    
  932.     # Handle cancel option
  933.     if current_item == :cancel
  934.       pbPlayCloseMenuSE
  935.       return true
  936.     end
  937.    
  938.     # Handle recipe crafting
  939.     if current_item&.is_a?(CraftingRecipe)
  940.       attempt_crafting(current_item)
  941.     else
  942.       pbPlayBuzzerSE
  943.     end
  944.    
  945.     false
  946.   end
  947.  
  948.   def attempt_crafting(recipe)
  949.     if recipe.can_craft?
  950.       pbPlayDecisionSE
  951.       if pbConfirmMessage("Craft #{recipe.name}?")
  952.         craft_recipe(recipe)
  953.       end
  954.     else
  955.       pbPlayBuzzerSE
  956.       show_crafting_error(recipe)
  957.     end
  958.   end
  959.  
  960.   def craft_recipe(recipe)
  961.     if recipe.craft!
  962.       pbMessage("Successfully crafted #{recipe.name}!")
  963.       @recipe_window.refresh
  964.       @scene.pbRefresh
  965.     else
  966.       pbPlayBuzzerSE
  967.       pbMessage("Failed to craft #{recipe.name}!")
  968.     end
  969.   end
  970.  
  971.   def show_crafting_error(recipe)
  972.     if recipe.unlocked?
  973.       pbMessage("You don't have the required materials to craft #{recipe.name}!")
  974.     else
  975.       unlock_text = recipe.unlock_condition || "Complete certain requirements"
  976.       pbMessage("This recipe is locked! #{unlock_text}")
  977.     end
  978.   end
  979. end
  980.  
  981. #===============================================================================
  982. # Main entry point and backward compatibility
  983. #===============================================================================
  984. def pbCraftingMenu
  985.   scene = CraftingScene.new
  986.   screen = CraftingScreen.new(scene)
  987.   screen.pbStartScreen
  988. end
  989.  
  990. # Backward compatibility
  991. class Scene_Crafting
  992.   def main
  993.     pbCraftingMenu
  994.   end
  995. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement