require_relative '../rails_helper' RSpec.describe Outfit do fixtures :zones, :colors, :species let(:blue) { colors(:blue) } let(:acara) { species(:acara) } before do PetType.destroy_all @pet_type = PetType.create!(color: blue, species: acara, body_id: 1) @pet_state = create_pet_state(@pet_type, "HAPPY_MASC") @outfit = Outfit.new(pet_state: @pet_state) end def create_pet_state(pet_type, pose) # Create a basic biology asset so pet state saves correctly swf_asset = SwfAsset.create!( type: "biology", remote_id: (SwfAsset.maximum(:remote_id) || 0) + 1, url: "https://images.neopets.example/biology.swf", zone: zones(:body), zones_restrict: "", body_id: pet_type.body_id ) PetState.create!( pet_type: pet_type, pose: pose, swf_assets: [swf_asset], swf_asset_ids: [swf_asset.id] ) end def create_item(name, zone, body_id: 1, zones_restrict: "") item = Item.create!( name: name, description: "Test item", thumbnail_url: "https://images.neopets.example/item.png", zones_restrict: zones_restrict, rarity: "Common", price: 100 ) swf_asset = SwfAsset.create!( type: "object", remote_id: (SwfAsset.maximum(:remote_id) || 0) + 1, url: "https://images.neopets.example/#{name}.swf", zone: zone, zones_restrict: zones_restrict, body_id: body_id ) item.swf_assets << swf_asset item end describe "Item::Appearance#compatible_with?" do it "returns true for items in different zones with no restrictions" do hat = create_item("Hat", zones(:hat1)) shirt = create_item("Shirt", zones(:shirtdress)) appearances = Item.appearances_for([hat, shirt], @pet_type) hat_appearance = appearances[hat.id] shirt_appearance = appearances[shirt.id] expect(hat_appearance.compatible_with?(shirt_appearance)).to be true expect(shirt_appearance.compatible_with?(hat_appearance)).to be true end it "returns false for items in the same zone" do hat1 = create_item("Hat 1", zones(:hat1)) hat2 = create_item("Hat 2", zones(:hat1)) appearances = Item.appearances_for([hat1, hat2], @pet_type) hat1_appearance = appearances[hat1.id] hat2_appearance = appearances[hat2.id] expect(hat1_appearance.compatible_with?(hat2_appearance)).to be false expect(hat2_appearance.compatible_with?(hat1_appearance)).to be false end it "returns false when one item restricts a zone the other occupies" do # Create a hat that restricts the ruff zone (zone 29) # The zones_restrict format is a 52-character bitstring where bit N corresponds to zone N+1 # Zones are 1-indexed, so zone 29 needs the bit at position 28 (0-indexed from right) # Build string from right to left: 28 zeros, then "1", then 23 zeros zones_restrict = ("0" * 23 + "1" + "0" * 28).reverse.chars.reverse.join # Simpler approach: create a 52-char string with bit 28 set to "1" zones_restrict_array = Array.new(52, "0") zones_restrict_array[28] = "1" # Set bit for zone 29 zones_restrict = zones_restrict_array.join restricting_hat = create_item("Restricting Hat", zones(:hat1), zones_restrict: zones_restrict) # Create an item in the ruff zone ruff_item = create_item("Ruff Item", zones(:ruff)) appearances = Item.appearances_for([restricting_hat, ruff_item], @pet_type) hat_appearance = appearances[restricting_hat.id] ruff_appearance = appearances[ruff_item.id] expect(hat_appearance.compatible_with?(ruff_appearance)).to be false expect(ruff_appearance.compatible_with?(hat_appearance)).to be false end it "returns true for empty appearances" do # Create items that don't fit the current pet (wrong body_id) hat = create_item("Hat", zones(:hat1), body_id: 999) shirt = create_item("Shirt", zones(:shirtdress), body_id: 999) appearances = Item.appearances_for([hat, shirt], @pet_type) hat_appearance = appearances[hat.id] shirt_appearance = appearances[shirt.id] # Both should be empty (no swf_assets for this pet) expect(hat_appearance).to be_empty expect(shirt_appearance).to be_empty # Empty appearances should be compatible expect(hat_appearance.compatible_with?(shirt_appearance)).to be true end end describe "#without_item" do it "returns a new outfit without the given item" do hat = create_item("Hat", zones(:hat1)) outfit_with_hat = @outfit.with_item(hat) new_outfit = outfit_with_hat.without_item(hat) expect(new_outfit.worn_items).not_to include(hat) expect(outfit_with_hat.worn_items).to include(hat) # Original unchanged end it "returns a new outfit instance (immutable)" do hat = create_item("Hat", zones(:hat1)) outfit_with_hat = @outfit.with_item(hat) new_outfit = outfit_with_hat.without_item(hat) expect(new_outfit).not_to eq(outfit_with_hat) expect(new_outfit.object_id).not_to eq(outfit_with_hat.object_id) end it "does nothing if the item is not worn" do hat = create_item("Hat", zones(:hat1)) new_outfit = @outfit.without_item(hat) expect(new_outfit.worn_items).to be_empty end end describe "#with_item" do it "adds an item when there are no conflicts" do hat = create_item("Hat", zones(:hat1)) new_outfit = @outfit.with_item(hat) expect(new_outfit.worn_items).to include(hat) end it "returns a new outfit instance (immutable)" do hat = create_item("Hat", zones(:hat1)) new_outfit = @outfit.with_item(hat) expect(new_outfit).not_to eq(@outfit) expect(new_outfit.object_id).not_to eq(@outfit.object_id) expect(@outfit.worn_items).to be_empty # Original unchanged end it "is idempotent (adding same item twice has no effect)" do hat = create_item("Hat", zones(:hat1)) outfit1 = @outfit.with_item(hat) outfit2 = outfit1.with_item(hat) expect(outfit1.worn_items.size).to eq(1) expect(outfit2.worn_items.size).to eq(1) expect(outfit2.worn_items).to include(hat) end it "does not add items that don't fit this pet" do # Create item with wrong body_id hat = create_item("Hat", zones(:hat1), body_id: 999) new_outfit = @outfit.with_item(hat) expect(new_outfit.worn_items).to be_empty end context "with conflicting items" do it "moves conflicting item to closet when items occupy the same zone" do hat1 = create_item("Hat 1", zones(:hat1)) hat2 = create_item("Hat 2", zones(:hat1)) outfit_with_hat1 = @outfit.with_item(hat1) outfit_with_hat2 = outfit_with_hat1.with_item(hat2) expect(outfit_with_hat2.worn_items).to include(hat2) expect(outfit_with_hat2.worn_items).not_to include(hat1) expect(outfit_with_hat2.closeted_items).to include(hat1) end it "moves conflicting item to closet when new item restricts zone" do # Create item in ruff zone ruff_item = create_item("Ruff Item", zones(:ruff)) # Create hat that restricts ruff zone (zone 29) # zones_restrict is 0-indexed, so zone 29 needs bit 28 to be "1" zones_restrict_array = Array.new(52, "0") zones_restrict_array[28] = "1" zones_restrict = zones_restrict_array.join restricting_hat = create_item("Restricting Hat", zones(:hat1), zones_restrict: zones_restrict) # First wear ruff item, then wear restricting hat outfit_with_ruff = @outfit.with_item(ruff_item) outfit_with_hat = outfit_with_ruff.with_item(restricting_hat) expect(outfit_with_hat.worn_items).to include(restricting_hat) expect(outfit_with_hat.worn_items).not_to include(ruff_item) expect(outfit_with_hat.closeted_items).to include(ruff_item) end it "keeps compatible items when adding new item" do hat = create_item("Hat", zones(:hat1)) shirt = create_item("Shirt", zones(:shirtdress)) pants = create_item("Pants", zones(:trousers)) outfit1 = @outfit.with_item(hat).with_item(shirt) outfit2 = outfit1.with_item(pants) expect(outfit2.worn_items).to include(hat, shirt, pants) expect(outfit2.closeted_items).to be_empty end it "can move multiple conflicting items to closet" do hat1 = create_item("Hat 1", zones(:hat1)) hat2 = create_item("Hat 2", zones(:hat1)) hat3 = create_item("Hat 3", zones(:hat1)) # Wear hat1 and hat2 by manually building the outfit # (normally you can't, but we're testing the conflict resolution) outfit = @outfit.dup outfit.worn_items << hat1 outfit.worn_items << hat2 # Now add hat3, which should move both hat1 and hat2 to closet outfit_with_hat3 = outfit.with_item(hat3) expect(outfit_with_hat3.worn_items).to contain_exactly(hat3) expect(outfit_with_hat3.closeted_items).to contain_exactly(hat1, hat2) end it "does not duplicate items in closet if already closeted" do hat1 = create_item("Hat 1", zones(:hat1)) hat2 = create_item("Hat 2", zones(:hat1)) # Wear hat1 outfit1 = @outfit.with_item(hat1) # Add hat2 (moves hat1 to closet) outfit2 = outfit1.with_item(hat2) # Add hat2 again (should be idempotent, not duplicate hat1 in closet) outfit3 = outfit2.with_item(hat2) expect(outfit3.closeted_items.size).to eq(1) expect(outfit3.closeted_items).to include(hat1) end end context "edge cases" do it "handles nil item gracefully" do expect { @outfit.with_item(nil) }.not_to raise_error end it "works with outfit that has no pet_state" do # This shouldn't happen in practice, but let's be defensive outfit_no_pet = Outfit.new hat = create_item("Hat", zones(:hat1)) # Should not crash, but also won't add the item expect { outfit_no_pet.with_item(hat) }.not_to raise_error end end end # Helper to create a pet state with specific swf_assets def build_pet_state(pet_type, pose: "HAPPY_FEM", swf_assets: []) pet_state = PetState.create!( pet_type: pet_type, pose: pose, swf_asset_ids: swf_assets.empty? ? [0] : swf_assets.map(&:id) ) pet_state.swf_assets = swf_assets pet_state end # Helper to create a SwfAsset for biology (pet layers) def build_biology_asset(zone, body_id:, zones_restrict: "") @remote_id = (@remote_id || 0) + 1 SwfAsset.create!( type: "biology", remote_id: @remote_id, url: "https://images.neopets.example/biology_#{@remote_id}.swf", zone: zone, body_id: body_id, zones_restrict: zones_restrict ) end # Helper to create a SwfAsset for items (object layers) def build_item_asset(zone, body_id:, zones_restrict: "") @remote_id = (@remote_id || 0) + 1 SwfAsset.create!( type: "object", remote_id: @remote_id, url: "https://images.neopets.example/object_#{@remote_id}.swf", zone: zone, body_id: body_id, zones_restrict: zones_restrict ) end # Helper to create an item with specific swf_assets def build_item(name, swf_assets: []) item = Item.create!( name: name, description: "Test item", thumbnail_url: "https://images.neopets.example/thumbnail.png", rarity: "Common", price: 100, zones_restrict: "", species_support_ids: "" ) swf_assets.each do |asset| ParentSwfAssetRelationship.create!( parent: item, swf_asset: asset ) end item end describe "#visible_layers" do before do # Clean up any existing pet types to avoid conflicts PetType.destroy_all # Create a basic pet type for testing @pet_type = PetType.create!( species: species(:acara), color: colors(:blue), body_id: 1, created_at: Time.new(2005) ) end context "basic layer composition" do it "returns pet layers when no items are worn" do # Create biology assets for the pet head = build_biology_asset(zones(:head), body_id: 1) body = build_biology_asset(zones(:body), body_id: 1) pet_state = build_pet_state(@pet_type, swf_assets: [head, body]) outfit = Outfit.new(pet_state: pet_state) layers = outfit.visible_layers expect(layers).to contain_exactly(head, body) end it "returns pet layers and item layers when items are worn" do # Create pet layers head = build_biology_asset(zones(:head), body_id: 1) body = build_biology_asset(zones(:body), body_id: 1) pet_state = build_pet_state(@pet_type, swf_assets: [head, body]) # Create item layers hat_asset = build_item_asset(zones(:hat1), body_id: 1) hat = build_item("Test Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [hat] layers = outfit.visible_layers expect(layers).to contain_exactly(head, body, hat_asset) end it "includes body_id=0 items that fit all pets" do # Create pet layers head = build_biology_asset(zones(:head), body_id: 1) pet_state = build_pet_state(@pet_type, swf_assets: [head]) # Create a background item (body_id=0, fits all) bg_asset = build_item_asset(zones(:background), body_id: 0) background = build_item("Test Background", swf_assets: [bg_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [background] layers = outfit.visible_layers expect(layers).to contain_exactly(head, bg_asset) end end context "items restricting pet layers (Rule 3a)" do it "hides pet layers in zones that items restrict" do # Create pet layers including hair head = build_biology_asset(zones(:head), body_id: 1) hair = build_biology_asset(zones(:hairfront), body_id: 1) pet_state = build_pet_state(@pet_type, swf_assets: [head, hair]) # Create a hat that restricts the hair zone # zones_restrict is a bitfield where position 37 (Hair Front zone id) is "1" zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1 hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict) hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [hat] layers = outfit.visible_layers # Hair should be hidden, but head and hat should be visible expect(layers).to contain_exactly(head, hat_asset) expect(layers).not_to include(hair) end it "hides multiple pet layers when item restricts multiple zones" do # Create pet layers head = build_biology_asset(zones(:head), body_id: 1) hair_front = build_biology_asset(zones(:hairfront), body_id: 1) head_transient = build_biology_asset(zones(:headtransientbiology), body_id: 1) pet_state = build_pet_state(@pet_type, swf_assets: [head, hair_front, head_transient]) # Create an item that restricts both Hair Front (37) and Head Transient Biology (38) zones_restrict = "0" * 36 + "11" + "0" * 20 # bits 37 and 38 = 1 hood_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict) hood = build_item("Agent Hood", swf_assets: [hood_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [hood] layers = outfit.visible_layers # Both hair_front and head_transient should be hidden expect(layers).to contain_exactly(head, hood_asset) expect(layers).not_to include(hair_front, head_transient) end end context "pets restricting body-specific item layers (Rule 3b)" do it "hides body-specific items in zones the pet restricts" do # Create a pet with a layer that restricts the Static zone (46) head = build_biology_asset(zones(:head), body_id: 1) zones_restrict = "0" * 45 + "1" + "0" * 10 # bit 46 = 1 restricting_layer = build_biology_asset(zones(:body), body_id: 1, zones_restrict: zones_restrict) pet_state = build_pet_state(@pet_type, swf_assets: [head, restricting_layer]) # Create a body-specific Static item static_asset = build_item_asset(zones(:static), body_id: 1) static_item = build_item("Body-specific Static", swf_assets: [static_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [static_item] layers = outfit.visible_layers # The body-specific static item should be hidden expect(layers).to contain_exactly(head, restricting_layer) expect(layers).not_to include(static_asset) end it "allows body_id=0 items even in zones the pet restricts" do # Create a pet with a layer that restricts the Background Item zone (48) # Background Item is type_id 3 (universal zone), so body_id=0 items should always work head = build_biology_asset(zones(:head), body_id: 1) zones_restrict = "0" * 47 + "1" + "0" * 10 # bit 48 = 1 restricting_layer = build_biology_asset(zones(:body), body_id: 1, zones_restrict: zones_restrict) pet_state = build_pet_state(@pet_type, swf_assets: [head, restricting_layer]) # Create a body_id=0 Background Item (fits all bodies, universal zone) bg_item_asset = build_item_asset(zones(:backgrounditem), body_id: 0) bg_item = build_item("Universal Background Item", swf_assets: [bg_item_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [bg_item] layers = outfit.visible_layers # The body_id=0 item should be visible even though the zone is restricted expect(layers).to contain_exactly(head, restricting_layer, bg_item_asset) end end context "UNCONVERTED pets (Rule 3b special case)" do it "rejects all body-specific items" do # Create an UNCONVERTED pet head = build_biology_asset(zones(:head), body_id: 1) body = build_biology_asset(zones(:body), body_id: 1) pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head, body]) # Create both body-specific and body_id=0 items body_specific_asset = build_item_asset(zones(:hat1), body_id: 1) body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset]) universal_asset = build_item_asset(zones(:background), body_id: 0) universal_item = build_item("Universal Background", swf_assets: [universal_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [body_specific_item, universal_item] layers = outfit.visible_layers # Only body_id=0 items should be visible expect(layers).to contain_exactly(head, body, universal_asset) expect(layers).not_to include(body_specific_asset) end it "rejects body-specific items regardless of zone restrictions" do # Create an UNCONVERTED pet with no zone restrictions head = build_biology_asset(zones(:head), body_id: 1) pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head]) # Create a body-specific item in a zone the pet doesn't restrict hat_asset = build_item_asset(zones(:hat1), body_id: 1) hat = build_item("Body-specific Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [hat] layers = outfit.visible_layers # The body-specific item should still be hidden expect(layers).to contain_exactly(head) expect(layers).not_to include(hat_asset) end end context "pets restricting their own layers (Rule 3c)" do it "hides pet layers in zones the pet itself restricts" do # Create a pet with a horn asset and a layer that restricts the horn's zone # (Simulating the Wraith Uni case) body = build_biology_asset(zones(:body), body_id: 1) # Create a horn in the Head Transient Biology zone (38) horn = build_biology_asset(zones(:headtransientbiology), body_id: 1) # Create a layer that restricts zone 38 zones_restrict = "0" * 37 + "1" + "0" * 20 # bit 38 = 1 restricting_layer = build_biology_asset(zones(:head), body_id: 1, zones_restrict: zones_restrict) pet_state = build_pet_state(@pet_type, swf_assets: [body, horn, restricting_layer]) outfit = Outfit.new(pet_state: pet_state) layers = outfit.visible_layers # The horn should be hidden by the pet's own restrictions expect(layers).to contain_exactly(body, restricting_layer) expect(layers).not_to include(horn) end it "applies self-restrictions in combination with item restrictions" do # Create a pet with multiple layers, some restricted by itself body = build_biology_asset(zones(:body), body_id: 1) hair = build_biology_asset(zones(:hairfront), body_id: 1) # Pet restricts its own Head zone (30) zones_restrict = "0" * 29 + "1" + "0" * 25 # bit 30 = 1 head = build_biology_asset(zones(:head), body_id: 1) restricting_layer = build_biology_asset(zones(:eyes), body_id: 1, zones_restrict: zones_restrict) pet_state = build_pet_state(@pet_type, swf_assets: [body, hair, head, restricting_layer]) # Add an item that restricts Hair Front (37) item_zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1 hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: item_zones_restrict) hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [hat] layers = outfit.visible_layers # Hair should be hidden by item, Head should be hidden by pet's own restrictions expect(layers).to contain_exactly(body, restricting_layer, hat_asset) expect(layers).not_to include(hair, head) end end context "depth sorting and layer ordering" do it "sorts layers by zone depth" do # Create layers in various zones with different depths background = build_biology_asset(zones(:background), body_id: 1) # depth 3 body_layer = build_biology_asset(zones(:body), body_id: 1) # depth 18 head_layer = build_biology_asset(zones(:head), body_id: 1) # depth 34 pet_state = build_pet_state(@pet_type, swf_assets: [head_layer, background, body_layer]) outfit = Outfit.new(pet_state: pet_state) layers = outfit.visible_layers # Should be sorted by depth: background (3) < body (18) < head (34) expect(layers[0]).to eq(background) expect(layers[1]).to eq(body_layer) expect(layers[2]).to eq(head_layer) end it "places item layers after pet layers at the same depth" do # Create a pet layer and item layer in zones with the same depth # Static zone has depth 48 pet_static = build_biology_asset(zones(:static), body_id: 1) pet_state = build_pet_state(@pet_type, swf_assets: [pet_static]) item_static = build_item_asset(zones(:static), body_id: 0) static_item = build_item("Static Item", swf_assets: [item_static]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [static_item] layers = outfit.visible_layers # Both should be present, with item layer last (on top) expect(layers).to eq([pet_static, item_static]) end it "sorts complex outfits correctly by depth" do # Create a complex outfit with multiple pet and item layers background = build_biology_asset(zones(:background), body_id: 1) # depth 3 body_layer = build_biology_asset(zones(:body), body_id: 1) # depth 18 head_layer = build_biology_asset(zones(:head), body_id: 1) # depth 34 pet_state = build_pet_state(@pet_type, swf_assets: [head_layer, background, body_layer]) # Add items at various depths bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4 hat_asset = build_item_asset(zones(:hat1), body_id: 1) # depth 44 shirt_asset = build_item_asset(zones(:shirtdress), body_id: 1) # depth 26 bg = build_item("Background Item", swf_assets: [bg_item]) hat = build_item("Hat", swf_assets: [hat_asset]) shirt = build_item("Shirt", swf_assets: [shirt_asset]) outfit = Outfit.new(pet_state: pet_state) outfit.worn_items = [hat, bg, shirt] layers = outfit.visible_layers # Expected order by depth: # background (3), bg_item (4), body_layer (18), shirt_asset (26), # head_layer (34), hat_asset (44) expect(layers.map(&:depth)).to eq([3, 4, 18, 26, 34, 44]) expect(layers).to eq([background, bg_item, body_layer, shirt_asset, head_layer, hat_asset]) end end context "alt styles (alternative pet appearances)" do before do # Create an alt style with its own body_id distinct from regular pets @alt_style = AltStyle.create!( species: species(:acara), color: colors(:blue), body_id: 999, # Distinct from the regular pet's body_id (1) series_name: "Nostalgic", thumbnail_url: "https://images.neopets.example/alt_style.png" ) end it "uses alt style layers instead of pet state layers" do # Create regular pet layers regular_head = build_biology_asset(zones(:head), body_id: 1) regular_body = build_biology_asset(zones(:body), body_id: 1) pet_state = build_pet_state(@pet_type, swf_assets: [regular_head, regular_body]) # Create alt style layers (with the alt style's body_id) alt_head = build_biology_asset(zones(:head), body_id: 999) alt_body = build_biology_asset(zones(:body), body_id: 999) @alt_style.swf_assets = [alt_head, alt_body] # Create outfit with alt_style outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) layers = outfit.visible_layers # Should use alt style layers, not pet state layers expect(layers).to contain_exactly(alt_head, alt_body) expect(layers).not_to include(regular_head, regular_body) end it "only includes body_id=0 items with alt styles" do # Create alt style layers alt_head = build_biology_asset(zones(:head), body_id: 999) @alt_style.swf_assets = [alt_head] pet_state = build_pet_state(@pet_type) # Create a body-specific item for the alt style's body_id body_specific_asset = build_item_asset(zones(:hat1), body_id: 999) body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset]) # Create a universal item (body_id=0) universal_asset = build_item_asset(zones(:background), body_id: 0) universal_item = build_item("Universal Background", swf_assets: [universal_asset]) outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) outfit.worn_items = [body_specific_item, universal_item] layers = outfit.visible_layers # Only the universal item should appear expect(layers).to contain_exactly(alt_head, universal_asset) expect(layers).not_to include(body_specific_asset) end it "does not include items from the regular pet's body_id" do # Create alt style layers alt_body = build_biology_asset(zones(:body), body_id: 999) @alt_style.swf_assets = [alt_body] pet_state = build_pet_state(@pet_type) # Create an item that fits the regular pet's body_id (1) regular_item_asset = build_item_asset(zones(:hat1), body_id: 1) regular_item = build_item("Regular Pet Hat", swf_assets: [regular_item_asset]) outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) outfit.worn_items = [regular_item] layers = outfit.visible_layers # The regular pet item should not appear on the alt style expect(layers).to contain_exactly(alt_body) expect(layers).not_to include(regular_item_asset) end it "applies item restriction rules with alt styles" do # Create alt style layers including hair alt_head = build_biology_asset(zones(:head), body_id: 999) alt_hair = build_biology_asset(zones(:hairfront), body_id: 999) @alt_style.swf_assets = [alt_head, alt_hair] pet_state = build_pet_state(@pet_type) # Create a universal hat that restricts the hair zone zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 (Hair Front) = 1 hat_asset = build_item_asset(zones(:hat1), body_id: 0, zones_restrict: zones_restrict) hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) outfit.worn_items = [hat] layers = outfit.visible_layers # Hair should be hidden by the hat's zone restrictions expect(layers).to contain_exactly(alt_head, hat_asset) expect(layers).not_to include(alt_hair) end it "applies pet restriction rules with alt styles" do # Create alt style with a layer that restricts a zone alt_head = build_biology_asset(zones(:head), body_id: 999) zones_restrict = "0" * 47 + "1" + "0" * 10 # bit 48 (Background Item) = 1 restricting_layer = build_biology_asset(zones(:body), body_id: 999, zones_restrict: zones_restrict) @alt_style.swf_assets = [alt_head, restricting_layer] pet_state = build_pet_state(@pet_type) # Create a universal Background Item bg_item_asset = build_item_asset(zones(:backgrounditem), body_id: 0) bg_item = build_item("Universal Background Item", swf_assets: [bg_item_asset]) outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) outfit.worn_items = [bg_item] layers = outfit.visible_layers # body_id=0 items should still appear even in restricted zones # (because they're not body-specific) expect(layers).to contain_exactly(alt_head, restricting_layer, bg_item_asset) end it "applies self-restriction rules with alt styles" do # Create alt style that restricts its own horn layer alt_body = build_biology_asset(zones(:body), body_id: 999) alt_horn = build_biology_asset(zones(:headtransientbiology), body_id: 999) zones_restrict = "0" * 37 + "1" + "0" * 20 # bit 38 (Head Transient Biology) = 1 restricting_layer = build_biology_asset(zones(:head), body_id: 999, zones_restrict: zones_restrict) @alt_style.swf_assets = [alt_body, alt_horn, restricting_layer] pet_state = build_pet_state(@pet_type) outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) layers = outfit.visible_layers # The horn should be hidden by the alt style's own restrictions expect(layers).to contain_exactly(alt_body, restricting_layer) expect(layers).not_to include(alt_horn) end it "sorts alt style and item layers by depth correctly" do # Create alt style layers at various depths alt_background = build_biology_asset(zones(:background), body_id: 999) # depth 3 alt_body = build_biology_asset(zones(:body), body_id: 999) # depth 18 alt_head = build_biology_asset(zones(:head), body_id: 999) # depth 34 @alt_style.swf_assets = [alt_head, alt_background, alt_body] pet_state = build_pet_state(@pet_type) # Add universal items at various depths bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4 trinket = build_item_asset(zones(:righthanditem1), body_id: 0) # depth 46 bg = build_item("Background Item", swf_assets: [bg_item]) trinket_item = build_item("Trinket", swf_assets: [trinket]) outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) outfit.worn_items = [trinket_item, bg] layers = outfit.visible_layers # Expected order by depth: # alt_background (3), bg_item (4), alt_body (18), alt_head (34), trinket (46) expect(layers.map(&:depth)).to eq([3, 4, 18, 34, 46]) expect(layers).to eq([alt_background, bg_item, alt_body, alt_head, trinket]) end end end end