forked from OpenNeo/impress
Add alt style support to Outfit#visible_layers
This commit is contained in:
parent
cf80f96410
commit
f823bac717
2 changed files with 207 additions and 24 deletions
|
|
@ -170,52 +170,67 @@ class Outfit < ApplicationRecord
|
|||
end
|
||||
|
||||
def visible_layers
|
||||
# TODO: This method doesn't currently handle alt styles! If the outfit has
|
||||
# an alt_style, we should use its layers instead of pet_state layers, and
|
||||
# filter items to only those with body_id=0. This isn't needed yet because
|
||||
# this method is only used on item pages, which don't support alt styles.
|
||||
# See useOutfitAppearance.js for the complete logic including alt styles.
|
||||
item_appearances = item_appearances(swf_asset_includes: [:zone])
|
||||
# Step 1: Choose biology layers - use alt style if present, otherwise pet state
|
||||
if alt_style
|
||||
biology_layers = alt_style.swf_assets.includes(:zone).to_a
|
||||
body = alt_style
|
||||
using_alt_style = true
|
||||
else
|
||||
biology_layers = pet_state.swf_assets.includes(:zone).to_a
|
||||
body = pet_type
|
||||
using_alt_style = false
|
||||
end
|
||||
|
||||
pet_layers = pet_state.swf_assets.includes(:zone).to_a
|
||||
# Step 2: Load item appearances for the appropriate body
|
||||
item_appearances = Item.appearances_for(
|
||||
worn_items,
|
||||
body,
|
||||
swf_asset_includes: [:zone]
|
||||
).values
|
||||
item_layers = item_appearances.map(&:swf_assets).flatten
|
||||
|
||||
pet_restricted_zone_ids = pet_layers.map(&:restricted_zone_ids).
|
||||
# For alt styles, only body_id=0 items are compatible
|
||||
if using_alt_style
|
||||
item_layers.reject! { |sa| sa.body_id != 0 }
|
||||
end
|
||||
|
||||
# Step 3: Apply restriction rules
|
||||
biology_restricted_zone_ids = biology_layers.map(&:restricted_zone_ids).
|
||||
flatten.to_set
|
||||
item_restricted_zone_ids = item_appearances.
|
||||
map(&:restricted_zone_ids).flatten.to_set
|
||||
|
||||
# When an item restricts a zone, it hides pet layers of the same zone.
|
||||
# Rule 3a: When an item restricts a zone, it hides biology layers of the same zone.
|
||||
# We use this to e.g. make a hat hide a hair ruff.
|
||||
#
|
||||
# NOTE: Items' restricted layers also affect what items you can wear at
|
||||
# the same time. We don't enforce anything about that here, and
|
||||
# instead assume that the input by this point is valid!
|
||||
pet_layers.reject! { |sa| item_restricted_zone_ids.include?(sa.zone_id) }
|
||||
biology_layers.reject! { |sa| item_restricted_zone_ids.include?(sa.zone_id) }
|
||||
|
||||
# When a pet appearance restricts a zone, or when the pet is Unconverted,
|
||||
# it makes body-specific items incompatible. We use this to disallow UCs
|
||||
# from wearing certain body-specific Biology Effects, Statics, etc, while
|
||||
# still allowing non-body-specific items in those zones! (I think this
|
||||
# happens for some Invisible pet stuff, too?)
|
||||
# Rule 3b: When a biology appearance restricts a zone, or when the pet is
|
||||
# Unconverted, it makes body-specific items incompatible. We use this to
|
||||
# disallow UCs from wearing certain body-specific Biology Effects, Statics,
|
||||
# etc, while still allowing non-body-specific items in those zones! (I think
|
||||
# this happens for some Invisible pet stuff, too?)
|
||||
#
|
||||
# TODO: We shouldn't be *hiding* these zones, like we do with items; we
|
||||
# should be doing this way earlier, to prevent the item from even
|
||||
# showing up even in search results!
|
||||
#
|
||||
# NOTE: This can result in both pet layers and items occupying the same
|
||||
# NOTE: This can result in both biology layers and items occupying the same
|
||||
# zone, like Static, so long as the item isn't body-specific! That's
|
||||
# correct, and the item layer should be on top! (Here, we implement
|
||||
# it by placing item layers second in the list, and rely on JS sort
|
||||
# stability, and *then* rely on the UI to respect that ordering when
|
||||
# rendering them by depth. Not great! 😅)
|
||||
#
|
||||
# NOTE: We used to also include the pet appearance's *occupied* zones in
|
||||
# NOTE: We used to also include the biology appearance's *occupied* zones in
|
||||
# this condition, not just the restricted zones, as a sensible
|
||||
# defensive default, even though we weren't aware of any relevant
|
||||
# items. But now we know that actually the "Bruce Brucey B Mouth"
|
||||
# occupies the real Mouth zone, and still should be visible and
|
||||
# above pet layers! So, we now only check *restricted* zones.
|
||||
# above biology layers! So, we now only check *restricted* zones.
|
||||
#
|
||||
# NOTE: UCs used to implement their restrictions by listing specific
|
||||
# zones, but it seems that the logic has changed to just be about
|
||||
|
|
@ -232,14 +247,16 @@ class Outfit < ApplicationRecord
|
|||
item_layers.reject! { |sa| sa.body_specific? }
|
||||
else
|
||||
item_layers.reject! { |sa| sa.body_specific? &&
|
||||
pet_restricted_zone_ids.include?(sa.zone_id) }
|
||||
biology_restricted_zone_ids.include?(sa.zone_id) }
|
||||
end
|
||||
|
||||
# A pet appearance can also restrict its own zones. The Wraith Uni is an
|
||||
# interesting example: it has a horn, but its zone restrictions hide it!
|
||||
pet_layers.reject! { |sa| pet_restricted_zone_ids.include?(sa.zone_id) }
|
||||
# Rule 3c: A biology appearance can also restrict its own zones. The Wraith
|
||||
# Uni is an interesting example: it has a horn, but its zone restrictions
|
||||
# hide it!
|
||||
biology_layers.reject! { |sa| biology_restricted_zone_ids.include?(sa.zone_id) }
|
||||
|
||||
(pet_layers + item_layers).sort_by(&:depth)
|
||||
# Step 4: Sort by depth and return
|
||||
(biology_layers + item_layers).sort_by(&:depth)
|
||||
end
|
||||
|
||||
def wardrobe_params
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ RSpec.describe Outfit do
|
|||
pet_state = PetState.create!(
|
||||
pet_type: pet_type,
|
||||
pose: pose,
|
||||
swf_asset_ids: swf_assets.map(&:id)
|
||||
swf_asset_ids: swf_assets.empty? ? [0] : swf_assets.map(&:id)
|
||||
)
|
||||
pet_state.swf_assets = swf_assets
|
||||
pet_state
|
||||
|
|
@ -376,5 +376,171 @@ RSpec.describe Outfit do
|
|||
expect(layers).to eq([background, bg_item, hat_asset, body_layer, shirt_asset, head_layer])
|
||||
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(:hat), 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(:hat), 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(:hat), 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(:righthanditem), body_id: 0) # depth 5
|
||||
|
||||
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), trinket (5), alt_body (18), alt_head (34)
|
||||
expect(layers.map(&:depth)).to eq([3, 4, 5, 18, 34])
|
||||
expect(layers).to eq([alt_background, bg_item, trinket, alt_body, alt_head])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue