Emi Matchu
7eb209e206
Previously, "Then: Go to unlabeled appearance" would always take you to the *first* unlabeled appearance in our database. Now, we go to the *next* unlabeled appearance in the list, relative to this one.
181 lines
5.6 KiB
Ruby
181 lines
5.6 KiB
Ruby
class PetState < ApplicationRecord
|
|
SwfAssetType = 'biology'
|
|
|
|
MAIN_POSES = %w(HAPPY_FEM HAPPY_MASC SAD_FEM SAD_MASC SICK_FEM SICK_MASC)
|
|
|
|
has_many :contributions, :as => :contributed,
|
|
:inverse_of => :contributed # in case of duplicates being merged
|
|
has_many :outfits
|
|
has_many :parent_swf_asset_relationships, :as => :parent
|
|
has_many :swf_assets, :through => :parent_swf_asset_relationships
|
|
|
|
serialize :swf_asset_ids, coder: Serializers::IntegerSet, type: Array
|
|
|
|
belongs_to :pet_type
|
|
|
|
delegate :species_id, :species, :color_id, :color, to: :pet_type
|
|
|
|
alias_method :swf_asset_ids_from_association, :swf_asset_ids
|
|
|
|
scope :glitched, -> { where(glitched: true) }
|
|
scope :needs_labeling, -> { unlabeled.where(glitched: false) }
|
|
scope :unlabeled, -> { with_pose("UNKNOWN") }
|
|
scope :usable, -> { where(labeled: true, glitched: false) }
|
|
|
|
scope :newest, -> { order(created_at: :desc) }
|
|
scope :newest_pet_type, -> { joins(:pet_type).merge(PetType.newest) }
|
|
scope :created_before, ->(time) { where(arel_table[:created_at].lt(time)) }
|
|
|
|
# A simple ordering that tries to bring reliable pet states to the front.
|
|
scope :emotion_order, -> {
|
|
order(Arel.sql(
|
|
"(mood_id IS NULL) ASC, mood_id ASC, female DESC, unconverted DESC, " +
|
|
"glitched ASC, id DESC"
|
|
))
|
|
}
|
|
|
|
# Filter pet states using the "pose" concept we use in the editor.
|
|
scope :with_pose, -> pose {
|
|
case pose
|
|
when "UNCONVERTED"
|
|
where(unconverted: true)
|
|
when "HAPPY_MASC"
|
|
where(mood_id: 1, female: false)
|
|
when "HAPPY_FEM"
|
|
where(mood_id: 1, female: true)
|
|
when "SAD_MASC"
|
|
where(mood_id: 2, female: false)
|
|
when "SAD_FEM"
|
|
where(mood_id: 2, female: true)
|
|
when "SICK_MASC"
|
|
where(mood_id: 4, female: false)
|
|
when "SICK_FEM"
|
|
where(mood_id: 4, female: true)
|
|
when "UNKNOWN"
|
|
where(mood_id: nil).or(where(female: nil))
|
|
else
|
|
raise ArgumentError, "unexpected pose value #{pose}"
|
|
end
|
|
}
|
|
|
|
def pose
|
|
if unconverted?
|
|
"UNCONVERTED"
|
|
elsif mood_id.nil? || female.nil?
|
|
"UNKNOWN"
|
|
elsif mood_id == 1 && !female?
|
|
"HAPPY_MASC"
|
|
elsif mood_id == 1 && female?
|
|
"HAPPY_FEM"
|
|
elsif mood_id == 2 && !female?
|
|
"SAD_MASC"
|
|
elsif mood_id == 2 && female?
|
|
"SAD_FEM"
|
|
elsif mood_id == 4 && !female?
|
|
"SICK_MASC"
|
|
elsif mood_id == 4 && female?
|
|
"SICK_FEM"
|
|
else
|
|
raise "could not identify pose: moodId=#{mood_id}, female=#{female}, " +
|
|
"unconverted=#{unconverted}"
|
|
end
|
|
end
|
|
|
|
# TODO: More and more, wanting to refactor poses…
|
|
def pose=(pose)
|
|
case pose
|
|
when "UNKNOWN"
|
|
label_pose nil, nil, unconverted: nil, labeled: false
|
|
when "HAPPY_MASC"
|
|
label_pose 1, false
|
|
when "HAPPY_FEM"
|
|
label_pose 1, true
|
|
when "SAD_MASC"
|
|
label_pose 2, false
|
|
when "SAD_FEM"
|
|
label_pose 2, true
|
|
when "SICK_MASC"
|
|
label_pose 4, false
|
|
when "SICK_FEM"
|
|
label_pose 4, true
|
|
when "UNCONVERTED"
|
|
label_pose nil, nil, unconverted: true
|
|
end
|
|
end
|
|
|
|
def to_param
|
|
"#{id}-#{pose.split('_').map(&:capitalize).join('-')}"
|
|
end
|
|
|
|
# Because our column is named `swf_asset_ids`, we need to ensure writes to
|
|
# it go to the attribute, and not the thing ActiveRecord does of finding the
|
|
# relevant `swf_assets`.
|
|
# TODO: Consider renaming the column to `cached_swf_asset_ids`?
|
|
def swf_asset_ids=(new_swf_asset_ids)
|
|
write_attribute(:swf_asset_ids, new_swf_asset_ids)
|
|
end
|
|
|
|
private
|
|
|
|
# A helper for the `pose=` method.
|
|
def label_pose(mood_id, female, unconverted: false, labeled: true)
|
|
self.labeled = labeled
|
|
self.mood_id = mood_id
|
|
self.female = female
|
|
self.unconverted = unconverted
|
|
end
|
|
|
|
def self.last_updated_key
|
|
PetState.maximum(:updated_at)
|
|
end
|
|
|
|
def self.all_supported_poses
|
|
Rails.cache.fetch("PetState.all_supported_poses #{last_updated_key}") do
|
|
{}.tap do |h|
|
|
includes(:pet_type).find_each do |pet_state|
|
|
h[pet_state.species_id] ||= {}
|
|
h[pet_state.species_id][pet_state.color_id] ||= []
|
|
h[pet_state.species_id][pet_state.color_id] << pet_state.pose
|
|
end
|
|
|
|
h.values.map(&:values).flatten(1).each(&:uniq!).each(&:sort!)
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.next_unlabeled_appearance(after_id: nil)
|
|
# Rather than just getting the newest unlabeled pet state, prioritize the
|
|
# newest *pet type*. This better matches the user's perception of what the
|
|
# newest state is, because the Rainbow Pool UI is grouped by pet type!
|
|
pet_states = needs_labeling.newest_pet_type.newest
|
|
|
|
# If `after_id` is given, convert it from a PetState ID to creation
|
|
# timestamps, and find the next record prior to those timestamps. This
|
|
# enables skipping past records the user doesn't want to label.
|
|
if after_id
|
|
begin
|
|
after_pet_state = PetState.find(after_id)
|
|
before_pt_created_at = after_pet_state.pet_type.created_at
|
|
before_ps_created_at = after_pet_state.created_at
|
|
rescue ActiveRecord::RecordNotFound
|
|
Rails.logger.warn "PetState.next_unlabeled_appearance: Could not " +
|
|
"find pet state ##{after_id}"
|
|
return nil
|
|
end
|
|
|
|
# Because we sort by `newest_pet_type` first, then breaks ties by
|
|
# `newest`, our filter needs to operate the same way. Kudos to:
|
|
# https://brunoscheufler.com/blog/2022-01-01-paginating-large-ordered-datasets-with-cursor-based-pagination
|
|
pet_states.merge!(
|
|
PetType.created_before(before_pt_created_at).or(
|
|
PetType.created_at(before_pt_created_at).and(
|
|
PetState.created_before(before_ps_created_at)
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
pet_states.first
|
|
end
|
|
end
|
|
|