1
0
Fork 0
forked from OpenNeo/impress
impress/spec/models/outfit_spec.rb

546 lines
21 KiB
Ruby

require_relative '../rails_helper'
RSpec.describe Outfit do
fixtures :zones, :colors, :species
# 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