From a656cd511a6fae3ea7717e325ca7c4a31df811a0 Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 5 Dec 2023 18:04:54 -0800 Subject: [PATCH] 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!) --- app/controllers/pet_types_controller.rb | 33 +++++++++++ app/models/color.rb | 3 +- app/models/pet_state.rb | 79 +------------------------ app/models/pet_type.rb | 38 +++++++++--- config/locales/en.yml | 1 - config/routes.rb | 7 ++- 6 files changed, 70 insertions(+), 91 deletions(-) diff --git a/app/controllers/pet_types_controller.rb b/app/controllers/pet_types_controller.rb index ed30e051..bcef99e7 100644 --- a/app/controllers/pet_types_controller.rb +++ b/app/controllers/pet_types_controller.rb @@ -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]). diff --git a/app/models/color.rb b/app/models/color.rb index d7c50e80..d57dcedd 100644 --- a/app/models/color.rb +++ b/app/models/color.rb @@ -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 diff --git a/app/models/pet_state.rb b/app/models/pet_state.rb index 8ab1563f..ac80eb72 100644 --- a/app/models/pet_state.rb +++ b/app/models/pet_state.rb @@ -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 diff --git a/app/models/pet_type.rb b/app/models/pet_type.rb index feaea0b8..159d0291 100644 --- a/app/models/pet_type.rb +++ b/app/models/pet_type.rb @@ -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| diff --git a/config/locales/en.yml b/config/locales/en.yml index 718f13bc..37d332a2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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}" diff --git a/config/routes.rb b/config/routes.rb index 2ff04e02..3e80b268 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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