[WV2] Outfit saving first draft
This commit is contained in:
parent
5e68d3809c
commit
0d4b553162
16 changed files with 337 additions and 28 deletions
|
|
@ -1,6 +1,36 @@
|
|||
// Wardrobe v2 - Simple Rails+Turbo outfit editor
|
||||
//
|
||||
// This page uses Turbo Frames for instant updates when changing species/color.
|
||||
// This page uses Turbo for instant updates when changing species/color.
|
||||
// The outfit_viewer Web Component handles the pet rendering.
|
||||
|
||||
console.log("Wardrobe v2 loaded!");
|
||||
// Unsaved changes warning: use a MutationObserver to watch the
|
||||
// data-has-unsaved-changes attribute on the wardrobe container. This is more
|
||||
// robust than event listeners because it works regardless of how the DOM is
|
||||
// updated (Turbo morph, direct manipulation, etc.).
|
||||
function setupUnsavedChangesObserver() {
|
||||
const container = document.querySelector("[data-has-unsaved-changes]");
|
||||
if (!container) return;
|
||||
|
||||
function update() {
|
||||
if (container.dataset.hasUnsavedChanges === "true") {
|
||||
window.onbeforeunload = (e) => {
|
||||
e.preventDefault();
|
||||
return "";
|
||||
};
|
||||
} else {
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial state
|
||||
update();
|
||||
|
||||
// Watch for attribute changes
|
||||
const observer = new MutationObserver(update);
|
||||
observer.observe(container, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-has-unsaved-changes"],
|
||||
});
|
||||
}
|
||||
|
||||
setupUnsavedChangesObserver();
|
||||
|
|
|
|||
|
|
@ -751,12 +751,6 @@ pose-picker-popover {
|
|||
overflow-y: auto;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.75rem;
|
||||
color: #448844;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
color: #448844;
|
||||
|
|
@ -845,6 +839,115 @@ pose-picker-popover {
|
|||
}
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Flash Messages
|
||||
=================================================================== */
|
||||
|
||||
.flash-messages {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flash-alert {
|
||||
background: #f8d7da;
|
||||
color: #842029;
|
||||
border: 1px solid #f5c2c7;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem 1rem;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Outfit Header (name + save button)
|
||||
=================================================================== */
|
||||
|
||||
.outfit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.outfit-name-form {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.outfit-name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #448844;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: transparent;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #ddd;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #448844;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #aaa;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* Progressive enhancement: hide rename submit when JS is available */
|
||||
.outfit-name-submit {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@media (scripting: enabled) {
|
||||
.outfit-name-submit {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.outfit-save-form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.outfit-save-button {
|
||||
white-space: nowrap;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
color: #888;
|
||||
border-color: #ddd;
|
||||
background: #f5f5f5;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.outfit-save-button {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Animations
|
||||
=================================================================== */
|
||||
|
|
|
|||
|
|
@ -6,9 +6,18 @@ class OutfitsController < ApplicationController
|
|||
@outfit.user = current_user
|
||||
|
||||
if @outfit.save
|
||||
render :json => @outfit
|
||||
respond_to do |format|
|
||||
format.html { redirect_to wardrobe_v2_outfit_path(@outfit) }
|
||||
format.json { render json: @outfit }
|
||||
end
|
||||
else
|
||||
render_outfit_errors
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_back fallback_location: wardrobe_v2_path,
|
||||
alert: @outfit.errors.full_messages.join(", ")
|
||||
end
|
||||
format.json { render_outfit_errors }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -123,9 +132,18 @@ class OutfitsController < ApplicationController
|
|||
|
||||
def update
|
||||
if @outfit.update(outfit_params)
|
||||
render :json => @outfit
|
||||
respond_to do |format|
|
||||
format.html { redirect_to wardrobe_v2_outfit_path(@outfit) }
|
||||
format.json { render json: @outfit }
|
||||
end
|
||||
else
|
||||
render_outfit_errors
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_back fallback_location: wardrobe_v2_outfit_path(@outfit),
|
||||
alert: @outfit.errors.full_messages.join(", ")
|
||||
end
|
||||
format.json { render_outfit_errors }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
class WardrobeController < ApplicationController
|
||||
def show
|
||||
# Load saved outfit if an ID is provided (e.g. /outfits/:id/v2)
|
||||
@saved_outfit = Outfit.find(params[:id]) if params[:id].present?
|
||||
|
||||
# If visiting a saved outfit with no state params, redirect with the
|
||||
# outfit's state as query params. This keeps URL-as-source-of-truth simple:
|
||||
# the rest of the action always reads from params.
|
||||
if @saved_outfit && !outfit_state_params_present?
|
||||
redirect_to wardrobe_v2_outfit_path(@saved_outfit, **@saved_outfit.wardrobe_params)
|
||||
return
|
||||
end
|
||||
|
||||
# Set the form target path for all wardrobe forms
|
||||
@wardrobe_path = @saved_outfit ? wardrobe_v2_outfit_path(@saved_outfit) : wardrobe_v2_path
|
||||
|
||||
# Get selected species and color from params, or default to Blue Acara
|
||||
@selected_species = params[:species] ? Species.find_by_id(params[:species]) : Species.find_by_name("Acara")
|
||||
@selected_color = params[:color] ? Color.find_by_id(params[:color]) : Color.find_by_name("Blue")
|
||||
|
|
@ -56,6 +70,7 @@ class WardrobeController < ApplicationController
|
|||
|
||||
# Build the outfit
|
||||
@outfit = Outfit.new(
|
||||
name: params[:name].presence || @saved_outfit&.name || "Untitled outfit",
|
||||
pet_state: @pet_state,
|
||||
alt_style: @alt_style,
|
||||
worn_items: items,
|
||||
|
|
@ -68,6 +83,14 @@ class WardrobeController < ApplicationController
|
|||
# Also preload alt style layer manifests for the style picker thumbnails
|
||||
SwfAsset.preload_manifests(@alt_style.swf_assets.to_a) if @alt_style
|
||||
|
||||
# Compute saved outfit state for the view
|
||||
if @saved_outfit
|
||||
@has_unsaved_changes = !@outfit.same_wardrobe_state_as?(@saved_outfit)
|
||||
@is_owner = user_signed_in? && current_user.id == @saved_outfit.user_id
|
||||
else
|
||||
@has_unsaved_changes = false
|
||||
end
|
||||
|
||||
# Handle search mode
|
||||
@search_mode = params[:q].present?
|
||||
if @search_mode
|
||||
|
|
@ -108,6 +131,10 @@ class WardrobeController < ApplicationController
|
|||
poses_hash
|
||||
end
|
||||
|
||||
def outfit_state_params_present?
|
||||
params[:species].present? || params[:color].present? || params[:objects].present?
|
||||
end
|
||||
|
||||
def build_search_filters(query_params, outfit)
|
||||
filters = []
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ module WardrobeHelper
|
|||
def outfit_state_params(outfit = @outfit, except: [])
|
||||
fields = []
|
||||
|
||||
fields << hidden_field_tag(:name, @outfit.name) if @outfit.name.present? && !except.include?(:name)
|
||||
fields << hidden_field_tag(:species, @outfit.species_id) unless except.include?(:species)
|
||||
fields << hidden_field_tag(:color, @outfit.color_id) unless except.include?(:color)
|
||||
fields << hidden_field_tag(:pose, @selected_pose) if @selected_pose && !except.include?(:pose)
|
||||
|
|
|
|||
|
|
@ -261,15 +261,18 @@ class Outfit < ApplicationRecord
|
|||
(biology_layers + item_layers).sort_by(&:depth)
|
||||
end
|
||||
|
||||
def same_wardrobe_state_as?(other)
|
||||
wardrobe_params == other.wardrobe_params
|
||||
end
|
||||
|
||||
def wardrobe_params
|
||||
params = {
|
||||
name: name,
|
||||
color: color_id,
|
||||
species: species_id,
|
||||
pose: pose,
|
||||
state: pet_state_id,
|
||||
objects: worn_item_ids,
|
||||
closet: closeted_item_ids,
|
||||
objects: worn_item_ids.sort,
|
||||
closet: closeted_item_ids.sort,
|
||||
}
|
||||
params[:style] = alt_style_id if alt_style_id.present?
|
||||
params
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
= render "items/badges/kind", item: item
|
||||
= render "items/badges/first_seen", item: item
|
||||
- if is_worn
|
||||
= button_to wardrobe_v2_path, method: :get, class: "item-remove-button", title: "Remove #{item.name}", "aria-label": "Remove #{item.name}" do
|
||||
= button_to @wardrobe_path, method: :get, class: "item-remove-button", title: "Remove #{item.name}", "aria-label": "Remove #{item.name}" do
|
||||
❌
|
||||
= outfit_state_params @outfit.without_item(item)
|
||||
- else
|
||||
= button_to wardrobe_v2_path, method: :get, class: "item-add-button", title: "Add #{item.name}", "aria-label": "Add #{item.name}" do
|
||||
= button_to @wardrobe_path, method: :get, class: "item-add-button", title: "Add #{item.name}", "aria-label": "Add #{item.name}" do
|
||||
➕
|
||||
= outfit_state_params @outfit.with_item(item)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
= form_with url: wardrobe_v2_path, method: :get, class: "pose-picker-form" do |f|
|
||||
= form_with url: @wardrobe_path, method: :get, class: "pose-picker-form" do |f|
|
||||
= outfit_state_params except: [:pose, :style]
|
||||
%table.pose-picker-table
|
||||
%thead
|
||||
|
|
|
|||
22
app/views/wardrobe/_save_button.html.haml
Normal file
22
app/views/wardrobe/_save_button.html.haml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
- if @saved_outfit
|
||||
- if @is_owner
|
||||
- if @has_unsaved_changes
|
||||
= form_with url: outfit_path(@saved_outfit), method: :patch, class: "outfit-save-form" do |f|
|
||||
= render "save_outfit_fields"
|
||||
- @saved_outfit.closeted_items.each do |item|
|
||||
= hidden_field_tag "outfit[item_ids][closeted][]", item.id
|
||||
= f.submit "Save", class: "outfit-save-button"
|
||||
- else
|
||||
%button.outfit-save-button{disabled: true} Saved!
|
||||
- elsif user_signed_in?
|
||||
= form_with url: outfits_path, method: :post, class: "outfit-save-form" do |f|
|
||||
= render "save_outfit_fields"
|
||||
= f.submit "Save a copy", class: "outfit-save-button"
|
||||
- else
|
||||
= link_to "Log in to save a copy",
|
||||
new_auth_user_session_path(return_to: request.fullpath),
|
||||
class: "outfit-save-button"
|
||||
- elsif user_signed_in?
|
||||
= form_with url: outfits_path, method: :post, class: "outfit-save-form" do |f|
|
||||
= render "save_outfit_fields"
|
||||
= f.submit "Save", class: "outfit-save-button"
|
||||
8
app/views/wardrobe/_save_outfit_fields.html.haml
Normal file
8
app/views/wardrobe/_save_outfit_fields.html.haml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
= hidden_field_tag "outfit[name]", @outfit.name
|
||||
= hidden_field_tag "outfit[biology][species_id]", @outfit.species_id
|
||||
= hidden_field_tag "outfit[biology][color_id]", @outfit.color_id
|
||||
= hidden_field_tag "outfit[biology][pose]", @outfit.pet_state.pose
|
||||
- if @alt_style
|
||||
= hidden_field_tag "outfit[alt_style_id]", @alt_style.id
|
||||
- @outfit.worn_items.each do |item|
|
||||
= hidden_field_tag "outfit[item_ids][worn][]", item.id
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
%species-color-picker
|
||||
= form_with url: wardrobe_v2_path, method: :get do |f|
|
||||
= form_with url: @wardrobe_path, method: :get do |f|
|
||||
= outfit_state_params except: [:color, :species]
|
||||
= select_tag :color,
|
||||
options_from_collection_for_select(@colors, "id", "human_name",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
= form_with url: wardrobe_v2_path, method: :get, class: "style-picker-form" do |f|
|
||||
= form_with url: @wardrobe_path, method: :get, class: "style-picker-form" do |f|
|
||||
= outfit_state_params except: [:style]
|
||||
.style-picker-list
|
||||
- @available_alt_styles.each do |alt_style|
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@
|
|||
= csrf_meta_tags
|
||||
%meta{name: 'outfit-viewer-morph-mode', value: 'full-page'}
|
||||
%body.wardrobe-v2
|
||||
.wardrobe-container
|
||||
- if flash[:alert]
|
||||
.flash-messages
|
||||
.flash-alert= flash[:alert]
|
||||
.wardrobe-container{data: @saved_outfit ? {"has-unsaved-changes": @has_unsaved_changes.to_s} : {}}
|
||||
.outfit-preview-section
|
||||
- if @pet_type.nil?
|
||||
.no-preview-message
|
||||
|
|
@ -48,10 +51,10 @@
|
|||
.outfit-controls-section
|
||||
.item-search-form
|
||||
- if @search_mode
|
||||
= button_to wardrobe_v2_path, method: :get, class: "back-button" do
|
||||
= button_to @wardrobe_path, method: :get, class: "back-button" do
|
||||
←
|
||||
= outfit_state_params except: [:q]
|
||||
= form_with url: wardrobe_v2_path, method: :get, class: "search-form" do |f|
|
||||
= form_with url: @wardrobe_path, method: :get, class: "search-form" do |f|
|
||||
= outfit_state_params
|
||||
= f.text_field "q[name]", placeholder: "Search for items...", value: params.dig(:q, :name), "aria-label": "Search for items"
|
||||
= f.submit "Search"
|
||||
|
|
@ -59,7 +62,14 @@
|
|||
- if @search_mode
|
||||
= render "search_results"
|
||||
- else
|
||||
%h1 Untitled outfit
|
||||
.outfit-header
|
||||
= form_with url: @wardrobe_path, method: :get, class: "outfit-name-form" do |f|
|
||||
= outfit_state_params except: [:name]
|
||||
= f.text_field :name, value: @outfit.name,
|
||||
class: "outfit-name-input", placeholder: "Untitled outfit",
|
||||
"aria-label": "Outfit name"
|
||||
= f.submit "Rename", name: nil, class: "outfit-name-submit"
|
||||
= render "save_button"
|
||||
- if @outfit.worn_items.any?
|
||||
.worn-items
|
||||
- outfit_items_by_zone(@outfit).each do |zone_group|
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ OpenneoImpressItems::Application.routes.draw do
|
|||
get '/outfits/new', to: 'outfits#edit', as: :wardrobe
|
||||
get '/wardrobe' => redirect('/outfits/new')
|
||||
get '/wardrobe/v2', to: 'wardrobe#show', as: :wardrobe_v2
|
||||
get '/outfits/:id/v2', to: 'wardrobe#show', as: :wardrobe_v2_outfit
|
||||
get '/start/:color_name/:species_name' => 'outfits#start'
|
||||
|
||||
# The outfits users have created!
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
|||
- **Outfit rendering**: Uses the shared `<outfit-viewer>` web component
|
||||
- **Progressive enhancement**: Everything works without JS; web components add auto-submit and smoother interactions
|
||||
- **Smooth navigation**: Idiomorph DOM morphing reuses `<outfit-viewer>` layers across full-page navigations
|
||||
- **Outfit saving/loading**: Load saved outfits at `/outfits/:id/v2`, save changes (owner) or save copies (non-owner), editable outfit name, unsaved changes warning
|
||||
|
||||
### Key implementation files
|
||||
|
||||
|
|
@ -44,10 +45,13 @@ Code lives in `app/controllers/wardrobe_controller.rb`, `app/views/wardrobe/`, `
|
|||
|
||||
The goal is a basic usable wardrobe. Species/color/pose selection, item search, and add/remove are already done.
|
||||
|
||||
**Outfit Saving/Loading** (critical, not started)
|
||||
- Save button, editable outfit name, auto-save with indicator
|
||||
- Route to load saved outfits (`GET /outfits/:id/v2`)
|
||||
- Owner-only editing, handle unsaved outfits gracefully
|
||||
**Outfit Saving/Loading** (basic implementation done)
|
||||
- Save button near editable outfit name, disabled/"Saved!" when state matches saved
|
||||
- Route to load saved outfits (`GET /outfits/:id/v2`) with redirect-based state initialization
|
||||
- "Save a copy" for non-owners, login prompt for unauthenticated users
|
||||
- `beforeunload` warning for unsaved changes via MutationObserver
|
||||
- Outfit name tracked as URL param (`name`) to survive navigation
|
||||
- Not yet done: auto-save, renaming (the field is present but isn't consistently tracked)
|
||||
|
||||
**Alt Styles Support** (done)
|
||||
- `Outfit#visible_layers` handles alt styles
|
||||
|
|
@ -55,6 +59,22 @@ The goal is a basic usable wardrobe. Species/color/pose selection, item search,
|
|||
- Stale style params dropped gracefully when switching species
|
||||
- Search results auto-filtered by alt style compatibility
|
||||
|
||||
**Closeted Items**
|
||||
- Instead of just wearing/unwearing items, also support a "closeted" state: the user is *considering* this item,
|
||||
but it is not displayed on the pet itself right now.
|
||||
- Wearing an item will stop wearing, but keep in closet, items that are mutually incompatible with it.
|
||||
- Baseline behavior: separate toggle buttons for worn state and closeted state.
|
||||
- Unworn items have "Add" (wear).
|
||||
- Worn items have "Hide" (stop wearing, keep in closet) and "Remove" (remove from worn and closet).
|
||||
- Closeted items have "Show" (wear) and "Remove" (remove from closet).
|
||||
- Progressive enhancement:
|
||||
- In the outfit view, items in the same zone group (mutually-incompatible) are a radio group. Whichever radio button
|
||||
is checked is the worn item. The others are merely closeted.
|
||||
- In the search view, each item has a worn checkbox (analogous to the worn radio button).
|
||||
- In both views, when the item is closeted (always the case in the outfit view), there is a "Remove" button.
|
||||
- The radio button and checkbox are visually hidden, and are reflected in styles that emphasize the selected item,
|
||||
e.g., somewhat darker border/background, bold, etc. (See Wardrobe 2020.)
|
||||
|
||||
### Phase 2: Polish & Parity
|
||||
|
||||
Match the quality and usability of Wardrobe 2020 where it matters.
|
||||
|
|
|
|||
|
|
@ -343,6 +343,72 @@ RSpec.describe Outfit do
|
|||
item
|
||||
end
|
||||
|
||||
describe "#same_wardrobe_state_as?" do
|
||||
it "returns true for outfits with identical state" do
|
||||
outfit1 = Outfit.new(name: "Test", pet_state: @pet_state)
|
||||
outfit2 = Outfit.new(name: "Test", pet_state: @pet_state)
|
||||
|
||||
expect(outfit1.same_wardrobe_state_as?(outfit2)).to be true
|
||||
end
|
||||
|
||||
it "returns false when names differ" do
|
||||
outfit1 = Outfit.new(name: "Outfit A", pet_state: @pet_state)
|
||||
outfit2 = Outfit.new(name: "Outfit B", pet_state: @pet_state)
|
||||
|
||||
expect(outfit1.same_wardrobe_state_as?(outfit2)).to be false
|
||||
end
|
||||
|
||||
it "returns false when poses differ" do
|
||||
other_pet_state = create_pet_state(@pet_type, "SAD_MASC")
|
||||
outfit1 = Outfit.new(name: "Test", pet_state: @pet_state)
|
||||
outfit2 = Outfit.new(name: "Test", pet_state: other_pet_state)
|
||||
|
||||
expect(outfit1.same_wardrobe_state_as?(outfit2)).to be false
|
||||
end
|
||||
|
||||
it "returns false when worn items differ" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
outfit1 = Outfit.new(name: "Test", pet_state: @pet_state, worn_items: [hat])
|
||||
outfit2 = Outfit.new(name: "Test", pet_state: @pet_state)
|
||||
|
||||
expect(outfit1.same_wardrobe_state_as?(outfit2)).to be false
|
||||
end
|
||||
|
||||
it "returns true regardless of worn item order" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
shirt = create_item("Shirt", zones(:shirtdress))
|
||||
outfit1 = Outfit.new(name: "Test", pet_state: @pet_state, worn_items: [hat, shirt])
|
||||
outfit2 = Outfit.new(name: "Test", pet_state: @pet_state, worn_items: [shirt, hat])
|
||||
|
||||
expect(outfit1.same_wardrobe_state_as?(outfit2)).to be true
|
||||
end
|
||||
|
||||
it "returns false when species differ" do
|
||||
other_pet_type = PetType.create!(color: blue, species: species(:blumaroo), body_id: 2)
|
||||
other_pet_state = create_pet_state(other_pet_type, "HAPPY_MASC")
|
||||
|
||||
outfit1 = Outfit.new(name: "Test", pet_state: @pet_state)
|
||||
outfit2 = Outfit.new(name: "Test", pet_state: other_pet_state)
|
||||
|
||||
expect(outfit1.same_wardrobe_state_as?(outfit2)).to be false
|
||||
end
|
||||
|
||||
it "returns false when alt styles differ" do
|
||||
alt_style = AltStyle.create!(
|
||||
species: acara,
|
||||
color: blue,
|
||||
body_id: 999,
|
||||
series_name: "Nostalgic",
|
||||
thumbnail_url: "https://images.neopets.example/alt.png"
|
||||
)
|
||||
outfit1 = Outfit.new(name: "Test", pet_state: @pet_state, alt_style: alt_style)
|
||||
outfit2 = Outfit.new(name: "Test", pet_state: @pet_state)
|
||||
|
||||
expect(outfit1.same_wardrobe_state_as?(outfit2)).to be false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#visible_layers" do
|
||||
before do
|
||||
# Clean up any existing pet types to avoid conflicts
|
||||
|
|
|
|||
Loading…
Reference in a new issue