Compare commits
No commits in common. "b03b32c538250ff5b5bf39df13cdc2f0c63a8d6b" and "f545510edcf2d5b73832f8b2d89a0aa25ddd2449" have entirely different histories.
b03b32c538
...
f545510edc
148 changed files with 338 additions and 1748 deletions
24
.solargraph.yml
Normal file
24
.solargraph.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
include:
|
||||
- "app/**/*.rb"
|
||||
- "config/**/*.rb"
|
||||
exclude:
|
||||
- "spec/**/*"
|
||||
- "test/**/*"
|
||||
- "vendor/**/*"
|
||||
- ".bundle/**/*"
|
||||
require:
|
||||
- actioncable
|
||||
- actionmailer
|
||||
- actionpack
|
||||
- actionview
|
||||
- activemodel
|
||||
- activerecord
|
||||
- activesupport
|
||||
plugins:
|
||||
- solargraph-rails
|
||||
domains: []
|
||||
reporters:
|
||||
- require_not_found
|
||||
require_paths: []
|
||||
max_files: 5000
|
||||
27
Gemfile
27
Gemfile
|
|
@ -11,11 +11,11 @@ gem 'mysql2', '~> 0.5.5'
|
|||
|
||||
# For reading the .env file, which you can use in development to more easily
|
||||
# set environment variables for secret data.
|
||||
gem 'dotenv', '~> 3.2'
|
||||
gem 'dotenv-rails', '~> 2.8', '>= 2.8.1'
|
||||
|
||||
# For the asset pipeline: templates, CSS, JS, etc.
|
||||
gem 'sprockets', '~> 4.2'
|
||||
gem 'haml', '~> 7.2'
|
||||
gem 'haml', '~> 6.1', '>= 6.1.1'
|
||||
gem 'sass-rails', '~> 6.0'
|
||||
gem 'terser', '~> 1.1', '>= 1.1.17'
|
||||
gem 'jsbundling-rails', '~> 1.3'
|
||||
|
|
@ -25,7 +25,7 @@ gem 'turbo-rails', '~> 2.0'
|
|||
gem 'devise', '~> 4.9', '>= 4.9.2'
|
||||
gem 'devise-encryptable', '~> 0.2.0'
|
||||
gem 'omniauth', '~> 2.1'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 2.0', '>= 2.0.1'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||
gem "omniauth_openid_connect", "~> 0.7.1"
|
||||
|
||||
# For pagination UI.
|
||||
|
|
@ -40,7 +40,7 @@ gem 'nokogiri', '~> 1.15', '>= 1.15.3'
|
|||
|
||||
# For safely rendering users' Markdown + HTML on item list pages.
|
||||
gem 'rdiscount', '~> 2.2', '>= 2.2.7.1'
|
||||
gem 'sanitize', '~> 7.0'
|
||||
gem 'sanitize', '~> 6.0', '>= 6.0.2'
|
||||
|
||||
# For working with Neopets APIs.
|
||||
# unstable version of RocketAMF interprets info registry as a hash instead of an array
|
||||
|
|
@ -61,9 +61,6 @@ gem "async", "~> 2.17", require: false
|
|||
gem "async-http", "~> 0.89.0", require: false
|
||||
gem "thread-local", "~> 1.1", require: false
|
||||
|
||||
# For image processing (outfit PNG rendering).
|
||||
gem "ruby-vips", "~> 2.2"
|
||||
|
||||
# For debugging.
|
||||
group :development do
|
||||
gem 'debug', '~> 1.9.2'
|
||||
|
|
@ -74,7 +71,7 @@ end
|
|||
gem 'bootsnap', '~> 1.16', require: false
|
||||
|
||||
# For investigating performance issues.
|
||||
gem 'rack-mini-profiler', '~> 4.0', '>= 4.0.1'
|
||||
gem "rack-mini-profiler", "~> 3.1"
|
||||
gem "memory_profiler", "~> 1.0"
|
||||
gem "stackprof", "~> 0.2.25"
|
||||
|
||||
|
|
@ -85,6 +82,16 @@ gem "sentry-rails", "~> 5.12"
|
|||
# For tasks that use shell commands.
|
||||
gem "shell", "~> 0.8.1"
|
||||
|
||||
# For workspace autocomplete.
|
||||
group :development do
|
||||
gem "solargraph", "~> 0.50.0"
|
||||
gem "solargraph-rails", "~> 1.1"
|
||||
end
|
||||
|
||||
# For automated tests.
|
||||
gem 'rspec-rails', '~> 8.0', '>= 8.0.2', group: [:development, :test]
|
||||
gem "webmock", "~> 3.24", group: [:test]
|
||||
group :development, :test do
|
||||
gem "rspec-rails", "~> 7.0"
|
||||
end
|
||||
group :test do
|
||||
gem "webmock", "~> 3.24"
|
||||
end
|
||||
|
|
|
|||
264
Gemfile.lock
264
Gemfile.lock
|
|
@ -6,31 +6,31 @@ PATH
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
action_text-trix (2.1.16)
|
||||
action_text-trix (2.1.15)
|
||||
railties
|
||||
actioncable (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actioncable (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionmailbox (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
activejob (= 8.1.1)
|
||||
activerecord (= 8.1.1)
|
||||
activestorage (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionmailer (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
actionview (= 8.1.1)
|
||||
activejob (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionpack (8.1.1)
|
||||
actionview (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
|
|
@ -38,36 +38,36 @@ GEM
|
|||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.1.2)
|
||||
actiontext (8.1.1)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionpack (= 8.1.1)
|
||||
activerecord (= 8.1.1)
|
||||
activestorage (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionview (8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activejob (8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activerecord (8.1.2)
|
||||
activemodel (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activemodel (8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
activerecord (8.1.1)
|
||||
activemodel (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activestorage (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
activejob (= 8.1.1)
|
||||
activerecord (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.1.2)
|
||||
activesupport (8.1.1)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
|
|
@ -83,13 +83,14 @@ GEM
|
|||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
async (2.35.3)
|
||||
ast (2.4.3)
|
||||
async (2.35.0)
|
||||
console (~> 1.29)
|
||||
fiber-annotation
|
||||
io-event (~> 1.11)
|
||||
metrics (~> 0.12)
|
||||
traces (~> 0.18)
|
||||
async-container (0.29.0)
|
||||
async-container (0.27.7)
|
||||
async (~> 2.22)
|
||||
async-http (0.89.0)
|
||||
async (>= 2.10.2)
|
||||
|
|
@ -105,17 +106,19 @@ GEM
|
|||
async-http (~> 0.56)
|
||||
async-pool (0.11.1)
|
||||
async (>= 2.0)
|
||||
async-service (0.17.0)
|
||||
async-service (0.16.0)
|
||||
async
|
||||
async-container (~> 0.28)
|
||||
async-container (~> 0.16)
|
||||
string-format (~> 0.2)
|
||||
attr_required (1.0.2)
|
||||
backport (1.2.0)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.21)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bindata (2.5.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.21.1)
|
||||
bootsnap (1.20.1)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.3.0)
|
||||
childprocess (5.1.0)
|
||||
|
|
@ -143,7 +146,10 @@ GEM
|
|||
devise-encryptable (0.2.0)
|
||||
devise (>= 2.1.0)
|
||||
diff-lcs (1.6.2)
|
||||
dotenv (3.2.0)
|
||||
dotenv (2.8.1)
|
||||
dotenv-rails (2.8.1)
|
||||
dotenv (= 2.8.1)
|
||||
railties (>= 3.2)
|
||||
drb (2.2.3)
|
||||
e2mmap (0.1.0)
|
||||
email_validator (2.2.4)
|
||||
|
|
@ -168,20 +174,21 @@ GEM
|
|||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.5.0)
|
||||
faraday-follow_redirects (0.4.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
ffi (1.17.3-aarch64-linux-gnu)
|
||||
ffi (1.17.3-arm64-darwin)
|
||||
ffi (1.17.3-x86_64-linux-gnu)
|
||||
ffi (1.17.2)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
fiber-annotation (0.2.0)
|
||||
fiber-local (1.1.0)
|
||||
fiber-storage
|
||||
fiber-storage (1.0.1)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
haml (7.2.0)
|
||||
haml (6.4.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
|
|
@ -199,6 +206,7 @@ GEM
|
|||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jaro_winkler (1.6.1)
|
||||
jsbundling-rails (1.3.1)
|
||||
railties (>= 6.0.0)
|
||||
json (2.18.0)
|
||||
|
|
@ -209,13 +217,19 @@ GEM
|
|||
bindata
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
kramdown (2.5.1)
|
||||
rexml (>= 3.3.9)
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.1.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
localhost (1.7.0)
|
||||
lint_roller (1.1.0)
|
||||
localhost (1.6.0)
|
||||
logger (1.7.0)
|
||||
loofah (2.25.0)
|
||||
crass (~> 1.0.2)
|
||||
|
|
@ -231,6 +245,7 @@ GEM
|
|||
memory_profiler (1.1.0)
|
||||
metrics (0.15.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (6.0.1)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
|
|
@ -248,18 +263,21 @@ GEM
|
|||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||
nokogiri (1.18.10)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm64-darwin)
|
||||
nokogiri (1.18.10-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||
nokogiri (1.18.10-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
omniauth (2.1.4)
|
||||
hashie (>= 3.4.6)
|
||||
logger
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-rails_csrf_protection (2.0.1)
|
||||
omniauth-rails_csrf_protection (1.0.2)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth_openid_connect (0.7.1)
|
||||
|
|
@ -280,34 +298,38 @@ GEM
|
|||
webfinger (~> 2.0)
|
||||
openssl (3.3.2)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.8.0)
|
||||
prism (1.7.0)
|
||||
process-metrics (0.8.0)
|
||||
console (~> 1.8)
|
||||
json (~> 2)
|
||||
samovar (~> 2.1)
|
||||
protocol-hpack (1.5.1)
|
||||
protocol-http (0.58.0)
|
||||
protocol-http1 (0.36.0)
|
||||
protocol-http (~> 0.58)
|
||||
protocol-http2 (0.24.0)
|
||||
protocol-http (0.56.1)
|
||||
protocol-http1 (0.35.2)
|
||||
protocol-http (~> 0.22)
|
||||
protocol-http2 (0.23.0)
|
||||
protocol-hpack (~> 1.4)
|
||||
protocol-http (~> 0.47)
|
||||
protocol-rack (0.21.0)
|
||||
protocol-rack (0.19.0)
|
||||
io-stream (>= 0.10)
|
||||
protocol-http (~> 0.58)
|
||||
protocol-http (~> 0.43)
|
||||
rack (>= 1.0)
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (7.0.2)
|
||||
public_suffix (7.0.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack-attack (6.8.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-mini-profiler (4.0.1)
|
||||
rack-mini-profiler (3.3.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-oauth2 (2.3.0)
|
||||
activesupport
|
||||
|
|
@ -327,20 +349,20 @@ GEM
|
|||
rack (>= 1.3)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rails (8.1.2)
|
||||
actioncable (= 8.1.2)
|
||||
actionmailbox (= 8.1.2)
|
||||
actionmailer (= 8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
actiontext (= 8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activemodel (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
rails (8.1.1)
|
||||
actioncable (= 8.1.1)
|
||||
actionmailbox (= 8.1.1)
|
||||
actionmailer (= 8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
actiontext (= 8.1.1)
|
||||
actionview (= 8.1.1)
|
||||
activejob (= 8.1.1)
|
||||
activemodel (= 8.1.1)
|
||||
activerecord (= 8.1.1)
|
||||
activestorage (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.1.2)
|
||||
railties (= 8.1.1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
|
|
@ -351,26 +373,31 @@ GEM
|
|||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
railties (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
railties (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
rbs (2.8.4)
|
||||
rdiscount (2.2.7.3)
|
||||
rdoc (7.1.0)
|
||||
rdoc (7.0.3)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
responders (3.2.0)
|
||||
actionpack (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
reverse_markdown (2.1.1)
|
||||
nokogiri
|
||||
rexml (3.4.4)
|
||||
rspec-core (3.13.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
|
|
@ -380,24 +407,36 @@ GEM
|
|||
rspec-mocks (3.13.7)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (8.0.2)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
rspec-rails (7.1.1)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.6)
|
||||
ruby-vips (2.3.0)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
rubocop (1.82.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.48.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.48.0)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
ruby-progressbar (1.13.0)
|
||||
samovar (2.4.1)
|
||||
console (~> 1.0)
|
||||
mapping (~> 1.0)
|
||||
sanitize (7.0.0)
|
||||
sanitize (6.1.3)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.16.8)
|
||||
nokogiri (>= 1.12.0)
|
||||
sass-rails (6.0.0)
|
||||
sassc-rails (~> 2.1, >= 2.1.1)
|
||||
sassc (2.4.0)
|
||||
|
|
@ -418,6 +457,25 @@ GEM
|
|||
shell (0.8.1)
|
||||
e2mmap
|
||||
sync
|
||||
solargraph (0.50.0)
|
||||
backport (~> 1.2)
|
||||
benchmark
|
||||
bundler (~> 2.0)
|
||||
diff-lcs (~> 1.4)
|
||||
e2mmap
|
||||
jaro_winkler (~> 1.5)
|
||||
kramdown (~> 2.3)
|
||||
kramdown-parser-gfm (~> 1.1)
|
||||
parser (~> 3.0)
|
||||
rbs (~> 2.0)
|
||||
reverse_markdown (~> 2.0)
|
||||
rubocop (~> 1.38)
|
||||
thor (~> 1.0)
|
||||
tilt (~> 2.0)
|
||||
yard (~> 0.9, >= 0.9.24)
|
||||
solargraph-rails (1.2.4)
|
||||
activesupport
|
||||
solargraph (>= 0.48.0, <= 0.57)
|
||||
sprockets (4.2.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
logger
|
||||
|
|
@ -438,17 +496,20 @@ GEM
|
|||
temple (0.10.4)
|
||||
terser (1.2.6)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
thor (1.5.0)
|
||||
thor (1.4.0)
|
||||
thread-local (1.1.0)
|
||||
tilt (2.7.0)
|
||||
tilt (2.6.1)
|
||||
timeout (0.6.0)
|
||||
traces (0.18.2)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.21)
|
||||
turbo-rails (2.0.20)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
validate_url (1.0.15)
|
||||
|
|
@ -474,11 +535,13 @@ GEM
|
|||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
will_paginate (4.0.1)
|
||||
yard (0.9.38)
|
||||
zeitwerk (2.7.4)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
|
|
@ -490,9 +553,9 @@ DEPENDENCIES
|
|||
debug (~> 1.9.2)
|
||||
devise (~> 4.9, >= 4.9.2)
|
||||
devise-encryptable (~> 0.2.0)
|
||||
dotenv (~> 3.2)
|
||||
dotenv-rails (~> 2.8, >= 2.8.1)
|
||||
falcon (~> 0.48.0)
|
||||
haml (~> 7.2)
|
||||
haml (~> 6.1, >= 6.1.1)
|
||||
http_accept_language (~> 2.1, >= 2.1.1)
|
||||
jsbundling-rails (~> 1.3)
|
||||
letter_opener (~> 1.8, >= 1.8.1)
|
||||
|
|
@ -500,20 +563,21 @@ DEPENDENCIES
|
|||
mysql2 (~> 0.5.5)
|
||||
nokogiri (~> 1.15, >= 1.15.3)
|
||||
omniauth (~> 2.1)
|
||||
omniauth-rails_csrf_protection (~> 2.0, >= 2.0.1)
|
||||
omniauth-rails_csrf_protection (~> 1.0)
|
||||
omniauth_openid_connect (~> 0.7.1)
|
||||
rack-attack (~> 6.7)
|
||||
rack-mini-profiler (~> 4.0, >= 4.0.1)
|
||||
rack-mini-profiler (~> 3.1)
|
||||
rails (~> 8.0, >= 8.0.1)
|
||||
rails-i18n (~> 8.0, >= 8.0.1)
|
||||
rdiscount (~> 2.2, >= 2.2.7.1)
|
||||
rspec-rails (~> 8.0, >= 8.0.2)
|
||||
ruby-vips (~> 2.2)
|
||||
sanitize (~> 7.0)
|
||||
rspec-rails (~> 7.0)
|
||||
sanitize (~> 6.0, >= 6.0.2)
|
||||
sass-rails (~> 6.0)
|
||||
sentry-rails (~> 5.12)
|
||||
sentry-ruby (~> 5.12)
|
||||
shell (~> 0.8.1)
|
||||
solargraph (~> 0.50.0)
|
||||
solargraph-rails (~> 1.1)
|
||||
sprockets (~> 4.2)
|
||||
stackprof (~> 0.2.25)
|
||||
terser (~> 1.1, >= 1.1.17)
|
||||
|
|
|
|||
|
|
@ -150,13 +150,13 @@ function updateStage() {
|
|||
|
||||
function updateCanvasDimensions() {
|
||||
// Set the canvas's internal dimensions to be higher, if the device has high
|
||||
// DPI. Scale the stage to match, too.
|
||||
// DPI. Scale the movie clip to match, too.
|
||||
const internalWidth = canvas.offsetWidth * window.devicePixelRatio;
|
||||
const internalHeight = canvas.offsetHeight * window.devicePixelRatio;
|
||||
canvas.width = internalWidth;
|
||||
canvas.height = internalHeight;
|
||||
stage.scaleX = internalWidth / library.properties.width;
|
||||
stage.scaleY = internalHeight / library.properties.height;
|
||||
movieClip.scaleX = internalWidth / library.properties.width;
|
||||
movieClip.scaleY = internalHeight / library.properties.height;
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
|
|
@ -176,28 +176,23 @@ window.addEventListener("resize", () => {
|
|||
////////////////////////////////////////////////////
|
||||
|
||||
async function startMovie() {
|
||||
// Install the MotionGuidePlugin, which is needed for motion path animations.
|
||||
createjs.MotionGuidePlugin.install();
|
||||
|
||||
// Load the movie's library (from the JS file already run), and use it to
|
||||
// build a movie clip.
|
||||
library = await getLibrary();
|
||||
movieClip = buildMovieClip(library);
|
||||
|
||||
updateCanvasDimensions();
|
||||
|
||||
if (canvas.getContext("2d") == null) {
|
||||
console.warn(`Out of memory, can't use canvas for ${libraryUrl}.`);
|
||||
// TODO: "Too many animations!"
|
||||
return;
|
||||
}
|
||||
|
||||
stage = new library.Stage(canvas);
|
||||
stage = new window.createjs.Stage(canvas);
|
||||
stage.addChild(movieClip);
|
||||
updateCanvasDimensions();
|
||||
updateStage();
|
||||
|
||||
// Signal to the library that the composition is ready.
|
||||
AdobeAn.compositionLoaded(library.properties.id);
|
||||
|
||||
loadingStatus = "loaded";
|
||||
canvas.setAttribute("data-status", "loaded");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ class ApplicationController < ActionController::Base
|
|||
before_action :save_return_to_path,
|
||||
if: ->(c) { c.controller_name == 'sessions' && c.action_name == 'new' }
|
||||
|
||||
# Enable profiling tools in development or when logged in as an admin.
|
||||
# Enable profiling tools if logged in as admin.
|
||||
before_action do
|
||||
Rack::MiniProfiler.authorize_request if Rails.env.development? || current_user&.admin?
|
||||
if current_user && current_user.admin?
|
||||
Rack::MiniProfiler.authorize_request
|
||||
end
|
||||
end
|
||||
|
||||
class AccessDenied < StandardError; end
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@ class ItemTradesController < ApplicationController
|
|||
@item = Item.find params[:item_id]
|
||||
@type = type_from_params
|
||||
|
||||
@item_trades = @item.visible_trades(
|
||||
scope: ClosetHanger.includes(:user, :list).
|
||||
order('users.last_trade_activity_at DESC'),
|
||||
user: current_user,
|
||||
remote_ip: request.remote_ip
|
||||
)
|
||||
@item_trades = @item.closet_hangers.trading.includes(:user, :list).
|
||||
user_is_active.order('users.last_trade_activity_at DESC').
|
||||
to_trades(current_user, request.remote_ip)
|
||||
@trades = @item_trades[@type]
|
||||
|
||||
if user_signed_in?
|
||||
|
|
|
|||
|
|
@ -80,10 +80,8 @@ class ItemsController < ApplicationController
|
|||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@trades = @item.visible_trades(
|
||||
user: current_user,
|
||||
remote_ip: request.remote_ip
|
||||
)
|
||||
@trades = @item.closet_hangers.trading.user_is_active.
|
||||
to_trades(current_user, request.remote_ip)
|
||||
|
||||
@contributors_with_counts = @item.contributors_with_counts
|
||||
|
||||
|
|
@ -109,15 +107,6 @@ class ItemsController < ApplicationController
|
|||
includes(:species).merge(Species.alphabetical)
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: @item.as_json(
|
||||
include_trade_counts: true,
|
||||
include_nc_trade_value: true,
|
||||
current_user: current_user,
|
||||
remote_ip: request.remote_ip
|
||||
)
|
||||
end
|
||||
|
||||
format.gif do
|
||||
expires_in 1.month
|
||||
redirect_to @item.thumbnail_url, allow_other_host: true
|
||||
|
|
|
|||
|
|
@ -13,26 +13,7 @@ class OutfitsController < ApplicationController
|
|||
end
|
||||
|
||||
def edit
|
||||
respond_to do |format|
|
||||
format.html { render "outfits/edit", layout: false }
|
||||
format.png do
|
||||
@outfit = build_outfit_from_wardrobe_params
|
||||
if @outfit.valid?
|
||||
renderer = OutfitImageRenderer.new(@outfit)
|
||||
png_data = renderer.render
|
||||
|
||||
if png_data
|
||||
send_data png_data, type: "image/png", disposition: "inline",
|
||||
filename: "outfit.png"
|
||||
expires_in 1.day, public: true
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
else
|
||||
head :bad_request
|
||||
end
|
||||
end
|
||||
end
|
||||
render "outfits/edit", layout: false
|
||||
end
|
||||
|
||||
def index
|
||||
|
|
@ -137,40 +118,6 @@ class OutfitsController < ApplicationController
|
|||
biology: [:species_id, :color_id, :pose, :pet_state_id])
|
||||
end
|
||||
|
||||
def build_outfit_from_wardrobe_params
|
||||
# Load items first
|
||||
worn_item_ids = params[:objects] ? Array(params[:objects]).map(&:to_i) : []
|
||||
closeted_item_ids = params[:closet] ? Array(params[:closet]).map(&:to_i) : []
|
||||
|
||||
worn_items = Item.where(id: worn_item_ids)
|
||||
closeted_items = Item.where(id: closeted_item_ids)
|
||||
|
||||
# Build outfit with biology and items
|
||||
outfit = Outfit.new(
|
||||
worn_items: worn_items,
|
||||
closeted_items: closeted_items,
|
||||
)
|
||||
|
||||
# Set biology from species, color, and pose params
|
||||
if params[:species] && params[:color] && params[:pose]
|
||||
outfit.biology = {
|
||||
species_id: params[:species],
|
||||
color_id: params[:color],
|
||||
pose: params[:pose]
|
||||
}
|
||||
elsif params[:state]
|
||||
# Alternative: use pet_state_id directly
|
||||
outfit.biology = { pet_state_id: params[:state] }
|
||||
end
|
||||
|
||||
# Set alt style if provided
|
||||
if params[:style]
|
||||
outfit.alt_style_id = params[:style].to_i
|
||||
end
|
||||
|
||||
outfit
|
||||
end
|
||||
|
||||
def find_authorized_outfit
|
||||
raise ActiveRecord::RecordNotFound unless user_signed_in?
|
||||
@outfit = current_user.outfits.find(params[:id])
|
||||
|
|
|
|||
|
|
@ -142,13 +142,6 @@ module ItemsHelper
|
|||
AUCTION_GENIE_URL_TEMPLATE.expand(auctiongenie: item.name).to_s
|
||||
end
|
||||
|
||||
LEBRON_URL_TEMPLATE = Addressable::Template.new(
|
||||
"https://stylisher.club/search/{name}"
|
||||
)
|
||||
def lebron_url_for(item)
|
||||
LEBRON_URL_TEMPLATE.expand(name: item.name).to_s
|
||||
end
|
||||
|
||||
def format_contribution_count(count)
|
||||
" (×#{count})".html_safe if count > 1
|
||||
end
|
||||
|
|
|
|||
|
|
@ -390,10 +390,6 @@ export function loadMovieLibrary(librarySrc, { preferArchive = false } = {}) {
|
|||
);
|
||||
}
|
||||
delete window.AdobeAn.compositions[compositionId];
|
||||
|
||||
// Install the MotionGuidePlugin, which is needed for motion path animations.
|
||||
window.createjs.MotionGuidePlugin.install();
|
||||
|
||||
const library = composition.getLibrary();
|
||||
|
||||
// One more loading step as part of loading this library is loading the
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Item < ApplicationRecord
|
|||
attr_writer :current_body_id, :owned, :wanted
|
||||
|
||||
NCRarities = [0, 500]
|
||||
PAINTBRUSH_SET_DESCRIPTION = 'This item is part of a deluxe paint brush set'
|
||||
PAINTBRUSH_SET_DESCRIPTION = 'This item is part of a deluxe paint brush set!'
|
||||
|
||||
scope :newest, -> {
|
||||
order(arel_table[:created_at].desc) if arel_table[:created_at]
|
||||
|
|
@ -162,7 +162,7 @@ class Item < ApplicationRecord
|
|||
end
|
||||
|
||||
def pb?
|
||||
I18n.with_locale(:en) { self.description.include?(PAINTBRUSH_SET_DESCRIPTION) }
|
||||
I18n.with_locale(:en) { self.description == PAINTBRUSH_SET_DESCRIPTION }
|
||||
end
|
||||
|
||||
def np?
|
||||
|
|
@ -444,34 +444,11 @@ class Item < ApplicationRecord
|
|||
created_at || Time.new(2010)
|
||||
end
|
||||
|
||||
# Returns the visible trades for this item, filtered by user visibility.
|
||||
# Accepts an optional scope to add additional query constraints (e.g., includes, order).
|
||||
def visible_trades(scope: nil, user: nil, remote_ip: nil)
|
||||
base = closet_hangers.trading.user_is_active
|
||||
base = base.merge(scope) if scope
|
||||
base.to_trades(user, remote_ip)
|
||||
end
|
||||
|
||||
def as_json(options={})
|
||||
result = super({
|
||||
super({
|
||||
only: [:id, :name, :description, :thumbnail_url, :rarity_index],
|
||||
methods: [:zones_restrict],
|
||||
}.merge(options))
|
||||
|
||||
if options[:include_trade_counts]
|
||||
trades = visible_trades(
|
||||
user: options[:current_user],
|
||||
remote_ip: options[:remote_ip]
|
||||
)
|
||||
result['num_trades_offering'] = trades[:offering].size
|
||||
result['num_trades_seeking'] = trades[:seeking].size
|
||||
end
|
||||
|
||||
if options[:include_nc_trade_value]
|
||||
result['nc_trade_value'] = nc_trade_value
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def compatible_body_ids(use_cached: true)
|
||||
|
|
|
|||
|
|
@ -172,67 +172,52 @@ class Outfit < ApplicationRecord
|
|||
def visible_layers
|
||||
return [] if pet_state.nil?
|
||||
|
||||
# Step 1: Choose biology layers - use alt style if present, otherwise pet state
|
||||
if alt_style
|
||||
biology_layers = alt_style.swf_assets.includes(:zone).to_a
|
||||
body = alt_style
|
||||
using_alt_style = true
|
||||
else
|
||||
biology_layers = pet_state.swf_assets.includes(:zone).to_a
|
||||
body = pet_type
|
||||
using_alt_style = false
|
||||
end
|
||||
# TODO: This method doesn't currently handle alt styles! If the outfit has
|
||||
# an alt_style, we should use its layers instead of pet_state layers, and
|
||||
# filter items to only those with body_id=0. This isn't needed yet because
|
||||
# this method is only used on item pages, which don't support alt styles.
|
||||
# See useOutfitAppearance.js for the complete logic including alt styles.
|
||||
item_appearances = item_appearances(swf_asset_includes: [:zone])
|
||||
|
||||
# Step 2: Load item appearances for the appropriate body
|
||||
item_appearances = Item.appearances_for(
|
||||
worn_items,
|
||||
body,
|
||||
swf_asset_includes: [:zone]
|
||||
).values
|
||||
pet_layers = pet_state.swf_assets.includes(:zone).to_a
|
||||
item_layers = item_appearances.map(&:swf_assets).flatten
|
||||
|
||||
# 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).
|
||||
pet_restricted_zone_ids = pet_layers.map(&:restricted_zone_ids).
|
||||
flatten.to_set
|
||||
item_restricted_zone_ids = item_appearances.
|
||||
map(&:restricted_zone_ids).flatten.to_set
|
||||
|
||||
# Rule 3a: When an item restricts a zone, it hides biology layers of the same zone.
|
||||
# When an item restricts a zone, it hides pet layers of the same zone.
|
||||
# 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
|
||||
# the same time. We don't enforce anything about that here, and
|
||||
# instead assume that the input by this point is valid!
|
||||
biology_layers.reject! { |sa| item_restricted_zone_ids.include?(sa.zone_id) }
|
||||
pet_layers.reject! { |sa| item_restricted_zone_ids.include?(sa.zone_id) }
|
||||
|
||||
# Rule 3b: When a biology appearance restricts a zone, or when the pet is
|
||||
# Unconverted, it makes body-specific items incompatible. We use this to
|
||||
# disallow UCs from wearing certain body-specific Biology Effects, Statics,
|
||||
# etc, while still allowing non-body-specific items in those zones! (I think
|
||||
# this happens for some Invisible pet stuff, too?)
|
||||
# When a pet appearance restricts a zone, or when the pet is Unconverted,
|
||||
# it makes body-specific items incompatible. We use this to disallow UCs
|
||||
# from wearing certain body-specific Biology Effects, Statics, etc, while
|
||||
# still allowing non-body-specific items in those zones! (I think this
|
||||
# happens for some Invisible pet stuff, too?)
|
||||
#
|
||||
# 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
|
||||
# showing up even in search results!
|
||||
#
|
||||
# NOTE: This can result in both biology layers and items occupying the same
|
||||
# NOTE: This can result in both pet layers and items occupying the same
|
||||
# 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
|
||||
# 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
|
||||
# rendering them by depth. Not great! 😅)
|
||||
#
|
||||
# NOTE: We used to also include the biology appearance's *occupied* zones in
|
||||
# NOTE: We used to also include the pet appearance's *occupied* zones in
|
||||
# this condition, not just the restricted zones, as a sensible
|
||||
# defensive default, even though we weren't aware of any relevant
|
||||
# items. But now we know that actually the "Bruce Brucey B Mouth"
|
||||
# occupies the real Mouth zone, and still should be visible and
|
||||
# above biology layers! So, we now only check *restricted* zones.
|
||||
# above pet layers! So, we now only check *restricted* zones.
|
||||
#
|
||||
# NOTE: UCs used to implement their restrictions by listing specific
|
||||
# zones, but it seems that the logic has changed to just be about
|
||||
|
|
@ -249,20 +234,18 @@ class Outfit < ApplicationRecord
|
|||
item_layers.reject! { |sa| sa.body_specific? }
|
||||
else
|
||||
item_layers.reject! { |sa| sa.body_specific? &&
|
||||
biology_restricted_zone_ids.include?(sa.zone_id) }
|
||||
pet_restricted_zone_ids.include?(sa.zone_id) }
|
||||
end
|
||||
|
||||
# Rule 3c: A biology appearance can also restrict its own zones. The Wraith
|
||||
# Uni is an interesting example: it has a horn, but its zone restrictions
|
||||
# hide it!
|
||||
biology_layers.reject! { |sa| biology_restricted_zone_ids.include?(sa.zone_id) }
|
||||
# A pet appearance can also restrict its own zones. The Wraith Uni is an
|
||||
# interesting example: it has a horn, but its zone restrictions hide it!
|
||||
pet_layers.reject! { |sa| pet_restricted_zone_ids.include?(sa.zone_id) }
|
||||
|
||||
# Step 4: Sort by depth and return
|
||||
(biology_layers + item_layers).sort_by(&:depth)
|
||||
(pet_layers + item_layers).sort_by(&:depth)
|
||||
end
|
||||
|
||||
def wardrobe_params
|
||||
params = {
|
||||
{
|
||||
name: name,
|
||||
color: color_id,
|
||||
species: species_id,
|
||||
|
|
@ -271,8 +254,6 @@ class Outfit < ApplicationRecord
|
|||
objects: worn_item_ids,
|
||||
closet: closeted_item_ids,
|
||||
}
|
||||
params[:style] = alt_style_id if alt_style_id.present?
|
||||
params
|
||||
end
|
||||
|
||||
def ensure_unique_name
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
# Pet::AutoModeling provides utilities for automatically modeling items on pet
|
||||
# bodies using the NC Mall preview API. This allows us to fetch appearance data
|
||||
# for items without needing a real pet of that type.
|
||||
#
|
||||
# The workflow:
|
||||
# 1. Generate a combined "SCI" (Species/Color Image hash) using NC Mall's
|
||||
# getPetData endpoint, which combines a pet type with items.
|
||||
# 2. Fetch the viewer data for that combined SCI using the CustomPets API.
|
||||
# 3. Process the viewer data to create SwfAsset records.
|
||||
module Pet::AutoModeling
|
||||
extend self
|
||||
|
||||
# Model an item on a specific body ID. This fetches the appearance data from
|
||||
# Neopets and creates/updates the SwfAsset records.
|
||||
#
|
||||
# @param item [Item] The item to model
|
||||
# @param body_id [Integer] The body ID to model on
|
||||
# @return [Symbol] Result status:
|
||||
# - :modeled - Successfully created SwfAsset records
|
||||
# - :not_compatible - Item is explicitly not compatible with this body
|
||||
# @raise [NoPetTypeForBody] If no PetType exists for this body_id
|
||||
# @raise [Neopets::NCMall::ResponseNotOK] On HTTP errors (transient for 5xx)
|
||||
# @raise [Neopets::NCMall::UnexpectedResponseFormat] On invalid response
|
||||
# @raise [Neopets::CustomPets::DownloadError] On AMF protocol errors
|
||||
def model_item_on_body(item, body_id)
|
||||
# Find a pet type with this body ID to use as a base
|
||||
pet_type = PetType.find_by(body_id: body_id)
|
||||
raise NoPetTypeForBody.new(body_id) if pet_type.nil?
|
||||
|
||||
# Fetch the viewer data for this item on this pet type
|
||||
new_image_hash = Neopets::NCMall.fetch_pet_data_sci(pet_type.image_hash, [item.id])
|
||||
viewer_data = Neopets::CustomPets.fetch_viewer_data("@#{new_image_hash}")
|
||||
|
||||
# If the item wasn't in the response, it's not compatible.
|
||||
object_info = viewer_data[:object_info_registry]&.to_h&.[](item.id.to_s)
|
||||
return :not_compatible if object_info.nil?
|
||||
|
||||
# Process the modeling data using the existing infrastructure
|
||||
snapshot = Pet::ModelingSnapshot.new(viewer_data)
|
||||
|
||||
# Save the pet type (may update image hash, etc.)
|
||||
snapshot.pet_type.save!
|
||||
|
||||
# Get the items from the snapshot and process them
|
||||
modeled_items = snapshot.items
|
||||
modeled_item = modeled_items.find { |i| i.id == item.id }
|
||||
|
||||
if modeled_item
|
||||
modeled_item.save!
|
||||
modeled_item.handle_assets!
|
||||
end
|
||||
|
||||
:modeled
|
||||
end
|
||||
|
||||
class NoPetTypeForBody < StandardError
|
||||
attr_reader :body_id
|
||||
def initialize(body_id)
|
||||
@body_id = body_id
|
||||
super("No PetType found for body_id=#{body_id}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -120,44 +120,6 @@ module Neopets::NCMall
|
|||
end
|
||||
end
|
||||
|
||||
# Generate a new image hash for a pet wearing specific items. Takes a base
|
||||
# pet sci (species/color image hash) and optional item IDs, and returns a
|
||||
# response containing the combined image hash in the :newsci field.
|
||||
# Use the returned hash with Neopets::CustomPets.fetch_viewer_data("@#{newsci}")
|
||||
# to get the full appearance data.
|
||||
PET_DATA_URL = "https://ncmall.neopets.com/mall/ajax/petview/getPetData.php"
|
||||
def self.fetch_pet_data_sci(pet_sci, item_ids = [])
|
||||
Sync do
|
||||
params = {"selPetsci" => pet_sci}
|
||||
item_ids.each { |id| params["itemsList[]"] = id.to_s }
|
||||
|
||||
DTIRequests.post(
|
||||
PET_DATA_URL,
|
||||
[["Content-Type", "application/x-www-form-urlencoded"]],
|
||||
params.to_query,
|
||||
) do |response|
|
||||
if response.status != 200
|
||||
raise ResponseNotOK.new(response.status),
|
||||
"expected status 200 but got #{response.status} (#{PET_DATA_URL})"
|
||||
end
|
||||
|
||||
begin
|
||||
data = JSON.parse(response.read)
|
||||
rescue JSON::ParserError
|
||||
raise UnexpectedResponseFormat,
|
||||
"failed to parse pet data response as JSON"
|
||||
end
|
||||
|
||||
unless data["newsci"].is_a?(String) && data["newsci"].present?
|
||||
raise UnexpectedResponseFormat,
|
||||
"missing or invalid field newsci in pet data response"
|
||||
end
|
||||
|
||||
data["newsci"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Map load_type from menu JSON to the v2 API type parameter.
|
||||
|
|
|
|||
|
|
@ -15,13 +15,10 @@
|
|||
= link_to t('items.show.resources.jn_items'), jn_items_url_for(item)
|
||||
= link_to t('items.show.resources.impress_2020'), impress_2020_url_for(item)
|
||||
- if item.nc_trade_value
|
||||
= link_to lebron_url_for(item),
|
||||
= link_to 'https://www.neopets.com/~lebron',
|
||||
title: nc_trade_value_updated_at_text(item.nc_trade_value) do
|
||||
= t 'items.show.resources.lebron_value',
|
||||
= t 'items.show.resources.lebron',
|
||||
value: nc_trade_value_estimate_text(item.nc_trade_value)
|
||||
- elsif item.nc?
|
||||
= link_to lebron_url_for(item) do
|
||||
= t 'items.show.resources.lebron'
|
||||
- unless item.nc?
|
||||
= link_to t('items.show.resources.shop_wizard'), shop_wizard_url_for(item)
|
||||
= link_to t('items.show.resources.trading_post'), trading_post_url_for(item)
|
||||
|
|
|
|||
27
bin/solargraph
Executable file
27
bin/solargraph
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# This file was generated by Bundler.
|
||||
#
|
||||
# The application 'solargraph' is installed as part of a gem, and
|
||||
# this file is here to facilitate running it.
|
||||
#
|
||||
|
||||
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
||||
|
||||
bundle_binstub = File.expand_path("bundle", __dir__)
|
||||
|
||||
if File.file?(bundle_binstub)
|
||||
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
||||
load(bundle_binstub)
|
||||
else
|
||||
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
||||
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
||||
end
|
||||
end
|
||||
|
||||
require "rubygems"
|
||||
require "bundler/setup"
|
||||
|
||||
load Gem.bin_path("solargraph", "solargraph")
|
||||
|
|
@ -310,8 +310,7 @@ en:
|
|||
resources:
|
||||
jn_items: JN Items
|
||||
impress_2020: DTI 2020
|
||||
lebron: Lebron
|
||||
lebron_value: "Lebron: %{value}"
|
||||
lebron: "Lebron: %{value}"
|
||||
shop_wizard: Shop Wizard
|
||||
trading_post: Trades
|
||||
auction_genie: Auctions
|
||||
|
|
|
|||
|
|
@ -191,7 +191,6 @@
|
|||
name:
|
||||
- libmysqlclient-dev
|
||||
- libyaml-dev
|
||||
- libvips-dev
|
||||
|
||||
- name: Create the app folder
|
||||
file:
|
||||
|
|
|
|||
|
|
@ -249,28 +249,6 @@ This crowdsourced approach is why DTI is "self-sustaining" - users passively con
|
|||
|
||||
See `app/models/pet/modeling_snapshot.rb` for the full implementation.
|
||||
|
||||
### Potential Upgrade: Auto-Modeling
|
||||
|
||||
We are currently not making full use of a recently-discovered Neopets feature: **we no longer need a real pet to model
|
||||
the item**. There's an NC Mall feature that supports previews of *any* item, not just those sold in the Mall.
|
||||
|
||||
See the `pets:load` task for implementation details.
|
||||
|
||||
We still need users to show us new items in the first place, to learn their item IDs and what body types they might
|
||||
fit. But once we have that, we can proactively attempt to model the pet on all relevant body types.
|
||||
|
||||
Let's pursue this in two steps:
|
||||
|
||||
1. [x] Create a backfill Rake task to attempt to load any models we suspect we need, based on the same logic as the
|
||||
`-is:modeled` item search filter.
|
||||
- Consider having this task auto-update the `modeling_status_hint` field on the item, if we've demonstrated that
|
||||
the item is almost certainly completely modeled, despite our heuristic indicating it is not. This will keep the
|
||||
`is:modeled` filter clean and approximately empty.
|
||||
- As part of this, let's refactor the logic out of `pets:load`, to more simply construct an "image hash" ("sci")
|
||||
from a pet type + items combination.
|
||||
2. [ ] Set this as a cron job to run very frequently, to quickly load in new items.
|
||||
- If we're able to reliably keep `is:modeled` basically empty, this could even be safe to run every, say, 2–5min.
|
||||
|
||||
### Cached Fields
|
||||
|
||||
To avoid expensive queries, several models cache computed data:
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
require "vips"
|
||||
|
||||
class OutfitImageRenderer
|
||||
CANVAS_SIZE = 600
|
||||
|
||||
def initialize(outfit)
|
||||
@outfit = outfit
|
||||
end
|
||||
|
||||
def render
|
||||
layers = @outfit.visible_layers
|
||||
|
||||
# Filter out layers without image URLs
|
||||
layers_with_images = layers.select(&:image_url?)
|
||||
|
||||
return nil if layers_with_images.empty?
|
||||
|
||||
# Fetch all layer images in parallel
|
||||
image_data_by_layer = fetch_layer_images(layers_with_images)
|
||||
|
||||
# Create transparent canvas in sRGB colorspace
|
||||
canvas = Vips::Image.black(CANVAS_SIZE, CANVAS_SIZE, bands: 4)
|
||||
canvas = canvas.new_from_image([0, 0, 0, 0])
|
||||
canvas = canvas.copy(interpretation: :srgb)
|
||||
|
||||
# Composite each layer onto the canvas
|
||||
layers_with_images.each do |layer|
|
||||
image_data = image_data_by_layer[layer]
|
||||
next unless image_data
|
||||
|
||||
begin
|
||||
layer_image = Vips::Image.new_from_buffer(image_data, "")
|
||||
|
||||
# Resize the layer to fit the canvas size
|
||||
# All layer images are square, but may not be CANVAS_SIZE x CANVAS_SIZE
|
||||
# We need to resize them to exactly CANVAS_SIZE x CANVAS_SIZE
|
||||
if layer_image.width != CANVAS_SIZE || layer_image.height != CANVAS_SIZE
|
||||
layer_image = layer_image.resize(
|
||||
CANVAS_SIZE.to_f / layer_image.width,
|
||||
vscale: CANVAS_SIZE.to_f / layer_image.height
|
||||
)
|
||||
end
|
||||
|
||||
# Composite this layer onto the canvas at (0, 0)
|
||||
# No offset needed since the layer is now exactly canvas-sized
|
||||
canvas = canvas.composite([layer_image], :over)
|
||||
rescue Vips::Error => e
|
||||
# Log and skip layers that fail to load/composite
|
||||
Rails.logger.warn "Failed to composite layer #{layer.id} (#{layer.image_url}): #{e.message}"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# Return PNG data
|
||||
canvas.write_to_buffer(".png")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_layer_images(layers)
|
||||
image_data_by_layer = {}
|
||||
|
||||
DTIRequests.load_many(max_at_once: 10) do |semaphore|
|
||||
layers.each do |layer|
|
||||
semaphore.async do
|
||||
begin
|
||||
response = DTIRequests.get(layer.image_url)
|
||||
if response.success?
|
||||
image_data_by_layer[layer] = response.read
|
||||
else
|
||||
Rails.logger.warn "Failed to fetch image for layer #{layer.id} (#{layer.image_url}): HTTP #{response.status}"
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.warn "Error fetching image for layer #{layer.id} (#{layer.image_url}): #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
image_data_by_layer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -30,17 +30,24 @@ module RocketAMFExtensions
|
|||
raise RocketAMF::AMFError.new(first_message_data)
|
||||
end
|
||||
|
||||
# HACK: Older items in Neopets' database have Windows-1250 encoding,
|
||||
# while newer items use proper UTF-8. We detect which encoding was used
|
||||
# by checking if the string is valid UTF-8, and only re-encode if needed.
|
||||
# HACK: It seems to me that these messages come back with Windows-1250
|
||||
# (or similar) encoding on the strings? I'm basing this on the
|
||||
# Patchwork Staff item, whose description arrives as:
|
||||
#
|
||||
# Example of Windows-1250 item: Patchwork Staff (57311), whose
|
||||
# description contains byte 0x96 (en-dash in Windows-1250).
|
||||
# "That staff is cute, but dont use it as a walking stick \x96 I " +
|
||||
# "dont think it will hold you up!"
|
||||
#
|
||||
# Example of UTF-8 item: Carnival Party Décor (80042), whose name
|
||||
# contains proper UTF-8 bytes [195, 169] for the é character.
|
||||
# And the `\x96` is meant to represent an endash, which it doesn't in
|
||||
# UTF-8 or in most extended ASCII encodings, but *does* in Windows's
|
||||
# specific extended ASCII.
|
||||
#
|
||||
# Idk if this is something to do with the AMFPHP spec or how the AMFPHP
|
||||
# server code they use serializes strings (I couldn't find any
|
||||
# reference to it?), or just their internal database encoding being
|
||||
# passed along as-is, or what? But this seems to be the most correct
|
||||
# interpretation I know how to do, so, let's do it!
|
||||
result.messages[0].data.body.tap do |body|
|
||||
reencode_strings_if_needed! body, "Windows-1250", "UTF-8"
|
||||
reencode_strings! body, "Windows-1250", "UTF-8"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -85,17 +92,13 @@ module RocketAMFExtensions
|
|||
end
|
||||
end
|
||||
|
||||
def reencode_strings_if_needed!(target, from, to)
|
||||
def reencode_strings!(target, from, to)
|
||||
if target.is_a? String
|
||||
# Only re-encode if the string is not valid UTF-8
|
||||
# (indicating it's in the old Windows-1250 encoding)
|
||||
unless target.valid_encoding?
|
||||
target.force_encoding(from).encode!(to)
|
||||
end
|
||||
elsif target.is_a? Array
|
||||
target.each { |x| reencode_strings_if_needed!(x, from, to) }
|
||||
target.each { |x| reencode_strings!(x, from, to) }
|
||||
elsif target.is_a? Hash
|
||||
target.values.each { |x| reencode_strings_if_needed!(x, from, to) }
|
||||
target.values.each { |x| reencode_strings!(x, from, to) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,90 +9,4 @@ namespace :items do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Auto-model items on missing body types using NC Mall preview API"
|
||||
task :auto_model, [:limit] => :environment do |task, args|
|
||||
limit = (args[:limit] || 100).to_i
|
||||
dry_run = ENV["DRY_RUN"] == "1"
|
||||
auto_hint = ENV["AUTO_HINT"] != "0"
|
||||
|
||||
puts "Auto-modeling up to #{limit} items#{dry_run ? ' (DRY RUN)' : ''}..."
|
||||
puts "Auto-hint: #{auto_hint ? 'enabled' : 'disabled'}"
|
||||
puts
|
||||
|
||||
# Find items that need modeling, newest first
|
||||
items = Item.is_not_modeled.order(created_at: :desc).limit(limit)
|
||||
puts "Found #{items.count} items to process"
|
||||
puts
|
||||
|
||||
items.each_with_index do |item, index|
|
||||
puts "[#{index + 1}/#{items.count}] Item ##{item.id}: #{item.name}"
|
||||
|
||||
missing_body_ids = item.predicted_missing_body_ids
|
||||
if missing_body_ids.empty?
|
||||
puts " ⚠️ No missing body IDs (item may already be fully modeled)"
|
||||
puts
|
||||
next
|
||||
end
|
||||
|
||||
puts " Missing #{missing_body_ids.size} body IDs: #{missing_body_ids.join(', ')}"
|
||||
|
||||
# Track results for this item
|
||||
results = {modeled: 0, not_compatible: 0, not_found: 0}
|
||||
had_transient_error = false
|
||||
|
||||
missing_body_ids.each do |body_id|
|
||||
if dry_run
|
||||
puts " Body #{body_id}: [DRY RUN] would attempt modeling"
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
result = Pet::AutoModeling.model_item_on_body(item, body_id)
|
||||
results[result] += 1
|
||||
|
||||
case result
|
||||
when :modeled
|
||||
puts " Body #{body_id}: ✅ Modeled successfully"
|
||||
when :not_compatible
|
||||
puts " Body #{body_id}: ❌ Not compatible (heuristic over-predicted)"
|
||||
end
|
||||
rescue Pet::AutoModeling::NoPetTypeForBody => e
|
||||
puts " Body #{body_id}: ⚠️ #{e.message}"
|
||||
rescue Neopets::NCMall::ResponseNotOK => e
|
||||
if e.status >= 500
|
||||
puts " Body #{body_id}: ⚠️ Server error (#{e.status}), will retry later"
|
||||
had_transient_error = true
|
||||
else
|
||||
puts " Body #{body_id}: ❌ HTTP error (#{e.status})"
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
rescue Neopets::NCMall::UnexpectedResponseFormat => e
|
||||
puts " Body #{body_id}: ❌ Unexpected response format: #{e.message}"
|
||||
Sentry.capture_exception(e)
|
||||
rescue Neopets::CustomPets::DownloadError => e
|
||||
puts " Body #{body_id}: ⚠️ AMF error: #{e.message}"
|
||||
had_transient_error = true
|
||||
end
|
||||
end
|
||||
|
||||
unless dry_run
|
||||
# Set hint if we've addressed all bodies without transient errors.
|
||||
# That way, if the item is not compatible with some bodies, we'll stop
|
||||
# trying to auto-model it.
|
||||
if auto_hint && !had_transient_error
|
||||
item.update!(modeling_status_hint: "done")
|
||||
puts " 📋 Set modeling_status_hint = 'done'"
|
||||
end
|
||||
end
|
||||
|
||||
puts " Summary: #{results[:modeled]} modeled, #{results[:not_compatible]} not compatible, #{results[:not_found]} not found"
|
||||
puts
|
||||
|
||||
# Be nice to Neopets API
|
||||
sleep 0.5 unless dry_run || index == items.count - 1
|
||||
end
|
||||
|
||||
puts "Done!"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,7 @@
|
|||
namespace :pets do
|
||||
desc "Load a pet's viewer data (by name or by color/species/items)"
|
||||
task :load, [:first] => [:environment] do |task, args|
|
||||
# Collect all arguments (first + extras)
|
||||
all_args = [args[:first]] + args.extras
|
||||
|
||||
# If only one argument, treat it as a pet name
|
||||
if all_args.length == 1
|
||||
viewer_data = Neopets::CustomPets.fetch_viewer_data(all_args[0])
|
||||
else
|
||||
# Multiple arguments: color, species, and optional item IDs
|
||||
color_name = all_args[0]
|
||||
species_name = all_args[1]
|
||||
item_ids = all_args[2..]
|
||||
|
||||
# Look up the PetType to use for the preview
|
||||
pet_type = PetType.matching_name(color_name, species_name).first!
|
||||
|
||||
# Convert it to an image hash for direct lookup
|
||||
new_image_hash = Neopets::NCMall.fetch_pet_data_sci(pet_type.image_hash, item_ids)
|
||||
pet_name = '@' + new_image_hash
|
||||
$stderr.puts "Loading pet #{pet_name}"
|
||||
|
||||
# Load the image hash as if it were a pet
|
||||
viewer_data = Neopets::CustomPets.fetch_viewer_data(pet_name)
|
||||
end
|
||||
|
||||
desc "Load a pet's viewer data"
|
||||
task :load, [:name] => [:environment] do |task, args|
|
||||
viewer_data = Neopets::CustomPets.fetch_viewer_data(args[:name])
|
||||
puts JSON.pretty_generate(viewer_data)
|
||||
end
|
||||
|
||||
|
|
|
|||
BIN
spec/fixtures/outfit_images/Blue Acara With Cape.png
vendored
BIN
spec/fixtures/outfit_images/Blue Acara With Cape.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
BIN
spec/fixtures/outfit_images/Blue Acara With Hat.png
vendored
BIN
spec/fixtures/outfit_images/Blue Acara With Hat.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
BIN
spec/fixtures/outfit_images/Blue Acara.png
vendored
BIN
spec/fixtures/outfit_images/Blue Acara.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
BIN
spec/fixtures/outfit_images/Cape.png
vendored
BIN
spec/fixtures/outfit_images/Cape.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
BIN
spec/fixtures/outfit_images/Hat.png
vendored
BIN
spec/fixtures/outfit_images/Hat.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
54
spec/fixtures/zones.yml
vendored
54
spec/fixtures/zones.yml
vendored
|
|
@ -28,9 +28,9 @@ hindbiology:
|
|||
type_id: 1
|
||||
label: Hind Biology
|
||||
plain_label: hindbiology
|
||||
markings1:
|
||||
id: 6
|
||||
depth: 8
|
||||
markings:
|
||||
id: 31
|
||||
depth: 35
|
||||
type_id: 2
|
||||
label: Markings
|
||||
plain_label: markings
|
||||
|
|
@ -88,12 +88,6 @@ body:
|
|||
type_id: 1
|
||||
label: Body
|
||||
plain_label: body
|
||||
markings2:
|
||||
id: 16
|
||||
depth: 19
|
||||
type_id: 2
|
||||
label: Markings
|
||||
plain_label: markings
|
||||
bodydisease:
|
||||
id: 17
|
||||
depth: 20
|
||||
|
|
@ -178,12 +172,6 @@ head:
|
|||
type_id: 1
|
||||
label: Head
|
||||
plain_label: head
|
||||
markings3:
|
||||
id: 31
|
||||
depth: 35
|
||||
type_id: 2
|
||||
label: Markings
|
||||
plain_label: markings
|
||||
headdisease:
|
||||
id: 32
|
||||
depth: 36
|
||||
|
|
@ -208,9 +196,9 @@ glasses:
|
|||
type_id: 2
|
||||
label: Glasses
|
||||
plain_label: glasses
|
||||
earrings1:
|
||||
id: 36
|
||||
depth: 39
|
||||
earrings:
|
||||
id: 41
|
||||
depth: 45
|
||||
type_id: 2
|
||||
label: Earrings
|
||||
plain_label: earrings
|
||||
|
|
@ -232,21 +220,15 @@ headdrippings:
|
|||
type_id: 1
|
||||
label: Head Drippings
|
||||
plain_label: headdrippings
|
||||
hat1:
|
||||
id: 40
|
||||
depth: 44
|
||||
hat:
|
||||
id: 50
|
||||
depth: 16
|
||||
type_id: 2
|
||||
label: Hat
|
||||
plain_label: hat
|
||||
earrings2:
|
||||
id: 41
|
||||
depth: 45
|
||||
type_id: 2
|
||||
label: Earrings
|
||||
plain_label: earrings
|
||||
righthanditem1:
|
||||
id: 42
|
||||
depth: 46
|
||||
righthanditem:
|
||||
id: 49
|
||||
depth: 5
|
||||
type_id: 2
|
||||
label: Right-hand Item
|
||||
plain_label: righthanditem
|
||||
|
|
@ -286,18 +268,6 @@ backgrounditem:
|
|||
type_id: 3
|
||||
label: Background Item
|
||||
plain_label: backgrounditem
|
||||
righthanditem2:
|
||||
id: 49
|
||||
depth: 5
|
||||
type_id: 2
|
||||
label: Right-hand Item
|
||||
plain_label: righthanditem
|
||||
hat2:
|
||||
id: 50
|
||||
depth: 16
|
||||
type_id: 2
|
||||
label: Hat
|
||||
plain_label: hat
|
||||
belt:
|
||||
id: 51
|
||||
depth: 27
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
require 'webmock/rspec'
|
||||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe OutfitImageRenderer do
|
||||
fixtures :zones, :colors, :species
|
||||
|
||||
# Helper to load a fixture image
|
||||
def load_fixture_image(filename)
|
||||
path = Rails.root.join('spec', 'fixtures', 'outfit_images', filename)
|
||||
File.read(path)
|
||||
end
|
||||
|
||||
# Helper to create a pet state with specific swf_assets
|
||||
def build_pet_state(pet_type, pose: "HAPPY_FEM", swf_assets: [])
|
||||
pet_state = PetState.create!(
|
||||
pet_type: pet_type,
|
||||
pose: pose,
|
||||
swf_asset_ids: swf_assets.empty? ? [0] : swf_assets.map(&:id)
|
||||
)
|
||||
pet_state.swf_assets = swf_assets
|
||||
pet_state
|
||||
end
|
||||
|
||||
# Helper to create a SwfAsset for biology (pet layers)
|
||||
def build_biology_asset(zone, body_id:)
|
||||
@remote_id = (@remote_id || 0) + 1
|
||||
SwfAsset.create!(
|
||||
type: "biology",
|
||||
remote_id: @remote_id,
|
||||
url: "https://images.neopets.example/biology_#{@remote_id}.swf",
|
||||
zone: zone,
|
||||
body_id: body_id,
|
||||
zones_restrict: "",
|
||||
has_image: true
|
||||
)
|
||||
end
|
||||
|
||||
# Helper to create a SwfAsset for items (object layers)
|
||||
def build_item_asset(zone, body_id:)
|
||||
@remote_id = (@remote_id || 0) + 1
|
||||
SwfAsset.create!(
|
||||
type: "object",
|
||||
remote_id: @remote_id,
|
||||
url: "https://images.neopets.example/object_#{@remote_id}.swf",
|
||||
zone: zone,
|
||||
body_id: body_id,
|
||||
zones_restrict: "",
|
||||
has_image: true
|
||||
)
|
||||
end
|
||||
|
||||
# Helper to create an item with specific swf_assets
|
||||
def build_item(name, swf_assets: [])
|
||||
item = Item.create!(
|
||||
name: name,
|
||||
description: "Test item",
|
||||
thumbnail_url: "https://images.neopets.example/thumbnail.png",
|
||||
rarity: "Common",
|
||||
price: 100,
|
||||
zones_restrict: "",
|
||||
species_support_ids: ""
|
||||
)
|
||||
swf_assets.each do |asset|
|
||||
ParentSwfAssetRelationship.create!(
|
||||
parent: item,
|
||||
swf_asset: asset
|
||||
)
|
||||
end
|
||||
item
|
||||
end
|
||||
|
||||
before do
|
||||
PetType.destroy_all
|
||||
@pet_type = PetType.create!(
|
||||
species: species(:acara),
|
||||
color: colors(:blue),
|
||||
body_id: 1,
|
||||
created_at: Time.new(2005)
|
||||
)
|
||||
end
|
||||
|
||||
describe "#render" do
|
||||
context "with a simple outfit" do
|
||||
it "composites biology and item layers into a single PNG" do
|
||||
# Load fixture images
|
||||
acara_png = load_fixture_image('Blue Acara.png')
|
||||
hat_png = load_fixture_image('Hat.png')
|
||||
expected_composite_png = load_fixture_image('Blue Acara With Hat.png')
|
||||
|
||||
# Create biology and item assets
|
||||
biology_asset = build_biology_asset(zones(:head), body_id: 1)
|
||||
item_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||
|
||||
# Stub HTTP requests for the actual image URLs that will be generated
|
||||
stub_request(:get, biology_asset.image_url).
|
||||
to_return(body: acara_png, status: 200)
|
||||
stub_request(:get, item_asset.image_url).
|
||||
to_return(body: hat_png, status: 200)
|
||||
|
||||
# Build outfit
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
|
||||
item = build_item("Test Hat", swf_assets: [item_asset])
|
||||
outfit = Outfit.new(
|
||||
pet_state: pet_state,
|
||||
worn_items: [item]
|
||||
)
|
||||
|
||||
# Render
|
||||
renderer = OutfitImageRenderer.new(outfit)
|
||||
result = renderer.render
|
||||
|
||||
# Verify we got PNG data back
|
||||
expect(result).not_to be_nil
|
||||
expect(result).to be_a(String)
|
||||
expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b) # PNG magic bytes
|
||||
|
||||
# Verify the result is a valid 600x600 PNG
|
||||
result_image = Vips::Image.new_from_buffer(result, "")
|
||||
expect(result_image.width).to eq(600)
|
||||
expect(result_image.height).to eq(600)
|
||||
|
||||
# Verify the composite matches the expected image pixel-perfectly
|
||||
expected_image = Vips::Image.new_from_buffer(expected_composite_png, "")
|
||||
|
||||
# Calculate the absolute difference between images
|
||||
diff = (result_image - expected_image).abs
|
||||
max_diff = diff.max
|
||||
|
||||
# Allow a small tolerance for minor encoding/compositing differences
|
||||
# The expected image was generated with a different method, so we expect
|
||||
# very close but not necessarily pixel-perfect matches
|
||||
tolerance = 2
|
||||
if max_diff > tolerance
|
||||
debug_path = Rails.root.join('tmp', 'test_render_result.png')
|
||||
result_image.write_to_file(debug_path.to_s)
|
||||
fail "Images should match within tolerance of #{tolerance}, but found max difference of #{max_diff}. Actual output saved to #{debug_path}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when a layer image fails to load" do
|
||||
it "skips the failed layer and continues" do
|
||||
hat_png = load_fixture_image('Hat.png')
|
||||
|
||||
biology_asset = build_biology_asset(zones(:head), body_id: 1)
|
||||
item_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||
|
||||
# Stub one successful request and one failure
|
||||
stub_request(:get, biology_asset.image_url).
|
||||
to_return(status: 404)
|
||||
stub_request(:get, item_asset.image_url).
|
||||
to_return(body: hat_png, status: 200)
|
||||
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
|
||||
item = build_item("Test Hat", swf_assets: [item_asset])
|
||||
outfit = Outfit.new(
|
||||
pet_state: pet_state,
|
||||
worn_items: [item]
|
||||
)
|
||||
|
||||
renderer = OutfitImageRenderer.new(outfit)
|
||||
result = renderer.render
|
||||
|
||||
# Should still render successfully with just the one layer
|
||||
expect(result).not_to be_nil
|
||||
expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no layers have images" do
|
||||
it "returns nil" do
|
||||
# Create an asset but stub image_url to return nil
|
||||
biology_asset = build_biology_asset(zones(:head), body_id: 1)
|
||||
allow_any_instance_of(SwfAsset).to receive(:image_url?).and_return(false)
|
||||
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
|
||||
renderer = OutfitImageRenderer.new(outfit)
|
||||
result = renderer.render
|
||||
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "resizes all layers to 600x600 before compositing" do
|
||||
# Load a 1200x1200 item layer (real-world case from Neopets)
|
||||
item_1200_png = load_fixture_image('Cape.png')
|
||||
acara_600_png = load_fixture_image('Blue Acara.png')
|
||||
expected_composite_png = load_fixture_image('Blue Acara With Cape.png')
|
||||
|
||||
# Create assets
|
||||
biology_asset = build_biology_asset(zones(:head), body_id: 1)
|
||||
item_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||
|
||||
# Stub HTTP requests
|
||||
stub_request(:get, biology_asset.image_url).
|
||||
to_return(body: acara_600_png, status: 200)
|
||||
stub_request(:get, item_asset.image_url).
|
||||
to_return(body: item_1200_png, status: 200)
|
||||
|
||||
# Build outfit
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
|
||||
item = build_item("Test Item", swf_assets: [item_asset])
|
||||
outfit = Outfit.new(
|
||||
pet_state: pet_state,
|
||||
worn_items: [item]
|
||||
)
|
||||
|
||||
# Render
|
||||
renderer = OutfitImageRenderer.new(outfit)
|
||||
result = renderer.render
|
||||
|
||||
# Verify we got valid PNG data
|
||||
expect(result).not_to be_nil
|
||||
expect(result).to be_a(String)
|
||||
expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b)
|
||||
|
||||
# Verify the result is exactly 600x600
|
||||
result_image = Vips::Image.new_from_buffer(result, "")
|
||||
expect(result_image.width).to eq(600)
|
||||
expect(result_image.height).to eq(600)
|
||||
|
||||
# Verify the composite matches the expected image pixel-perfectly
|
||||
expected_image = Vips::Image.new_from_buffer(expected_composite_png, "")
|
||||
|
||||
# Calculate the absolute difference between images
|
||||
diff = (result_image - expected_image).abs
|
||||
max_diff = diff.max
|
||||
|
||||
# Allow a small tolerance for minor encoding/compositing differences
|
||||
tolerance = 2
|
||||
if max_diff > tolerance
|
||||
debug_path = Rails.root.join('tmp', 'test_render_1200_result.png')
|
||||
result_image.write_to_file(debug_path.to_s)
|
||||
fail "Images should match within tolerance of #{tolerance}, but found max difference of #{max_diff}. Actual output saved to #{debug_path}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe Outfit do
|
||||
fixtures :zones, :colors, :species
|
||||
fixtures :colors, :species, :zones
|
||||
|
||||
let(:blue) { colors(:blue) }
|
||||
let(:acara) { species(:acara) }
|
||||
|
|
@ -54,7 +54,7 @@ RSpec.describe Outfit do
|
|||
|
||||
describe "Item::Appearance#compatible_with?" do
|
||||
it "returns true for items in different zones with no restrictions" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
shirt = create_item("Shirt", zones(:shirtdress))
|
||||
|
||||
appearances = Item.appearances_for([hat, shirt], @pet_type)
|
||||
|
|
@ -66,8 +66,8 @@ RSpec.describe Outfit do
|
|||
end
|
||||
|
||||
it "returns false for items in the same zone" do
|
||||
hat1 = create_item("Hat 1", zones(:hat1))
|
||||
hat2 = create_item("Hat 2", zones(:hat1))
|
||||
hat1 = create_item("Hat 1", zones(:hat))
|
||||
hat2 = create_item("Hat 2", zones(:hat))
|
||||
|
||||
appearances = Item.appearances_for([hat1, hat2], @pet_type)
|
||||
hat1_appearance = appearances[hat1.id]
|
||||
|
|
@ -89,7 +89,7 @@ RSpec.describe Outfit do
|
|||
zones_restrict_array[28] = "1" # Set bit for zone 29
|
||||
zones_restrict = zones_restrict_array.join
|
||||
|
||||
restricting_hat = create_item("Restricting Hat", zones(:hat1), zones_restrict: zones_restrict)
|
||||
restricting_hat = create_item("Restricting Hat", zones(:hat), zones_restrict: zones_restrict)
|
||||
|
||||
# Create an item in the ruff zone
|
||||
ruff_item = create_item("Ruff Item", zones(:ruff))
|
||||
|
|
@ -104,7 +104,7 @@ RSpec.describe Outfit do
|
|||
|
||||
it "returns true for empty appearances" do
|
||||
# Create items that don't fit the current pet (wrong body_id)
|
||||
hat = create_item("Hat", zones(:hat1), body_id: 999)
|
||||
hat = create_item("Hat", zones(:hat), body_id: 999)
|
||||
shirt = create_item("Shirt", zones(:shirtdress), body_id: 999)
|
||||
|
||||
appearances = Item.appearances_for([hat, shirt], @pet_type)
|
||||
|
|
@ -122,7 +122,7 @@ RSpec.describe Outfit do
|
|||
|
||||
describe "#without_item" do
|
||||
it "returns a new outfit without the given item" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
outfit_with_hat = @outfit.with_item(hat)
|
||||
|
||||
new_outfit = outfit_with_hat.without_item(hat)
|
||||
|
|
@ -132,7 +132,7 @@ RSpec.describe Outfit do
|
|||
end
|
||||
|
||||
it "returns a new outfit instance (immutable)" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
outfit_with_hat = @outfit.with_item(hat)
|
||||
|
||||
new_outfit = outfit_with_hat.without_item(hat)
|
||||
|
|
@ -142,7 +142,7 @@ RSpec.describe Outfit do
|
|||
end
|
||||
|
||||
it "does nothing if the item is not worn" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
|
||||
new_outfit = @outfit.without_item(hat)
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ RSpec.describe Outfit do
|
|||
|
||||
describe "#with_item" do
|
||||
it "adds an item when there are no conflicts" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
|
||||
new_outfit = @outfit.with_item(hat)
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ RSpec.describe Outfit do
|
|||
end
|
||||
|
||||
it "returns a new outfit instance (immutable)" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
|
||||
new_outfit = @outfit.with_item(hat)
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ RSpec.describe Outfit do
|
|||
end
|
||||
|
||||
it "is idempotent (adding same item twice has no effect)" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
|
||||
outfit1 = @outfit.with_item(hat)
|
||||
outfit2 = outfit1.with_item(hat)
|
||||
|
|
@ -182,7 +182,7 @@ RSpec.describe Outfit do
|
|||
|
||||
it "does not add items that don't fit this pet" do
|
||||
# Create item with wrong body_id
|
||||
hat = create_item("Hat", zones(:hat1), body_id: 999)
|
||||
hat = create_item("Hat", zones(:hat), body_id: 999)
|
||||
|
||||
new_outfit = @outfit.with_item(hat)
|
||||
|
||||
|
|
@ -191,8 +191,8 @@ RSpec.describe Outfit do
|
|||
|
||||
context "with conflicting items" do
|
||||
it "moves conflicting item to closet when items occupy the same zone" do
|
||||
hat1 = create_item("Hat 1", zones(:hat1))
|
||||
hat2 = create_item("Hat 2", zones(:hat1))
|
||||
hat1 = create_item("Hat 1", zones(:hat))
|
||||
hat2 = create_item("Hat 2", zones(:hat))
|
||||
|
||||
outfit_with_hat1 = @outfit.with_item(hat1)
|
||||
outfit_with_hat2 = outfit_with_hat1.with_item(hat2)
|
||||
|
|
@ -211,7 +211,7 @@ RSpec.describe Outfit do
|
|||
zones_restrict_array = Array.new(52, "0")
|
||||
zones_restrict_array[28] = "1"
|
||||
zones_restrict = zones_restrict_array.join
|
||||
restricting_hat = create_item("Restricting Hat", zones(:hat1), zones_restrict: zones_restrict)
|
||||
restricting_hat = create_item("Restricting Hat", zones(:hat), zones_restrict: zones_restrict)
|
||||
|
||||
# First wear ruff item, then wear restricting hat
|
||||
outfit_with_ruff = @outfit.with_item(ruff_item)
|
||||
|
|
@ -223,7 +223,7 @@ RSpec.describe Outfit do
|
|||
end
|
||||
|
||||
it "keeps compatible items when adding new item" do
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
shirt = create_item("Shirt", zones(:shirtdress))
|
||||
pants = create_item("Pants", zones(:trousers))
|
||||
|
||||
|
|
@ -235,9 +235,9 @@ RSpec.describe Outfit do
|
|||
end
|
||||
|
||||
it "can move multiple conflicting items to closet" do
|
||||
hat1 = create_item("Hat 1", zones(:hat1))
|
||||
hat2 = create_item("Hat 2", zones(:hat1))
|
||||
hat3 = create_item("Hat 3", zones(:hat1))
|
||||
hat1 = create_item("Hat 1", zones(:hat))
|
||||
hat2 = create_item("Hat 2", zones(:hat))
|
||||
hat3 = create_item("Hat 3", zones(:hat))
|
||||
|
||||
# Wear hat1 and hat2 by manually building the outfit
|
||||
# (normally you can't, but we're testing the conflict resolution)
|
||||
|
|
@ -253,8 +253,8 @@ RSpec.describe Outfit do
|
|||
end
|
||||
|
||||
it "does not duplicate items in closet if already closeted" do
|
||||
hat1 = create_item("Hat 1", zones(:hat1))
|
||||
hat2 = create_item("Hat 2", zones(:hat1))
|
||||
hat1 = create_item("Hat 1", zones(:hat))
|
||||
hat2 = create_item("Hat 2", zones(:hat))
|
||||
|
||||
# Wear hat1
|
||||
outfit1 = @outfit.with_item(hat1)
|
||||
|
|
@ -278,552 +278,11 @@ RSpec.describe Outfit do
|
|||
it "works with outfit that has no pet_state" do
|
||||
# This shouldn't happen in practice, but let's be defensive
|
||||
outfit_no_pet = Outfit.new
|
||||
hat = create_item("Hat", zones(:hat1))
|
||||
hat = create_item("Hat", zones(:hat))
|
||||
|
||||
# Should not crash, but also won't add the item
|
||||
expect { outfit_no_pet.with_item(hat) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to create a pet state with specific swf_assets
|
||||
def build_pet_state(pet_type, pose: "HAPPY_FEM", swf_assets: [])
|
||||
pet_state = PetState.create!(
|
||||
pet_type: pet_type,
|
||||
pose: pose,
|
||||
swf_asset_ids: swf_assets.empty? ? [0] : swf_assets.map(&:id)
|
||||
)
|
||||
pet_state.swf_assets = swf_assets
|
||||
pet_state
|
||||
end
|
||||
|
||||
# Helper to create a SwfAsset for biology (pet layers)
|
||||
def build_biology_asset(zone, body_id:, zones_restrict: "")
|
||||
@remote_id = (@remote_id || 0) + 1
|
||||
SwfAsset.create!(
|
||||
type: "biology",
|
||||
remote_id: @remote_id,
|
||||
url: "https://images.neopets.example/biology_#{@remote_id}.swf",
|
||||
zone: zone,
|
||||
body_id: body_id,
|
||||
zones_restrict: zones_restrict
|
||||
)
|
||||
end
|
||||
|
||||
# Helper to create a SwfAsset for items (object layers)
|
||||
def build_item_asset(zone, body_id:, zones_restrict: "")
|
||||
@remote_id = (@remote_id || 0) + 1
|
||||
SwfAsset.create!(
|
||||
type: "object",
|
||||
remote_id: @remote_id,
|
||||
url: "https://images.neopets.example/object_#{@remote_id}.swf",
|
||||
zone: zone,
|
||||
body_id: body_id,
|
||||
zones_restrict: zones_restrict
|
||||
)
|
||||
end
|
||||
|
||||
# Helper to create an item with specific swf_assets
|
||||
def build_item(name, swf_assets: [])
|
||||
item = Item.create!(
|
||||
name: name,
|
||||
description: "Test item",
|
||||
thumbnail_url: "https://images.neopets.example/thumbnail.png",
|
||||
rarity: "Common",
|
||||
price: 100,
|
||||
zones_restrict: "",
|
||||
species_support_ids: ""
|
||||
)
|
||||
swf_assets.each do |asset|
|
||||
ParentSwfAssetRelationship.create!(
|
||||
parent: item,
|
||||
swf_asset: asset
|
||||
)
|
||||
end
|
||||
item
|
||||
end
|
||||
|
||||
describe "#visible_layers" do
|
||||
before do
|
||||
# Clean up any existing pet types to avoid conflicts
|
||||
PetType.destroy_all
|
||||
|
||||
# Create a basic pet type for testing
|
||||
@pet_type = PetType.create!(
|
||||
species: species(:acara),
|
||||
color: colors(:blue),
|
||||
body_id: 1,
|
||||
created_at: Time.new(2005)
|
||||
)
|
||||
end
|
||||
|
||||
context "basic layer composition" do
|
||||
it "returns pet layers when no items are worn" do
|
||||
# Create biology assets for the pet
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
body = build_biology_asset(zones(:body), body_id: 1)
|
||||
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head, body])
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
expect(layers).to contain_exactly(head, body)
|
||||
end
|
||||
|
||||
it "returns pet layers and item layers when items are worn" do
|
||||
# Create pet layers
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
body = build_biology_asset(zones(:body), body_id: 1)
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head, body])
|
||||
|
||||
# Create item layers
|
||||
hat_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||
hat = build_item("Test Hat", swf_assets: [hat_asset])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [hat]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
expect(layers).to contain_exactly(head, body, hat_asset)
|
||||
end
|
||||
|
||||
it "includes body_id=0 items that fit all pets" do
|
||||
# Create pet layers
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head])
|
||||
|
||||
# Create a background item (body_id=0, fits all)
|
||||
bg_asset = build_item_asset(zones(:background), body_id: 0)
|
||||
background = build_item("Test Background", swf_assets: [bg_asset])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [background]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
expect(layers).to contain_exactly(head, bg_asset)
|
||||
end
|
||||
end
|
||||
|
||||
context "items restricting pet layers (Rule 3a)" do
|
||||
it "hides pet layers in zones that items restrict" do
|
||||
# Create pet layers including hair
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
hair = build_biology_asset(zones(:hairfront), body_id: 1)
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head, hair])
|
||||
|
||||
# Create a hat that restricts the hair zone
|
||||
# zones_restrict is a bitfield where position 37 (Hair Front zone id) is "1"
|
||||
zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1
|
||||
hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict)
|
||||
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [hat]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# Hair should be hidden, but head and hat should be visible
|
||||
expect(layers).to contain_exactly(head, hat_asset)
|
||||
expect(layers).not_to include(hair)
|
||||
end
|
||||
|
||||
it "hides multiple pet layers when item restricts multiple zones" do
|
||||
# Create pet layers
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
hair_front = build_biology_asset(zones(:hairfront), body_id: 1)
|
||||
head_transient = build_biology_asset(zones(:headtransientbiology), body_id: 1)
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head, hair_front, head_transient])
|
||||
|
||||
# Create an item that restricts both Hair Front (37) and Head Transient Biology (38)
|
||||
zones_restrict = "0" * 36 + "11" + "0" * 20 # bits 37 and 38 = 1
|
||||
hood_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict)
|
||||
hood = build_item("Agent Hood", swf_assets: [hood_asset])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [hood]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# Both hair_front and head_transient should be hidden
|
||||
expect(layers).to contain_exactly(head, hood_asset)
|
||||
expect(layers).not_to include(hair_front, head_transient)
|
||||
end
|
||||
end
|
||||
|
||||
context "pets restricting body-specific item layers (Rule 3b)" do
|
||||
it "hides body-specific items in zones the pet restricts" do
|
||||
# Create a pet with a layer that restricts the Static zone (46)
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
zones_restrict = "0" * 45 + "1" + "0" * 10 # bit 46 = 1
|
||||
restricting_layer = build_biology_asset(zones(:body), body_id: 1, zones_restrict: zones_restrict)
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head, restricting_layer])
|
||||
|
||||
# Create a body-specific Static item
|
||||
static_asset = build_item_asset(zones(:static), body_id: 1)
|
||||
static_item = build_item("Body-specific Static", swf_assets: [static_asset])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [static_item]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# The body-specific static item should be hidden
|
||||
expect(layers).to contain_exactly(head, restricting_layer)
|
||||
expect(layers).not_to include(static_asset)
|
||||
end
|
||||
|
||||
it "allows body_id=0 items even in zones the pet restricts" do
|
||||
# Create a pet with a layer that restricts the Background Item zone (48)
|
||||
# Background Item is type_id 3 (universal zone), so body_id=0 items should always work
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
zones_restrict = "0" * 47 + "1" + "0" * 10 # bit 48 = 1
|
||||
restricting_layer = build_biology_asset(zones(:body), body_id: 1, zones_restrict: zones_restrict)
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head, restricting_layer])
|
||||
|
||||
# Create a body_id=0 Background Item (fits all bodies, universal zone)
|
||||
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)
|
||||
outfit.worn_items = [bg_item]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# The body_id=0 item should be visible even though the zone is restricted
|
||||
expect(layers).to contain_exactly(head, restricting_layer, bg_item_asset)
|
||||
end
|
||||
end
|
||||
|
||||
context "UNCONVERTED pets (Rule 3b special case)" do
|
||||
it "rejects all body-specific items" do
|
||||
# Create an UNCONVERTED pet
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
body = build_biology_asset(zones(:body), body_id: 1)
|
||||
pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head, body])
|
||||
|
||||
# Create both body-specific and body_id=0 items
|
||||
body_specific_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||
body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset])
|
||||
|
||||
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)
|
||||
outfit.worn_items = [body_specific_item, universal_item]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# Only body_id=0 items should be visible
|
||||
expect(layers).to contain_exactly(head, body, universal_asset)
|
||||
expect(layers).not_to include(body_specific_asset)
|
||||
end
|
||||
|
||||
it "rejects body-specific items regardless of zone restrictions" do
|
||||
# Create an UNCONVERTED pet with no zone restrictions
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head])
|
||||
|
||||
# Create a body-specific item in a zone the pet doesn't restrict
|
||||
hat_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||
hat = build_item("Body-specific Hat", swf_assets: [hat_asset])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [hat]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# The body-specific item should still be hidden
|
||||
expect(layers).to contain_exactly(head)
|
||||
expect(layers).not_to include(hat_asset)
|
||||
end
|
||||
end
|
||||
|
||||
context "pets restricting their own layers (Rule 3c)" do
|
||||
it "hides pet layers in zones the pet itself restricts" do
|
||||
# Create a pet with a horn asset and a layer that restricts the horn's zone
|
||||
# (Simulating the Wraith Uni case)
|
||||
body = build_biology_asset(zones(:body), body_id: 1)
|
||||
|
||||
# Create a horn in the Head Transient Biology zone (38)
|
||||
horn = build_biology_asset(zones(:headtransientbiology), body_id: 1)
|
||||
|
||||
# Create a layer that restricts zone 38
|
||||
zones_restrict = "0" * 37 + "1" + "0" * 20 # bit 38 = 1
|
||||
restricting_layer = build_biology_asset(zones(:head), body_id: 1, zones_restrict: zones_restrict)
|
||||
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [body, horn, restricting_layer])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# The horn should be hidden by the pet's own restrictions
|
||||
expect(layers).to contain_exactly(body, restricting_layer)
|
||||
expect(layers).not_to include(horn)
|
||||
end
|
||||
|
||||
it "applies self-restrictions in combination with item restrictions" do
|
||||
# Create a pet with multiple layers, some restricted by itself
|
||||
body = build_biology_asset(zones(:body), body_id: 1)
|
||||
hair = build_biology_asset(zones(:hairfront), body_id: 1)
|
||||
|
||||
# Pet restricts its own Head zone (30)
|
||||
zones_restrict = "0" * 29 + "1" + "0" * 25 # bit 30 = 1
|
||||
head = build_biology_asset(zones(:head), body_id: 1)
|
||||
restricting_layer = build_biology_asset(zones(:eyes), body_id: 1, zones_restrict: zones_restrict)
|
||||
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [body, hair, head, restricting_layer])
|
||||
|
||||
# Add an item that restricts Hair Front (37)
|
||||
item_zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1
|
||||
hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: item_zones_restrict)
|
||||
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [hat]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# Hair should be hidden by item, Head should be hidden by pet's own restrictions
|
||||
expect(layers).to contain_exactly(body, restricting_layer, hat_asset)
|
||||
expect(layers).not_to include(hair, head)
|
||||
end
|
||||
end
|
||||
|
||||
context "depth sorting and layer ordering" do
|
||||
it "sorts layers by zone depth" do
|
||||
# Create layers in various zones with different depths
|
||||
background = build_biology_asset(zones(:background), body_id: 1) # depth 3
|
||||
body_layer = build_biology_asset(zones(:body), body_id: 1) # depth 18
|
||||
head_layer = build_biology_asset(zones(:head), body_id: 1) # depth 34
|
||||
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head_layer, background, body_layer])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# Should be sorted by depth: background (3) < body (18) < head (34)
|
||||
expect(layers[0]).to eq(background)
|
||||
expect(layers[1]).to eq(body_layer)
|
||||
expect(layers[2]).to eq(head_layer)
|
||||
end
|
||||
|
||||
it "places item layers after pet layers at the same depth" do
|
||||
# Create a pet layer and item layer in zones with the same depth
|
||||
# Static zone has depth 48
|
||||
pet_static = build_biology_asset(zones(:static), body_id: 1)
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [pet_static])
|
||||
|
||||
item_static = build_item_asset(zones(:static), body_id: 0)
|
||||
static_item = build_item("Static Item", swf_assets: [item_static])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [static_item]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# Both should be present, with item layer last (on top)
|
||||
expect(layers).to eq([pet_static, item_static])
|
||||
end
|
||||
|
||||
it "sorts complex outfits correctly by depth" do
|
||||
# Create a complex outfit with multiple pet and item layers
|
||||
background = build_biology_asset(zones(:background), body_id: 1) # depth 3
|
||||
body_layer = build_biology_asset(zones(:body), body_id: 1) # depth 18
|
||||
head_layer = build_biology_asset(zones(:head), body_id: 1) # depth 34
|
||||
|
||||
pet_state = build_pet_state(@pet_type, swf_assets: [head_layer, background, body_layer])
|
||||
|
||||
# Add items at various depths
|
||||
bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4
|
||||
hat_asset = build_item_asset(zones(:hat1), body_id: 1) # depth 44
|
||||
shirt_asset = build_item_asset(zones(:shirtdress), body_id: 1) # depth 26
|
||||
|
||||
bg = build_item("Background Item", swf_assets: [bg_item])
|
||||
hat = build_item("Hat", swf_assets: [hat_asset])
|
||||
shirt = build_item("Shirt", swf_assets: [shirt_asset])
|
||||
|
||||
outfit = Outfit.new(pet_state: pet_state)
|
||||
outfit.worn_items = [hat, bg, shirt]
|
||||
|
||||
layers = outfit.visible_layers
|
||||
|
||||
# Expected order by depth:
|
||||
# background (3), bg_item (4), body_layer (18), shirt_asset (26),
|
||||
# head_layer (34), hat_asset (44)
|
||||
expect(layers.map(&:depth)).to eq([3, 4, 18, 26, 34, 44])
|
||||
expect(layers).to eq([background, bg_item, body_layer, shirt_asset, head_layer, hat_asset])
|
||||
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(:hat1), 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(:hat1), 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(:hat1), 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(:righthanditem1), body_id: 0) # depth 46
|
||||
|
||||
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), alt_body (18), alt_head (34), trinket (46)
|
||||
expect(layers.map(&:depth)).to eq([3, 4, 18, 34, 46])
|
||||
expect(layers).to eq([alt_background, bg_item, alt_body, alt_head, trinket])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
require_relative '../../rails_helper'
|
||||
require_relative '../../support/mocks/custom_pets'
|
||||
require_relative '../../support/mocks/nc_mall'
|
||||
|
||||
RSpec.describe Pet::AutoModeling, type: :model do
|
||||
fixtures :colors, :species, :zones
|
||||
|
||||
# Set up a Purple Chia pet type (body_id 212) for testing
|
||||
let!(:pet_type) do
|
||||
PetType.create!(
|
||||
species_id: Species.find_by_name!("chia").id,
|
||||
color_id: Color.find_by_name!("purple").id,
|
||||
body_id: 212,
|
||||
image_hash: "purpchia"
|
||||
)
|
||||
end
|
||||
|
||||
# A known compatible item for testing (exists in mock data)
|
||||
let(:compatible_item) do
|
||||
Item.create!(
|
||||
id: 71706,
|
||||
name: "On the Roof Background",
|
||||
description: "Who is that on the roof?! Could it be...?",
|
||||
thumbnail_url: "https://images.neopets.com/items/gif_roof_onthe_fg.gif",
|
||||
rarity: "Special",
|
||||
rarity_index: 101,
|
||||
price: 0,
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000"
|
||||
)
|
||||
end
|
||||
|
||||
describe ".model_item_on_body" do
|
||||
context "when item is compatible with the body" do
|
||||
let(:item) { compatible_item }
|
||||
|
||||
it "returns :modeled" do
|
||||
result = Pet::AutoModeling.model_item_on_body(item, 212)
|
||||
expect(result).to eq :modeled
|
||||
end
|
||||
|
||||
it "creates SwfAsset records for the item" do
|
||||
expect {
|
||||
Pet::AutoModeling.model_item_on_body(item, 212)
|
||||
}.to change { SwfAsset.where(type: "object").count }.by(1)
|
||||
end
|
||||
|
||||
it "associates the SwfAsset with the item" do
|
||||
Pet::AutoModeling.model_item_on_body(item, 212)
|
||||
item.reload
|
||||
|
||||
asset = item.swf_assets.find_by(remote_id: 410722)
|
||||
expect(asset).to be_present
|
||||
expect(asset.body_id).to eq 0 # This item fits all bodies
|
||||
expect(asset.zone_id).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
context "when item is not in the response" do
|
||||
let(:item) do
|
||||
# Create an item that won't be in our mock response
|
||||
Item.create!(
|
||||
id: 99999,
|
||||
name: "Nonexistent Item",
|
||||
description: "This item doesn't exist in the mock",
|
||||
thumbnail_url: "https://example.com/item.gif",
|
||||
rarity: "Special",
|
||||
rarity_index: 101,
|
||||
price: 0,
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000"
|
||||
)
|
||||
end
|
||||
|
||||
it "returns :not_compatible" do
|
||||
result = Pet::AutoModeling.model_item_on_body(item, 212)
|
||||
expect(result).to eq :not_compatible
|
||||
end
|
||||
end
|
||||
|
||||
context "when no PetType exists for the body_id" do
|
||||
let(:item) { compatible_item }
|
||||
|
||||
it "raises NoPetTypeForBody" do
|
||||
expect {
|
||||
Pet::AutoModeling.model_item_on_body(item, 99999)
|
||||
}.to raise_error(Pet::AutoModeling::NoPetTypeForBody) do |error|
|
||||
expect(error.body_id).to eq 99999
|
||||
expect(error.message).to include "99999"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,14 +3,9 @@ module Neopets::CustomPets
|
|||
DATA_DIR = Pathname.new(__dir__) / "custom_pets"
|
||||
|
||||
def self.fetch_viewer_data(pet_name, ...)
|
||||
# NOTE: Windows doesn't support `@` in filenames, so we use a `scis` directory instead.
|
||||
path = if pet_name.start_with?('@')
|
||||
DATA_DIR / "scis" / "#{pet_name[1..]}.json"
|
||||
else
|
||||
DATA_DIR / "#{pet_name}.json"
|
||||
File.open(DATA_DIR / "#{pet_name}.json") do |file|
|
||||
HashWithIndifferentAccess.new JSON.load(file)
|
||||
end
|
||||
|
||||
File.open(path) { |f| HashWithIndifferentAccess.new JSON.load(f) }
|
||||
end
|
||||
|
||||
def self.fetch_metadata(...)
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"custom_pet": {
|
||||
"name": "@mock:m:thyass:39552",
|
||||
"owner": "",
|
||||
"slot": 1.0,
|
||||
"scale": 0.5,
|
||||
"muted": true,
|
||||
"body_id": 212.0,
|
||||
"species_id": 6.0,
|
||||
"color_id": 61.0,
|
||||
"alt_style": false,
|
||||
"alt_color": 61.0,
|
||||
"style_closet_id": null,
|
||||
"biology_by_zone": {
|
||||
"37": {
|
||||
"part_id": 10083.0,
|
||||
"zone_id": 37.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/010/10083_8a1111a13f.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/010/10083_8a1111a13f/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"15": {
|
||||
"part_id": 11613.0,
|
||||
"zone_id": 15.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/011/11613_f7d8d377ab.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/011/11613_f7d8d377ab/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"34": {
|
||||
"part_id": 14187.0,
|
||||
"zone_id": 34.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/014/14187_0e65c2082f.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/014/14187_0e65c2082f/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"33": {
|
||||
"part_id": 14189.0,
|
||||
"zone_id": 33.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/014/14189_102e4991e9.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/014/14189_102e4991e9/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
"equipped_by_zone": {},
|
||||
"original_biology": []
|
||||
},
|
||||
"closet_items": {},
|
||||
"object_info_registry": {
|
||||
"39552": {
|
||||
"obj_info_id": 39552.0,
|
||||
"assets_by_zone": {},
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000",
|
||||
"is_compatible": false,
|
||||
"is_paid": true,
|
||||
"thumbnail_url": "https://images.neopets.com/items/mall_springyeyeglasses.gif",
|
||||
"name": "Springy Eye Glasses",
|
||||
"description": "Hey, keep your eyes in your head!",
|
||||
"category": "Clothes",
|
||||
"type": "Clothes",
|
||||
"rarity": "Artifact",
|
||||
"rarity_index": 500.0,
|
||||
"price": 0.0,
|
||||
"weight_lbs": 1.0,
|
||||
"species_support": [3.0],
|
||||
"converted": true
|
||||
}
|
||||
},
|
||||
"object_asset_registry": {}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
{
|
||||
"custom_pet": {
|
||||
"name": "@mock:m:thyass:71706",
|
||||
"owner": "",
|
||||
"slot": 1.0,
|
||||
"scale": 0.5,
|
||||
"muted": true,
|
||||
"body_id": 212.0,
|
||||
"species_id": 6.0,
|
||||
"color_id": 61.0,
|
||||
"alt_style": false,
|
||||
"alt_color": 61.0,
|
||||
"style_closet_id": null,
|
||||
"biology_by_zone": {
|
||||
"37": {
|
||||
"part_id": 10083.0,
|
||||
"zone_id": 37.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/010/10083_8a1111a13f.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/010/10083_8a1111a13f/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"15": {
|
||||
"part_id": 11613.0,
|
||||
"zone_id": 15.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/011/11613_f7d8d377ab.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/011/11613_f7d8d377ab/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"34": {
|
||||
"part_id": 14187.0,
|
||||
"zone_id": 34.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/014/14187_0e65c2082f.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/014/14187_0e65c2082f/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"33": {
|
||||
"part_id": 14189.0,
|
||||
"zone_id": 33.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/014/14189_102e4991e9.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/014/14189_102e4991e9/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
"equipped_by_zone": {
|
||||
"3": {
|
||||
"asset_id": 410722.0,
|
||||
"zone_id": 3.0,
|
||||
"closet_obj_id": 0.0
|
||||
}
|
||||
},
|
||||
"original_biology": []
|
||||
},
|
||||
"closet_items": {},
|
||||
"object_info_registry": {
|
||||
"71706": {
|
||||
"obj_info_id": 71706.0,
|
||||
"assets_by_zone": {
|
||||
"3": 410722.0
|
||||
},
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000",
|
||||
"is_compatible": true,
|
||||
"is_paid": false,
|
||||
"thumbnail_url": "https://images.neopets.com/items/gif_roof_onthe_fg.gif",
|
||||
"name": "On the Roof Background",
|
||||
"description": "Who is that on the roof?! Could it be...?",
|
||||
"category": "Special",
|
||||
"type": "Mystical Surroundings",
|
||||
"rarity": "Special",
|
||||
"rarity_index": 101.0,
|
||||
"price": 0.0,
|
||||
"weight_lbs": 1.0,
|
||||
"species_support": [],
|
||||
"converted": true
|
||||
}
|
||||
},
|
||||
"object_asset_registry": {
|
||||
"410722": {
|
||||
"asset_id": 410722.0,
|
||||
"zone_id": 3.0,
|
||||
"asset_url": "https://images.neopets.com/cp/items/swf/000/000/410/410722_3bcd2f5e11.swf",
|
||||
"obj_info_id": 71706.0,
|
||||
"manifest": "https://images.neopets.com/cp/items/data/000/000/410/410722_3bcd2f5e11/manifest.json?v=1706"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"custom_pet": {
|
||||
"name": "@purpchia:99999",
|
||||
"body_id": 212.0,
|
||||
"species_id": 6.0,
|
||||
"color_id": 61.0,
|
||||
"alt_style": false,
|
||||
"alt_color": 61.0,
|
||||
"biology_by_zone": {
|
||||
"15": {
|
||||
"part_id": 11613.0,
|
||||
"zone_id": 15.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/011/11613_f7d8d377ab.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/011/11613_f7d8d377ab/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
"equipped_by_zone": [],
|
||||
"original_biology": []
|
||||
},
|
||||
"closet_items": [],
|
||||
"object_info_registry": [],
|
||||
"object_asset_registry": []
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# We replace Neopets::NCMall.fetch_pet_data_sci with a mocked implementation.
|
||||
module Neopets::NCMall
|
||||
# Mock implementation that generates predictable SCI hashes for testing.
|
||||
# The hash is derived from the pet_sci and item_ids to ensure consistency.
|
||||
def self.fetch_pet_data_sci(pet_sci, item_ids = [])
|
||||
"#{pet_sci}-#{item_ids.sort.join('-')}"
|
||||
end
|
||||
end
|
||||
BIN
vendor/cache/action_text-trix-2.1.15.gem
vendored
Normal file
BIN
vendor/cache/action_text-trix-2.1.15.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/action_text-trix-2.1.16.gem
vendored
BIN
vendor/cache/action_text-trix-2.1.16.gem
vendored
Binary file not shown.
BIN
vendor/cache/actioncable-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actioncable-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actioncable-8.1.2.gem
vendored
BIN
vendor/cache/actioncable-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actionmailbox-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actionmailbox-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actionmailbox-8.1.2.gem
vendored
BIN
vendor/cache/actionmailbox-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actionmailer-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actionmailer-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actionmailer-8.1.2.gem
vendored
BIN
vendor/cache/actionmailer-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actionpack-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actionpack-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actionpack-8.1.2.gem
vendored
BIN
vendor/cache/actionpack-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actiontext-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actiontext-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actiontext-8.1.2.gem
vendored
BIN
vendor/cache/actiontext-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actionview-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actionview-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actionview-8.1.2.gem
vendored
BIN
vendor/cache/actionview-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activejob-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activejob-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activejob-8.1.2.gem
vendored
BIN
vendor/cache/activejob-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activemodel-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activemodel-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activemodel-8.1.2.gem
vendored
BIN
vendor/cache/activemodel-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activerecord-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activerecord-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activerecord-8.1.2.gem
vendored
BIN
vendor/cache/activerecord-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activestorage-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activestorage-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activestorage-8.1.2.gem
vendored
BIN
vendor/cache/activestorage-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activesupport-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activesupport-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activesupport-8.1.2.gem
vendored
BIN
vendor/cache/activesupport-8.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/ast-2.4.3.gem
vendored
Normal file
BIN
vendor/cache/ast-2.4.3.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-2.35.0.gem
vendored
Normal file
BIN
vendor/cache/async-2.35.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-2.35.3.gem
vendored
BIN
vendor/cache/async-2.35.3.gem
vendored
Binary file not shown.
BIN
vendor/cache/async-container-0.27.7.gem
vendored
Normal file
BIN
vendor/cache/async-container-0.27.7.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-container-0.29.0.gem
vendored
BIN
vendor/cache/async-container-0.29.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/async-service-0.16.0.gem
vendored
Normal file
BIN
vendor/cache/async-service-0.16.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-service-0.17.0.gem
vendored
BIN
vendor/cache/async-service-0.17.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/backport-1.2.0.gem
vendored
Normal file
BIN
vendor/cache/backport-1.2.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/bcrypt-3.1.20.gem
vendored
Normal file
BIN
vendor/cache/bcrypt-3.1.20.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/bcrypt-3.1.21.gem
vendored
BIN
vendor/cache/bcrypt-3.1.21.gem
vendored
Binary file not shown.
BIN
vendor/cache/benchmark-0.5.0.gem
vendored
Normal file
BIN
vendor/cache/benchmark-0.5.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/bootsnap-1.20.1.gem
vendored
Normal file
BIN
vendor/cache/bootsnap-1.20.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/bootsnap-1.21.1.gem
vendored
BIN
vendor/cache/bootsnap-1.21.1.gem
vendored
Binary file not shown.
BIN
vendor/cache/dotenv-2.8.1.gem
vendored
Normal file
BIN
vendor/cache/dotenv-2.8.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/dotenv-3.2.0.gem
vendored
BIN
vendor/cache/dotenv-3.2.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/dotenv-rails-2.8.1.gem
vendored
Normal file
BIN
vendor/cache/dotenv-rails-2.8.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/faraday-follow_redirects-0.4.0.gem
vendored
Normal file
BIN
vendor/cache/faraday-follow_redirects-0.4.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/faraday-follow_redirects-0.5.0.gem
vendored
BIN
vendor/cache/faraday-follow_redirects-0.5.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/ffi-1.17.2-aarch64-linux-gnu.gem
vendored
Normal file
BIN
vendor/cache/ffi-1.17.2-aarch64-linux-gnu.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/ffi-1.17.2-arm64-darwin.gem
vendored
Normal file
BIN
vendor/cache/ffi-1.17.2-arm64-darwin.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/ffi-1.17.2-x86_64-linux-gnu.gem
vendored
Normal file
BIN
vendor/cache/ffi-1.17.2-x86_64-linux-gnu.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/ffi-1.17.2.gem
vendored
Normal file
BIN
vendor/cache/ffi-1.17.2.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/ffi-1.17.3-aarch64-linux-gnu.gem
vendored
BIN
vendor/cache/ffi-1.17.3-aarch64-linux-gnu.gem
vendored
Binary file not shown.
BIN
vendor/cache/ffi-1.17.3-arm64-darwin.gem
vendored
BIN
vendor/cache/ffi-1.17.3-arm64-darwin.gem
vendored
Binary file not shown.
BIN
vendor/cache/ffi-1.17.3-x86_64-linux-gnu.gem
vendored
BIN
vendor/cache/ffi-1.17.3-x86_64-linux-gnu.gem
vendored
Binary file not shown.
BIN
vendor/cache/haml-6.4.0.gem
vendored
Normal file
BIN
vendor/cache/haml-6.4.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/haml-7.2.0.gem
vendored
BIN
vendor/cache/haml-7.2.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/jaro_winkler-1.6.1.gem
vendored
Normal file
BIN
vendor/cache/jaro_winkler-1.6.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/kramdown-2.5.1.gem
vendored
Normal file
BIN
vendor/cache/kramdown-2.5.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/kramdown-parser-gfm-1.1.0.gem
vendored
Normal file
BIN
vendor/cache/kramdown-parser-gfm-1.1.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/language_server-protocol-3.17.0.5.gem
vendored
Normal file
BIN
vendor/cache/language_server-protocol-3.17.0.5.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/lint_roller-1.1.0.gem
vendored
Normal file
BIN
vendor/cache/lint_roller-1.1.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/localhost-1.6.0.gem
vendored
Normal file
BIN
vendor/cache/localhost-1.6.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/localhost-1.7.0.gem
vendored
BIN
vendor/cache/localhost-1.7.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/mini_portile2-2.8.9.gem
vendored
Normal file
BIN
vendor/cache/mini_portile2-2.8.9.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/nokogiri-1.18.10-arm64-darwin.gem
vendored
Normal file
BIN
vendor/cache/nokogiri-1.18.10-arm64-darwin.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/nokogiri-1.18.10.gem
vendored
Normal file
BIN
vendor/cache/nokogiri-1.18.10.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/nokogiri-1.19.0-aarch64-linux-gnu.gem
vendored
BIN
vendor/cache/nokogiri-1.19.0-aarch64-linux-gnu.gem
vendored
Binary file not shown.
BIN
vendor/cache/nokogiri-1.19.0-arm64-darwin.gem
vendored
BIN
vendor/cache/nokogiri-1.19.0-arm64-darwin.gem
vendored
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue