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
|
end
|
||||||
|
|
||||||
def visible_layers
|
def visible_layers
|
||||||
# TODO: This method doesn't currently handle alt styles! If the outfit has
|
# Step 1: Choose biology layers - use alt style if present, otherwise pet state
|
||||||
# an alt_style, we should use its layers instead of pet_state layers, and
|
if alt_style
|
||||||
# filter items to only those with body_id=0. This isn't needed yet because
|
biology_layers = alt_style.swf_assets.includes(:zone).to_a
|
||||||
# this method is only used on item pages, which don't support alt styles.
|
body = alt_style
|
||||||
# See useOutfitAppearance.js for the complete logic including alt styles.
|
using_alt_style = true
|
||||||
item_appearances = item_appearances(swf_asset_includes: [:zone])
|
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
|
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
|
flatten.to_set
|
||||||
item_restricted_zone_ids = item_appearances.
|
item_restricted_zone_ids = item_appearances.
|
||||||
map(&:restricted_zone_ids).flatten.to_set
|
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.
|
# 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
|
# NOTE: Items' restricted layers also affect what items you can wear at
|
||||||
# the same time. We don't enforce anything about that here, and
|
# the same time. We don't enforce anything about that here, and
|
||||||
# instead assume that the input by this point is valid!
|
# 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,
|
# Rule 3b: When a biology appearance restricts a zone, or when the pet is
|
||||||
# it makes body-specific items incompatible. We use this to disallow UCs
|
# Unconverted, it makes body-specific items incompatible. We use this to
|
||||||
# from wearing certain body-specific Biology Effects, Statics, etc, while
|
# disallow UCs from wearing certain body-specific Biology Effects, Statics,
|
||||||
# still allowing non-body-specific items in those zones! (I think this
|
# etc, while still allowing non-body-specific items in those zones! (I think
|
||||||
# happens for some Invisible pet stuff, too?)
|
# this happens for some Invisible pet stuff, too?)
|
||||||
#
|
#
|
||||||
# TODO: We shouldn't be *hiding* these zones, like we do with items; we
|
# 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
|
# should be doing this way earlier, to prevent the item from even
|
||||||
# showing up even in search results!
|
# 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
|
# 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
|
# 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
|
# 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
|
# stability, and *then* rely on the UI to respect that ordering when
|
||||||
# rendering them by depth. Not great! 😅)
|
# 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
|
# this condition, not just the restricted zones, as a sensible
|
||||||
# defensive default, even though we weren't aware of any relevant
|
# defensive default, even though we weren't aware of any relevant
|
||||||
# items. But now we know that actually the "Bruce Brucey B Mouth"
|
# items. But now we know that actually the "Bruce Brucey B Mouth"
|
||||||
# occupies the real Mouth zone, and still should be visible and
|
# 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
|
# NOTE: UCs used to implement their restrictions by listing specific
|
||||||
# zones, but it seems that the logic has changed to just be about
|
# 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? }
|
item_layers.reject! { |sa| sa.body_specific? }
|
||||||
else
|
else
|
||||||
item_layers.reject! { |sa| sa.body_specific? &&
|
item_layers.reject! { |sa| sa.body_specific? &&
|
||||||
pet_restricted_zone_ids.include?(sa.zone_id) }
|
biology_restricted_zone_ids.include?(sa.zone_id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# A pet appearance can also restrict its own zones. The Wraith Uni is an
|
# Rule 3c: A biology appearance can also restrict its own zones. The Wraith
|
||||||
# interesting example: it has a horn, but its zone restrictions hide it!
|
# Uni is an interesting example: it has a horn, but its zone restrictions
|
||||||
pet_layers.reject! { |sa| pet_restricted_zone_ids.include?(sa.zone_id) }
|
# 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
|
end
|
||||||
|
|
||||||
def wardrobe_params
|
def wardrobe_params
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ RSpec.describe Outfit do
|
||||||
pet_state = PetState.create!(
|
pet_state = PetState.create!(
|
||||||
pet_type: pet_type,
|
pet_type: pet_type,
|
||||||
pose: pose,
|
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.swf_assets = swf_assets
|
||||||
pet_state
|
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])
|
expect(layers).to eq([background, bg_item, hat_asset, body_layer, shirt_asset, head_layer])
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue