Create endpoint to show all species appearances for a given color

Building toward replacing more of the 2020 data sources! I think this is
an endpoint that benefits from bulk loading, esp with the way the item
page previews work. I also like taking the concept of "canonical" out of
the GQL interface, and instead just loading for each of the 50 species
and letting the client decide. (And then it can fast-swap between them!)
This commit is contained in:
Emi Matchu 2023-12-05 18:04:54 -08:00
parent c8de3dae63
commit a656cd511a
6 changed files with 70 additions and 91 deletions

View file

@ -1,4 +1,37 @@
class PetTypesController < ApplicationController
def index
color = Color.find params[:color_id]
pet_types = color.pet_types.includes(pet_states: [:swf_assets]).
includes(species: [:translations])
# This is a relatively big request, for relatively static data. Let's
# consider it fresh for 10min (so new pet releases show up quickly), but
# it's also okay for the client to quickly serve the cached copy then
# reload in the background, if it's been less than a day.
expires_in 10.minutes, stale_while_revalidate: 1.day, public: true
# We dive deep to get all the right fields for appearance data, cuz this
# endpoint is primarily used to power the preview on the item page!
render json: pet_types.map { |pt|
pt.as_json(
only: [:id, :body_id],
include: {
species: {only: [:id], methods: [:name, :human_name]},
canonical_pet_state: {
only: [:id],
methods: [:pose],
include: {
swf_assets: {
only: [:id, :known_glitches],
methods: [:zone, :restricted_zones, :urls],
},
},
},
},
)
}
end
def show
@pet_type = PetType.
where(species_id: params[:species_id]).

View file

@ -1,5 +1,6 @@
class Color < ApplicationRecord
translates :name
has_many :pet_types
scope :alphabetical, -> {
ct = Color::Translation.arel_table
@ -24,7 +25,7 @@ class Color < ApplicationRecord
end
def as_json(options={})
{id: id, name: human_name, unfunny_name: unfunny_human_name, prank: prank?}
{id: id, name: name, human_name: human_name}
end
def human_name

View file

@ -25,7 +25,7 @@ class PetState < ApplicationRecord
}
# Filter pet states using the "pose" concept we use in the editor.
scope :with_pose, ->(pose) {
scope :with_pose, -> pose {
case pose
when "UNCONVERTED"
where(unconverted: true)
@ -71,16 +71,6 @@ class PetState < ApplicationRecord
end
end
def as_json(options={})
{
id: id,
gender_mood_description: gender_mood_description,
swf_asset_ids: swf_asset_ids_array,
artist_name: artist_name,
artist_url: artist_url
}
end
def reassign_children_to!(main_pet_state)
self.contributions.each do |contribution|
contribution.contributed = main_pet_state
@ -127,47 +117,6 @@ class PetState < ApplicationRecord
rel.save!
end
end
def mood
Mood.find(self.mood_id)
end
def gender_name
if female?
I18n.translate("pet_states.description.gender.female")
else
I18n.translate("pet_states.description.gender.male")
end
end
def mood_name
I18n.translate("pet_states.description.mood.#{mood.name}")
end
def gender_mood_description
if glitched?
I18n.translate('pet_states.description.glitched')
elsif unconverted?
I18n.translate('pet_states.description.unconverted')
elsif labeled?
I18n.translate('pet_states.description.main', :gender => gender_name,
:mood => mood_name)
else
I18n.translate('pet_states.description.unlabeled')
end
end
def artist_name
artist_neopets_username || I18n.translate("pet_states.default_artist_name")
end
def artist_url
if artist_neopets_username
"https://www.neopets.com/userlookup.phtml?user=#{artist_neopets_username}"
else
nil
end
end
def self.from_pet_type_and_biology_info(pet_type, info)
swf_asset_ids = []
@ -223,31 +172,5 @@ class PetState < ApplicationRecord
pet_state.unconverted = (relationships.size == 1)
pet_state
end
# Copied from https://github.com/matchu/neopets/blob/5d13a720b616ba57fbbd54541f3e5daf02b3fedc/lib/neopets/pet/mood.rb
class Mood
attr_reader :id, :name
def initialize(options)
@id = options[:id]
@name = options[:name]
end
def self.find(id)
self.all_by_id[id.to_i]
end
def self.all
@all ||= [
Mood.new(:id => 1, :name => :happy),
Mood.new(:id => 2, :name => :sad),
Mood.new(:id => 4, :name => :sick)
]
end
def self.all_by_id
@all_by_id ||= self.all.inject({}) { |h, m| h[m.id] = m; h }
end
end
end

View file

@ -59,15 +59,10 @@ class PetType < ApplicationRecord
end
def as_json(options={})
if options[:for] == 'wardrobe'
{
:id => id,
:body_id => body_id,
:pet_states => pet_states.emotion_order.as_json
}
else
{:image_hash => image_hash}
end
super({
only: [:id],
methods: [:color, :species]
}.merge(options))
end
def image_hash
@ -157,6 +152,31 @@ class PetType < ApplicationRecord
end
end
def canonical_pet_state
# For consistency (randomness is always scary!), we use the PetType ID to
# determine which gender to prefer. That way, it'll be stable, but we'll
# still get the *vibes* of uniform randomness.
preferred_gender = id % 2 == 0 ? :fem : :masc
# NOTE: If this were only being called on one pet type at a time, it would
# be more efficient to send this as a single query with an `order` part and
# just get the first record. But we most importantly call this on all the
# pet types for a single color at once, in which case it's better for the
# caller to use `includes(:pet_states)` to preload the pet states then sort
# then in Ruby here, rather than send ~50 queries. Also, there's generally
# very few pet states per pet type, so the perf difference is negligible.
pet_states.sort_by { |pet_state|
gender = pet_state.female? ? :fem : :masc
[
pet_state.mood_id.present? ? -1 : 1, # Prefer mood is labeled
pet_state.mood_id, # Prefer mood is happy, then sad, then sick
gender == preferred_gender ? -1 : 1, # Prefer our "random" gender
-pet_state.id, # Prefer newer pet states
!pet_state.glitched? ? -1 : 1, # Prefer is not glitched
]
}.first
end
def self.all_by_ids_or_children(ids, pet_states)
pet_states_by_pet_type_id = {}
pet_states.each do |pet_state|

View file

@ -715,7 +715,6 @@ en:
glitched: Glitched
unconverted: Unconverted
unlabeled: Unlabeled
default_artist_name: the OpenNeo team
pet_types:
human_name: "%{color_human_name} %{species_human_name}"

View file

@ -23,11 +23,14 @@ OpenneoImpressItems::Application.routes.draw do
get :needed
end
end
resources :species do
resources :colors do
resources :species, only: [] do
resources :colors, only: [] do
get :pet_type, to: 'pet_types#show'
end
end
resources :colors, only: [] do
resources :pet_types, only: [:index]
end
# Loading and modeling pets!
post '/pets/load' => 'pets#load', :as => :load_pet