Compare commits
155 commits
rainbow-po
...
main
Author | SHA1 | Date | |
---|---|---|---|
c7bea666c9 | |||
f49f9f386d | |||
3937ba354f | |||
388bb9a251 | |||
e846a75f7a | |||
270b27c1d2 | |||
4cbac13df1 | |||
0261d02137 | |||
e82c606ee8 | |||
ed5b62e161 | |||
5472ccebef | |||
f6f618c9d5 | |||
39bed6b157 | |||
af5187edb6 | |||
21eaf7b266 | |||
91851bc340 | |||
3e7d27eaa3 | |||
f7109e398a | |||
f90380c4e6 | |||
218dc5b6f9 | |||
bc0097850d | |||
ec0b8d9cb9 | |||
a57b3629db | |||
1d1dc15320 | |||
b6c21dfe40 | |||
c4a7e7916f | |||
217d25edab | |||
dd213e8078 | |||
c5995a2bd1 | |||
1ad3ea8f96 | |||
b245690a60 | |||
3ed1c46b64 | |||
9e3ce74ed5 | |||
5f31e38428 | |||
8f9daf4d52 | |||
3242981eb2 | |||
54b25ef08e | |||
e4e81f0694 | |||
e3d196fe87 | |||
0b3dd02323 | |||
48c1a58df9 | |||
42e7eabdd8 | |||
a208fca8d2 | |||
3ac89e830e | |||
d82c7f817a | |||
5264947608 | |||
90407403ba | |||
242b85470d | |||
43717e2535 | |||
bc1f7152bf | |||
9eaee4a2d4 | |||
52ca41dbff | |||
c03e7446e3 | |||
6402e5abc3 | |||
f81415d327 | |||
13ceec8fcc | |||
40765c729e | |||
d26f3a7598 | |||
06721f77e9 | |||
f9be3dceb1 | |||
c9c080e74d | |||
e65634d8bc | |||
4c5d14c591 | |||
28bd6ecca4 | |||
7a837edaf6 | |||
f3894759d6 | |||
30ada0b7e1 | |||
8a38ce90dc | |||
6d25b3424f | |||
8902527438 | |||
044dface14 | |||
b1890d4f6f | |||
3a5f33fd56 | |||
a54a844e03 | |||
c78d45a0b5 | |||
930bfca028 | |||
29aa769bda | |||
66438eae1a | |||
3b5b13c172 | |||
5b1d1f0695 | |||
e92e315743 | |||
eb2fb125b9 | |||
d8ff99475e | |||
9726ecb1a5 | |||
540ce08caa | |||
881e63cfbd | |||
09e5a39b4c | |||
bf20c9bb31 | |||
7607c2c015 | |||
abfe1e6df7 | |||
e36e273d50 | |||
83e5ad6bcc | |||
acb52cb870 | |||
7ef689d658 | |||
23c083ff1d | |||
6b7c73870a | |||
e7a0ff1234 | |||
50c9ba53e7 | |||
89c729ecbe | |||
bb83f6fd36 | |||
7891acd3b1 | |||
16deee94e4 | |||
2cc0c5b031 | |||
381a892af8 | |||
1a0fb68b1c | |||
4f9fbc1ac0 | |||
ad51690617 | |||
5648f55d2c | |||
6934b636fb | |||
83fe0d20e0 | |||
be5ad31a1d | |||
1626f0706c | |||
7fad6abfed | |||
c985c50a1b | |||
bba863b94b | |||
7c1b3ca447 | |||
71f0aa4908 | |||
13a0362e6d | |||
fe67211fdf | |||
0244653cb0 | |||
2c0d55edd1 | |||
be0faaa36e | |||
f87f4e61b3 | |||
dfca88bed3 | |||
bd001e643e | |||
1d51e28144 | |||
fe4db1b605 | |||
860b8eef72 | |||
61e22e3943 | |||
03e4233f67 | |||
b6bddb14be | |||
e52838ba70 | |||
7ba68c52d4 | |||
26add4577c | |||
efda6d74ab | |||
4a431a4ae8 | |||
4bcc3aaebb | |||
5890e52e53 | |||
dd8426fefd | |||
2a9818b2d1 | |||
0b72b5568c | |||
86e1f31231 | |||
a99fb3ec02 | |||
d11c18129d | |||
0958111341 | |||
775baa250b | |||
2bd8afd486 | |||
1f1c6d92b1 | |||
e4a640ccee | |||
d465f4125e | |||
946a6326ac | |||
d5a901b917 | |||
39e5ca59c4 | |||
4fa80d33cc | |||
d66f81c96b |
124 changed files with 4069 additions and 793 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,6 +4,8 @@ log/*.log
|
||||||
tmp/**/*
|
tmp/**/*
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
/spec/examples.txt
|
||||||
|
/.yardoc
|
||||||
|
|
||||||
/app/assets/builds/*
|
/app/assets/builds/*
|
||||||
!/app/assets/builds/.keep
|
!/app/assets/builds/.keep
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn lint --max-warnings=0 --fix
|
# Run the linter, and all our tests.
|
||||||
|
yarn lint --max-warnings=0 --fix && bin/rake test spec
|
||||||
|
|
1
.rspec
Normal file
1
.rspec
Normal file
|
@ -0,0 +1 @@
|
||||||
|
--require spec_helper
|
14
Gemfile
14
Gemfile
|
@ -19,7 +19,7 @@ gem 'haml', '~> 6.1', '>= 6.1.1'
|
||||||
gem 'sass-rails', '~> 6.0'
|
gem 'sass-rails', '~> 6.0'
|
||||||
gem 'terser', '~> 1.1', '>= 1.1.17'
|
gem 'terser', '~> 1.1', '>= 1.1.17'
|
||||||
gem 'react-rails', '~> 2.7', '>= 2.7.1'
|
gem 'react-rails', '~> 2.7', '>= 2.7.1'
|
||||||
gem 'jsbundling-rails', '~> 1.1'
|
gem 'jsbundling-rails', '~> 1.3'
|
||||||
gem 'turbo-rails', '~> 2.0'
|
gem 'turbo-rails', '~> 2.0'
|
||||||
|
|
||||||
# For authentication.
|
# For authentication.
|
||||||
|
@ -84,5 +84,13 @@ gem "sentry-rails", "~> 5.12"
|
||||||
gem "shell", "~> 0.8.1"
|
gem "shell", "~> 0.8.1"
|
||||||
|
|
||||||
# For workspace autocomplete.
|
# For workspace autocomplete.
|
||||||
gem "solargraph", "~> 0.50.0", group: :development
|
group :development do
|
||||||
gem "solargraph-rails", "~> 1.1", group: :development
|
gem "solargraph", "~> 0.50.0"
|
||||||
|
gem "solargraph-rails", "~> 1.1"
|
||||||
|
end
|
||||||
|
|
||||||
|
# For automated tests.
|
||||||
|
group :development, :test do
|
||||||
|
gem "rspec-rails", "~> 7.0"
|
||||||
|
gem "webmock", "~> 3.24", group: :test
|
||||||
|
end
|
||||||
|
|
29
Gemfile.lock
29
Gemfile.lock
|
@ -128,6 +128,9 @@ GEM
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
fiber-local (~> 1.1)
|
fiber-local (~> 1.1)
|
||||||
json
|
json
|
||||||
|
crack (1.0.0)
|
||||||
|
bigdecimal
|
||||||
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
csv (3.3.0)
|
csv (3.3.0)
|
||||||
date (3.3.4)
|
date (3.3.4)
|
||||||
|
@ -182,6 +185,7 @@ GEM
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
|
hashdiff (1.1.2)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
httparty (0.22.0)
|
httparty (0.22.0)
|
||||||
|
@ -376,6 +380,23 @@ GEM
|
||||||
reverse_markdown (2.1.1)
|
reverse_markdown (2.1.1)
|
||||||
nokogiri
|
nokogiri
|
||||||
rexml (3.3.7)
|
rexml (3.3.7)
|
||||||
|
rspec-core (3.13.2)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-expectations (3.13.3)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-mocks (3.13.2)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-rails (7.0.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.1)
|
||||||
rubocop (1.66.1)
|
rubocop (1.66.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
|
@ -479,6 +500,10 @@ GEM
|
||||||
activesupport
|
activesupport
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
|
webmock (3.24.0)
|
||||||
|
addressable (>= 2.8.0)
|
||||||
|
crack (>= 0.3.2)
|
||||||
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webrick (1.8.2)
|
webrick (1.8.2)
|
||||||
websocket-driver (0.7.6)
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
|
@ -503,7 +528,7 @@ DEPENDENCIES
|
||||||
haml (~> 6.1, >= 6.1.1)
|
haml (~> 6.1, >= 6.1.1)
|
||||||
http_accept_language (~> 2.1, >= 2.1.1)
|
http_accept_language (~> 2.1, >= 2.1.1)
|
||||||
httparty (~> 0.22.0)
|
httparty (~> 0.22.0)
|
||||||
jsbundling-rails (~> 1.1)
|
jsbundling-rails (~> 1.3)
|
||||||
letter_opener (~> 1.8, >= 1.8.1)
|
letter_opener (~> 1.8, >= 1.8.1)
|
||||||
memory_profiler (~> 1.0)
|
memory_profiler (~> 1.0)
|
||||||
mysql2 (~> 0.5.5)
|
mysql2 (~> 0.5.5)
|
||||||
|
@ -518,6 +543,7 @@ DEPENDENCIES
|
||||||
rails-i18n (~> 7.0, >= 7.0.7)
|
rails-i18n (~> 7.0, >= 7.0.7)
|
||||||
rdiscount (~> 2.2, >= 2.2.7.1)
|
rdiscount (~> 2.2, >= 2.2.7.1)
|
||||||
react-rails (~> 2.7, >= 2.7.1)
|
react-rails (~> 2.7, >= 2.7.1)
|
||||||
|
rspec-rails (~> 7.0)
|
||||||
sanitize (~> 6.0, >= 6.0.2)
|
sanitize (~> 6.0, >= 6.0.2)
|
||||||
sass-rails (~> 6.0)
|
sass-rails (~> 6.0)
|
||||||
sentry-rails (~> 5.12)
|
sentry-rails (~> 5.12)
|
||||||
|
@ -531,6 +557,7 @@ DEPENDENCIES
|
||||||
thread-local (~> 1.1)
|
thread-local (~> 1.1)
|
||||||
turbo-rails (~> 2.0)
|
turbo-rails (~> 2.0)
|
||||||
web-console (~> 4.2)
|
web-console (~> 4.2)
|
||||||
|
webmock (~> 3.24)
|
||||||
will_paginate (~> 4.0)
|
will_paginate (~> 4.0)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
Before Width: | Height: | Size: 45 KiB |
BIN
app/assets/images/rainbow_pool.png
Normal file
BIN
app/assets/images/rainbow_pool.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
|
@ -81,23 +81,35 @@ class SpeciesFacePickerOptions extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MeasuredContent extends HTMLElement {
|
// TODO: If it ever gets wide support, remove this in favor of the CSS rule
|
||||||
|
// `interpolate-size: allow-keywords`, to animate directly from `auto`.
|
||||||
|
// https://drafts.csswg.org/css-values-5/#valdef-interpolate-size-allow-keywords
|
||||||
|
class MeasuredContainer extends HTMLElement {
|
||||||
|
static observedAttributes = ["style"];
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
setTimeout(() => this.#measure(), 0);
|
setTimeout(() => this.#measure(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#measure() {
|
attributeChangedCallback() {
|
||||||
// Find our `<measured-container>` parent, and set our natural width
|
// When `--natural-width` gets morphed away by Turbo, measure it again!
|
||||||
// as `var(--natural-width)` in the context of its CSS styles.
|
if (this.style.getPropertyValue("--natural-width") === "") {
|
||||||
const container = this.closest("measured-container");
|
this.#measure();
|
||||||
if (container == null) {
|
|
||||||
throw new Error(`<measured-content> must be in a <measured-container>`);
|
|
||||||
}
|
}
|
||||||
container.style.setProperty("--natural-width", this.offsetWidth + "px");
|
}
|
||||||
|
|
||||||
|
#measure() {
|
||||||
|
// Find our `<measured-content>` child, and set our natural width as
|
||||||
|
// `var(--natural-width)` in the context of our CSS styles.
|
||||||
|
const content = this.querySelector("measured-content");
|
||||||
|
if (content == null) {
|
||||||
|
throw new Error(`<measured-container> must contain a <measured-content>`);
|
||||||
|
}
|
||||||
|
this.style.setProperty("--natural-width", content.offsetWidth + "px");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("species-color-picker", SpeciesColorPicker);
|
customElements.define("species-color-picker", SpeciesColorPicker);
|
||||||
customElements.define("species-face-picker", SpeciesFacePicker);
|
customElements.define("species-face-picker", SpeciesFacePicker);
|
||||||
customElements.define("species-face-picker-options", SpeciesFacePickerOptions);
|
customElements.define("species-face-picker-options", SpeciesFacePickerOptions);
|
||||||
customElements.define("measured-content", MeasuredContent);
|
customElements.define("measured-container", MeasuredContainer);
|
||||||
|
|
|
@ -74,7 +74,7 @@ $container_width: 800px
|
||||||
input, button, select, label
|
input, button, select, label
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
input[type=text], input[type=password], input[type=search], input[type=number], input[type=email], select, textarea
|
input[type=text], input[type=password], input[type=search], input[type=number], input[type=email], input[type=url], select, textarea
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
background: #fff
|
background: #fff
|
||||||
border: 1px solid $input-border-color
|
border: 1px solid $input-border-color
|
||||||
|
@ -83,6 +83,15 @@ input[type=text], input[type=password], input[type=search], input[type=number],
|
||||||
&:focus, &:active
|
&:focus, &:active
|
||||||
color: inherit
|
color: inherit
|
||||||
|
|
||||||
|
select:has(option[value='']:checked)
|
||||||
|
color: #666
|
||||||
|
|
||||||
|
option[value='']
|
||||||
|
color: #666
|
||||||
|
|
||||||
|
option:not([value=''])
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
textarea
|
textarea
|
||||||
font: inherit
|
font: inherit
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,20 @@ body.use-responsive-design
|
||||||
max-width: 100%
|
max-width: 100%
|
||||||
padding-inline: 1rem
|
padding-inline: 1rem
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
|
padding-top: 0
|
||||||
|
|
||||||
|
#main-nav
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
#home-link, #userbar
|
||||||
|
position: static
|
||||||
|
|
||||||
#home-link
|
#home-link
|
||||||
margin-left: 1rem
|
padding-inline: .5rem
|
||||||
padding-inline: 0
|
margin-inline: -.5rem
|
||||||
|
margin-right: auto
|
||||||
|
|
||||||
#userbar
|
#userbar
|
||||||
margin-right: 1rem
|
margin-left: auto
|
||||||
|
text-align: right
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
body.alt_styles-index
|
|
||||||
.alt-styles-header
|
|
||||||
margin-top: 1em
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
.alt-styles-list
|
|
||||||
list-style: none
|
|
||||||
display: flex
|
|
||||||
flex-wrap: wrap
|
|
||||||
gap: 1.5em
|
|
||||||
|
|
||||||
.alt-style
|
|
||||||
text-align: center
|
|
||||||
width: 80px
|
|
||||||
|
|
||||||
.alt-style-thumbnail
|
|
||||||
width: 80px
|
|
||||||
height: 80px
|
|
4
app/assets/stylesheets/alt_styles/edit.sass
Normal file
4
app/assets/stylesheets/alt_styles/edit.sass
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.alt-style-preview
|
||||||
|
width: 300px
|
||||||
|
height: 300px
|
||||||
|
margin: 0 auto
|
3
app/assets/stylesheets/alt_styles/index.sass
Normal file
3
app/assets/stylesheets/alt_styles/index.sass
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.rainbow-pool-list
|
||||||
|
.name span
|
||||||
|
display: inline-block
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
@import partials/jquery.jgrowl
|
@import partials/jquery.jgrowl
|
||||||
|
|
||||||
@import alt_styles/index
|
|
||||||
@import closet_hangers/index
|
@import closet_hangers/index
|
||||||
@import closet_lists/form
|
@import closet_lists/form
|
||||||
@import neopets_page_import_tasks/new
|
@import neopets_page_import_tasks/new
|
||||||
|
|
23
app/assets/stylesheets/application/breadcrumbs.sass
Normal file
23
app/assets/stylesheets/application/breadcrumbs.sass
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#title:has(+ .breadcrumbs)
|
||||||
|
margin-bottom: .125em
|
||||||
|
|
||||||
|
.breadcrumbs
|
||||||
|
list-style-type: none
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
margin-block: .5em
|
||||||
|
font-size: .85em
|
||||||
|
|
||||||
|
li
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
li:not(:first-child)
|
||||||
|
&::before
|
||||||
|
margin-inline: .35em
|
||||||
|
content: "→"
|
||||||
|
|
||||||
|
&[data-relation-to-prev=sibling]::before
|
||||||
|
content: "+"
|
||||||
|
|
||||||
|
&[data-relation-to-prev=menu]::before
|
||||||
|
content: "-"
|
|
@ -1,6 +1,8 @@
|
||||||
@import "../partials/clean/constants"
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
.pet-filters
|
.rainbow-pool-filters
|
||||||
|
margin-block: .5em
|
||||||
|
|
||||||
fieldset
|
fieldset
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
|
@ -12,19 +14,20 @@
|
||||||
display: contents
|
display: contents
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
|
|
||||||
[role=navigation]
|
select
|
||||||
margin-block: .5em
|
width: 16ch
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.pet-types
|
.rainbow-pool-list
|
||||||
list-style-type: none
|
list-style-type: none
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
justify-content: center
|
justify-content: center
|
||||||
gap: .5em
|
gap: .5em
|
||||||
|
|
||||||
|
--preview-base-width: 150px
|
||||||
|
|
||||||
> li
|
> li
|
||||||
width: 150px
|
width: var(--preview-base-width)
|
||||||
max-width: calc(50% - .25em)
|
max-width: calc(50% - .25em)
|
||||||
min-width: 150px
|
min-width: 150px
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
|
@ -40,7 +43,7 @@
|
||||||
outline: 1px solid $module-border-color
|
outline: 1px solid $module-border-color
|
||||||
background: $module-bg-color
|
background: $module-bg-color
|
||||||
|
|
||||||
img
|
.preview
|
||||||
width: 100%
|
width: 100%
|
||||||
height: auto
|
height: auto
|
||||||
aspect-ratio: 1 / 1
|
aspect-ratio: 1 / 1
|
||||||
|
@ -53,3 +56,19 @@
|
||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
position: relative
|
position: relative
|
||||||
z-index: 1
|
z-index: 1
|
||||||
|
|
||||||
|
.info
|
||||||
|
font-size: .85em
|
||||||
|
p
|
||||||
|
margin-block: .25em
|
||||||
|
|
||||||
|
.rainbow-pool-pagination
|
||||||
|
margin-block: .5em
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
gap: 1em
|
||||||
|
|
||||||
|
.rainbow-pool-no-results
|
||||||
|
margin-block: 1em
|
||||||
|
text-align: center
|
||||||
|
font-style: italic
|
57
app/assets/stylesheets/application/support-form.sass
Normal file
57
app/assets/stylesheets/application/support-form.sass
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
.support-form
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 1em
|
||||||
|
align-items: flex-start
|
||||||
|
|
||||||
|
fieldset
|
||||||
|
width: 100%
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: auto 1fr
|
||||||
|
align-items: center
|
||||||
|
gap: 1em
|
||||||
|
|
||||||
|
> *:nth-child(2n)
|
||||||
|
width: 40rch
|
||||||
|
max-width: 100%
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
input[type=url]
|
||||||
|
font-size: .85em
|
||||||
|
|
||||||
|
> label, .field-name
|
||||||
|
font-weight: bold
|
||||||
|
&:has(+ .radio-field)
|
||||||
|
align-self: start
|
||||||
|
|
||||||
|
.thumbnail-field
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .25em
|
||||||
|
|
||||||
|
img
|
||||||
|
width: 40px
|
||||||
|
height: 40px
|
||||||
|
|
||||||
|
input
|
||||||
|
flex: 1 0 20ch
|
||||||
|
|
||||||
|
.radio-field
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: .25em
|
||||||
|
|
||||||
|
.field_with_errors
|
||||||
|
display: contents
|
||||||
|
|
||||||
|
.actions
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 1em
|
||||||
|
|
||||||
|
label
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .25em
|
||||||
|
font-size: .85em
|
||||||
|
font-style: italic
|
|
@ -67,14 +67,21 @@
|
||||||
background: #FEEBC8
|
background: #FEEBC8
|
||||||
color: #7B341E
|
color: #7B341E
|
||||||
|
|
||||||
|
.support-form
|
||||||
|
grid-area: support
|
||||||
|
font-size: 85%
|
||||||
|
text-align: left
|
||||||
|
|
||||||
.user-lists-info
|
.user-lists-info
|
||||||
grid-area: lists
|
grid-area: lists
|
||||||
font-size: 85%
|
font-size: 85%
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|
||||||
.user-lists-form-opener
|
display: flex
|
||||||
&::after
|
gap: 1em
|
||||||
content: " ›"
|
|
||||||
|
a::after
|
||||||
|
content: " ›"
|
||||||
|
|
||||||
.user-lists-form
|
.user-lists-form
|
||||||
background: $background-color
|
background: $background-color
|
||||||
|
|
25
app/assets/stylesheets/pet_states/edit.sass
Normal file
25
app/assets/stylesheets/pet_states/edit.sass
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
|
outfit-viewer
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
.pose-options
|
||||||
|
list-style-type: none
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: 1fr 1fr 1fr
|
||||||
|
gap: .25em
|
||||||
|
|
||||||
|
label
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .5em
|
||||||
|
padding: .5em 1em
|
||||||
|
border: 1px solid $soft-border-color
|
||||||
|
border-radius: 1em
|
||||||
|
|
||||||
|
input
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
&:has(:checked)
|
||||||
|
background: $module-bg-color
|
||||||
|
border-color: $module-border-color
|
|
@ -1,5 +0,0 @@
|
||||||
outfit-viewer
|
|
||||||
margin: 0 auto
|
|
||||||
|
|
||||||
dt
|
|
||||||
cursor: help
|
|
|
@ -1,43 +1,8 @@
|
||||||
@import "../partials/clean/constants"
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
.pet-states
|
.rainbow-pool-list
|
||||||
list-style-type: none
|
--preview-base-width: 200px
|
||||||
display: flex
|
margin-bottom: 2em
|
||||||
flex-wrap: wrap
|
|
||||||
justify-content: center
|
|
||||||
gap: .5em
|
|
||||||
|
|
||||||
> li
|
|
||||||
width: 200px
|
|
||||||
max-width: calc(50% - .25em)
|
|
||||||
min-width: 150px
|
|
||||||
box-sizing: border-box
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
a
|
|
||||||
display: block
|
|
||||||
border-radius: 1em
|
|
||||||
padding: .5em
|
|
||||||
background: white
|
|
||||||
text-decoration: none
|
|
||||||
&:hover
|
|
||||||
outline: 1px solid $module-border-color
|
|
||||||
background: $module-bg-color
|
|
||||||
|
|
||||||
outfit-viewer
|
|
||||||
width: 100%
|
|
||||||
height: auto
|
|
||||||
aspect-ratio: 1 / 1
|
|
||||||
position: relative
|
|
||||||
z-index: 0
|
|
||||||
margin-bottom: -1em
|
|
||||||
|
|
||||||
.name
|
|
||||||
background: inherit
|
|
||||||
padding: .25em .5em
|
|
||||||
border-radius: .5em
|
|
||||||
position: relative
|
|
||||||
z-index: 1
|
|
||||||
|
|
||||||
.glitched
|
.glitched
|
||||||
cursor: help
|
cursor: help
|
||||||
|
|
|
@ -1,21 +1,40 @@
|
||||||
class AltStylesController < ApplicationController
|
class AltStylesController < ApplicationController
|
||||||
|
before_action :support_staff_only, except: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@alt_styles = AltStyle.includes(:species, :color, :swf_assets).
|
@all_alt_styles = AltStyle.includes(:species, :color)
|
||||||
order(:species_id, :color_id)
|
|
||||||
|
|
||||||
if params[:species_id]
|
@all_colors = @all_alt_styles.map(&:color).uniq.sort_by(&:name)
|
||||||
@species = Species.find(params[:species_id])
|
@all_species = @all_alt_styles.map(&:species).uniq.sort_by(&:name)
|
||||||
@alt_styles = @alt_styles.merge(@species.alt_styles)
|
|
||||||
end
|
|
||||||
|
|
||||||
# We're going to link to the HTML5 image URL, so make sure we have all the
|
@all_series_names = @all_alt_styles.map(&:series_name).uniq.sort
|
||||||
|
@all_color_names = @all_colors.map(&:human_name)
|
||||||
|
@all_species_names = @all_species.map(&:human_name)
|
||||||
|
|
||||||
|
@series_name = params[:series]
|
||||||
|
@color = find_color
|
||||||
|
@species = find_species
|
||||||
|
|
||||||
|
@alt_styles = @all_alt_styles.includes(:swf_assets)
|
||||||
|
@alt_styles.where!(series_name: @series_name) if @series_name.present?
|
||||||
|
@alt_styles.merge!(@color.alt_styles) if @color
|
||||||
|
@alt_styles.merge!(@species.alt_styles) if @species
|
||||||
|
|
||||||
|
# We're using the HTML5 image for our preview, so make sure we have all the
|
||||||
# manifests ready!
|
# manifests ready!
|
||||||
SwfAsset.preload_manifests @alt_styles.map(&:swf_assets).flatten
|
SwfAsset.preload_manifests @alt_styles.map(&:swf_assets).flatten
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render }
|
format.html {
|
||||||
|
@alt_styles = @alt_styles.
|
||||||
|
by_creation_date.order(:color_id, :species_id, :series_name).
|
||||||
|
paginate(page: params[:page], per_page: 30)
|
||||||
|
render
|
||||||
|
}
|
||||||
format.json {
|
format.json {
|
||||||
render json: @alt_styles.includes(swf_assets: [:zone]).as_json(
|
@alt_styles = @alt_styles.includes(swf_assets: [:zone]).
|
||||||
|
sort_by(&:full_name)
|
||||||
|
render json: @alt_styles.as_json(
|
||||||
only: [:id, :species_id, :color_id, :body_id, :series_name,
|
only: [:id, :species_id, :color_id, :body_id, :series_name,
|
||||||
:adjective_name, :thumbnail_url],
|
:adjective_name, :thumbnail_url],
|
||||||
include: {
|
include: {
|
||||||
|
@ -30,4 +49,56 @@ class AltStylesController < ApplicationController
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@alt_style = AltStyle.find params[:id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@alt_style = AltStyle.find params[:id]
|
||||||
|
|
||||||
|
if @alt_style.update(alt_style_params)
|
||||||
|
flash[:notice] = "\"#{@alt_style.full_name}\" successfully saved!"
|
||||||
|
redirect_to destination_after_save
|
||||||
|
else
|
||||||
|
render action: :edit, status: :bad_request
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def alt_style_params
|
||||||
|
params.require(:alt_style).permit(:real_series_name, :thumbnail_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_color
|
||||||
|
if params[:color]
|
||||||
|
Color.find_by(name: params[:color])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_species
|
||||||
|
if params[:species_id]
|
||||||
|
Species.find_by(id: params[:species_id])
|
||||||
|
elsif params[:species]
|
||||||
|
Species.find_by(name: params[:species])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destination_after_save
|
||||||
|
if params[:next] == "unlabeled-style"
|
||||||
|
next_unlabeled_style_path
|
||||||
|
else
|
||||||
|
alt_styles_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_unlabeled_style_path
|
||||||
|
unlabeled_style = AltStyle.unlabeled.newest.first
|
||||||
|
if unlabeled_style
|
||||||
|
edit_alt_style_path(unlabeled_style, next: "unlabeled-style")
|
||||||
|
else
|
||||||
|
alt_styles_path
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -110,5 +110,11 @@ class ApplicationController < ActionController::Base
|
||||||
Rails.logger.debug "Using return_to path: #{return_to.inspect}"
|
Rails.logger.debug "Using return_to path: #{return_to.inspect}"
|
||||||
return_to || root_path
|
return_to || root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def support_staff_only
|
||||||
|
unless current_user&.support_staff?
|
||||||
|
raise AccessDenied, "Support staff only"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
class ItemsController < ApplicationController
|
class ItemsController < ApplicationController
|
||||||
before_action :set_query
|
before_action :set_query
|
||||||
|
before_action :support_staff_only, except: [:index, :show, :sources]
|
||||||
rescue_from Item::Search::Error, :with => :search_error
|
rescue_from Item::Search::Error, :with => :search_error
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -112,6 +113,21 @@ class ItemsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@item = Item.find params[:id]
|
||||||
|
render layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@item = Item.find params[:id]
|
||||||
|
if @item.update(item_params)
|
||||||
|
flash[:notice] = "\"#{@item.name}\" successfully saved!"
|
||||||
|
redirect_to @item
|
||||||
|
else
|
||||||
|
render action: "edit", layout: "application"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def sources
|
def sources
|
||||||
# Load all the items, then group them by source.
|
# Load all the items, then group them by source.
|
||||||
item_ids = params[:ids].split(",")
|
item_ids = params[:ids].split(",")
|
||||||
|
@ -164,6 +180,15 @@ class ItemsController < ApplicationController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
def item_params
|
||||||
|
params.require(:item).permit(
|
||||||
|
:name, :thumbnail_url, :description, :modeling_status_hint,
|
||||||
|
:is_manually_nc, :explicitly_body_specific,
|
||||||
|
).tap do |p|
|
||||||
|
p[:modeling_status_hint] = nil if p[:modeling_status_hint] == ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def assign_closeted!(items)
|
def assign_closeted!(items)
|
||||||
current_user.assign_closeted_to_items!(items) if user_signed_in?
|
current_user.assign_closeted_to_items!(items) if user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,29 +47,24 @@ class OutfitsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@colors = Color.funny.alphabetical
|
@colors = Color.alphabetical
|
||||||
@species = Species.alphabetical
|
@species = Species.alphabetical
|
||||||
|
|
||||||
# HACK: Skip this in development, because it's slow!
|
newest_items = Item.newest.limit(18)
|
||||||
unless Rails.env.development?
|
@newest_modeled_items, @newest_unmodeled_items =
|
||||||
newest_items = Item.newest.
|
newest_items.partition(&:predicted_fully_modeled?)
|
||||||
select(:id, :name, :updated_at, :thumbnail_url, :rarity_index, :is_manually_nc)
|
|
||||||
.limit(18)
|
|
||||||
@newest_modeled_items, @newest_unmodeled_items =
|
|
||||||
newest_items.partition(&:predicted_fully_modeled?)
|
|
||||||
|
|
||||||
@newest_unmodeled_items_predicted_missing_species_by_color = {}
|
@newest_unmodeled_items_predicted_missing_species_by_color = {}
|
||||||
@newest_unmodeled_items_predicted_modeled_ratio = {}
|
@newest_unmodeled_items_predicted_modeled_ratio = {}
|
||||||
@newest_unmodeled_items.each do |item|
|
@newest_unmodeled_items.each do |item|
|
||||||
h = item.predicted_missing_nonstandard_body_ids_by_species_by_color
|
h = item.predicted_missing_nonstandard_body_ids_by_species_by_color
|
||||||
standard_body_ids_by_species = item.
|
standard_body_ids_by_species = item.
|
||||||
predicted_missing_standard_body_ids_by_species
|
predicted_missing_standard_body_ids_by_species
|
||||||
if standard_body_ids_by_species.present?
|
if standard_body_ids_by_species.present?
|
||||||
h[:standard] = standard_body_ids_by_species
|
h[:standard] = standard_body_ids_by_species
|
||||||
end
|
|
||||||
@newest_unmodeled_items_predicted_missing_species_by_color[item] = h
|
|
||||||
@newest_unmodeled_items_predicted_modeled_ratio[item] = item.predicted_modeled_ratio
|
|
||||||
end
|
end
|
||||||
|
@newest_unmodeled_items_predicted_missing_species_by_color[item] = h
|
||||||
|
@newest_unmodeled_items_predicted_modeled_ratio[item] = item.predicted_modeled_ratio
|
||||||
end
|
end
|
||||||
|
|
||||||
@species_count = Species.count
|
@species_count = Species.count
|
||||||
|
|
|
@ -1,6 +1,27 @@
|
||||||
class PetStatesController < ApplicationController
|
class PetStatesController < ApplicationController
|
||||||
def show
|
before_action :find_pet_state
|
||||||
@pet_type = PetType.matching_name_param(params[:pet_type_name]).first!
|
before_action :support_staff_only
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @pet_state.update(pet_state_params)
|
||||||
|
flash[:notice] = "Pet appearance \##{@pet_state.id} successfully saved!"
|
||||||
|
redirect_to @pet_type
|
||||||
|
else
|
||||||
|
render action: :edit, status: :bad_request
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def find_pet_state
|
||||||
|
@pet_type = PetType.find_by_param!(params[:pet_type_name])
|
||||||
@pet_state = @pet_type.pet_states.find(params[:id])
|
@pet_state = @pet_type.pet_states.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pet_state_params
|
||||||
|
params.require(:pet_state).permit(:pose, :glitched)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,27 +1,51 @@
|
||||||
class PetTypesController < ApplicationController
|
class PetTypesController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@species_names = Species.order(:name).map(&:human_name)
|
respond_to do |format|
|
||||||
@color_names = Color.order(:name).map(&:human_name)
|
format.html {
|
||||||
|
@species_names = Species.order(:name).map(&:human_name)
|
||||||
|
@color_names = Color.order(:name).map(&:human_name)
|
||||||
|
|
||||||
if params[:species].present?
|
if params[:species].present?
|
||||||
@selected_species = Species.find_by!(name: params[:species])
|
@selected_species = Species.find_by!(name: params[:species])
|
||||||
@selected_species_name = @selected_species.human_name
|
@selected_species_name = @selected_species.human_name
|
||||||
end
|
end
|
||||||
if params[:color].present?
|
if params[:color].present?
|
||||||
@selected_color = Color.find_by!(name: params[:color])
|
@selected_color = Color.find_by!(name: params[:color])
|
||||||
@selected_color_name = @selected_color.human_name
|
@selected_color_name = @selected_color.human_name
|
||||||
end
|
end
|
||||||
|
@selected_order =
|
||||||
|
if @selected_species.present? || @selected_color.present?
|
||||||
|
:alphabetical
|
||||||
|
else
|
||||||
|
:newest
|
||||||
|
end
|
||||||
|
|
||||||
@pet_types = PetType.
|
@pet_types = PetType.
|
||||||
includes(:color, :species).
|
includes(:color, :species, :pet_states).
|
||||||
order(created_at: :desc).
|
paginate(page: params[:page], per_page: 30)
|
||||||
paginate(page: params[:page], per_page: 30)
|
|
||||||
|
|
||||||
if @selected_species
|
@pet_types.where!(species_id: @selected_species) if @selected_species
|
||||||
@pet_types = @pet_types.where(species_id: @selected_species)
|
@pet_types.where!(color_id: @selected_color) if @selected_color
|
||||||
end
|
if @selected_order == :newest
|
||||||
if @selected_color
|
@pet_types.order!(created_at: :desc)
|
||||||
@pet_types = @pet_types.where(color_id: @selected_color)
|
elsif @selected_order == :alphabetical
|
||||||
|
@pet_types.merge!(Color.alphabetical).merge!(Species.alphabetical)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @selected_species && @selected_color && @pet_types.size == 1
|
||||||
|
redirect_to @pet_types.first
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
format.json {
|
||||||
|
if stale?(etag: PetState.last_updated_key)
|
||||||
|
render json: {
|
||||||
|
species: Species.order(:name).all,
|
||||||
|
colors: Color.order(:name).all,
|
||||||
|
supported_poses: PetState.all_supported_poses,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -46,9 +70,7 @@ class PetTypesController < ApplicationController
|
||||||
color_id: params[:color_id],
|
color_id: params[:color_id],
|
||||||
)
|
)
|
||||||
elsif params[:name]
|
elsif params[:name]
|
||||||
color_name, _, species_name = params[:name].rpartition("-")
|
PetType.find_by_param!(params[:name])
|
||||||
raise ActiveRecord::RecordNotFound if species_name.blank?
|
|
||||||
PetType.matching_name(color_name, species_name).first!
|
|
||||||
else
|
else
|
||||||
raise "expected params: species_id and color_id, or name"
|
raise "expected params: species_id and color_id, or name"
|
||||||
end
|
end
|
||||||
|
@ -59,11 +81,12 @@ class PetTypesController < ApplicationController
|
||||||
#
|
#
|
||||||
# If no main poses are available, then we just make all the poses
|
# If no main poses are available, then we just make all the poses
|
||||||
# "canonical", and show the whole mish-mash!
|
# "canonical", and show the whole mish-mash!
|
||||||
MAIN_POSES = %w(HAPPY_FEM HAPPY_MASC SAD_FEM SAD_MASC SICK_FEM SICK_MASC)
|
|
||||||
def group_pet_states(pet_states)
|
def group_pet_states(pet_states)
|
||||||
pose_groups = pet_states.emotion_order.group_by(&:pose)
|
pose_groups = pet_states.emotion_order.group_by(&:pose)
|
||||||
main_groups = pose_groups.select { |k| MAIN_POSES.include?(k) }.values
|
main_groups =
|
||||||
other_groups = pose_groups.reject { |k| MAIN_POSES.include?(k) }.values
|
pose_groups.select { |k| PetState::MAIN_POSES.include?(k) }.values
|
||||||
|
other_groups =
|
||||||
|
pose_groups.reject { |k| PetState::MAIN_POSES.include?(k) }.values
|
||||||
|
|
||||||
if main_groups.empty?
|
if main_groups.empty?
|
||||||
return {canonical: other_groups.flatten(1).sort_by(&:pose), other: []}
|
return {canonical: other_groups.flatten(1).sort_by(&:pose), other: []}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
class PetsController < ApplicationController
|
class PetsController < ApplicationController
|
||||||
rescue_from Pet::PetNotFound, with: :pet_not_found
|
rescue_from Neopets::CustomPets::PetNotFound, with: :pet_not_found
|
||||||
rescue_from PetType::DownloadError, SwfAsset::DownloadError, with: :asset_download_error
|
rescue_from Neopets::CustomPets::DownloadError, with: :pet_download_error
|
||||||
rescue_from Pet::DownloadError, with: :pet_download_error
|
rescue_from Pet::ModelingDisabled, with: :modeling_disabled
|
||||||
rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format
|
rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format
|
||||||
|
|
||||||
def load
|
def load
|
||||||
# Uncomment this to temporarily disable modeling for most users.
|
raise Neopets::CustomPets::PetNotFound unless params[:name]
|
||||||
# return modeling_disabled unless user_signed_in? && current_user.admin?
|
|
||||||
|
|
||||||
raise Pet::PetNotFound unless params[:name]
|
|
||||||
@pet = Pet.load(params[:name])
|
@pet = Pet.load(params[:name])
|
||||||
points = contribute(current_user, @pet)
|
points = contribute(current_user, @pet)
|
||||||
|
|
||||||
|
@ -48,12 +45,6 @@ class PetsController < ApplicationController
|
||||||
:status => :not_found
|
:status => :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
def asset_download_error(e)
|
|
||||||
Rails.logger.warn e.message
|
|
||||||
pet_load_error :long_message => t('pets.load.asset_download_error'),
|
|
||||||
:status => :gateway_timeout
|
|
||||||
end
|
|
||||||
|
|
||||||
def pet_download_error(e)
|
def pet_download_error(e)
|
||||||
Rails.logger.warn e.message
|
Rails.logger.warn e.message
|
||||||
Rails.logger.warn e.backtrace.join("\n")
|
Rails.logger.warn e.backtrace.join("\n")
|
||||||
|
|
13
app/helpers/alt_styles_helper.rb
Normal file
13
app/helpers/alt_styles_helper.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module AltStylesHelper
|
||||||
|
def view_or_edit_alt_style_url(alt_style)
|
||||||
|
if support_staff?
|
||||||
|
edit_alt_style_path alt_style
|
||||||
|
else
|
||||||
|
wardrobe_path(
|
||||||
|
species: alt_style.species_id,
|
||||||
|
color: alt_style.color_id,
|
||||||
|
style: alt_style.id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,5 @@
|
||||||
module OutfitsHelper
|
module OutfitsHelper
|
||||||
LAST_DAY_OF_ANNOUNCEMENT = Date.parse("2024-09-27")
|
LAST_DAY_OF_ANNOUNCEMENT = Date.parse("2024-11-08")
|
||||||
def show_announcement?
|
def show_announcement?
|
||||||
Date.today <= LAST_DAY_OF_ANNOUNCEMENT
|
Date.today <= LAST_DAY_OF_ANNOUNCEMENT
|
||||||
end
|
end
|
||||||
|
@ -70,16 +70,11 @@ module OutfitsHelper
|
||||||
text_field_tag 'name', nil, options
|
text_field_tag 'name', nil, options
|
||||||
end
|
end
|
||||||
|
|
||||||
def outfit_viewer(outfit_or_options)
|
def outfit_viewer(outfit=nil, pet_state: nil, **html_options)
|
||||||
outfit = if outfit_or_options.is_a? Hash
|
outfit = Outfit.new(pet_state:) if outfit.nil? && pet_state.present?
|
||||||
Outfit.new(outfit_or_options)
|
raise "outfit_viewer must have outfit or pet state" if outfit.nil?
|
||||||
elsif outfit_or_options.is_a? Outfit
|
|
||||||
outfit_or_options
|
|
||||||
else
|
|
||||||
raise TypeError, "must be an outfit or hash of options to create one"
|
|
||||||
end
|
|
||||||
|
|
||||||
render partial: "outfit_viewer", locals: {outfit:}
|
render partial: "outfit_viewer", locals: {outfit:, html_options:}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,26 @@ module PetStatesHelper
|
||||||
when "UNCONVERTED"
|
when "UNCONVERTED"
|
||||||
"Unconverted"
|
"Unconverted"
|
||||||
else
|
else
|
||||||
"(Unknown)"
|
"Not labeled yet"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
POSE_OPTIONS = %w(HAPPY_FEM SAD_FEM SICK_FEM HAPPY_MASC SAD_MASC SICK_MASC
|
||||||
|
UNCONVERTED UNKNOWN)
|
||||||
|
def pose_options
|
||||||
|
POSE_OPTIONS
|
||||||
|
end
|
||||||
|
|
||||||
|
def useful_pet_state_path(pet_type, pet_state)
|
||||||
|
if support_staff?
|
||||||
|
edit_pet_type_pet_state_path(pet_type, pet_state)
|
||||||
|
else
|
||||||
|
wardrobe_path(
|
||||||
|
color: pet_type.color_id,
|
||||||
|
species: pet_type.species_id,
|
||||||
|
pose: pet_state.pose,
|
||||||
|
state: pet_state.id,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
16
app/helpers/pet_types_helper.rb
Normal file
16
app/helpers/pet_types_helper.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module PetTypesHelper
|
||||||
|
def moon_progress(num, total)
|
||||||
|
nearest_quarter = (4.0 * num / total).round / 4.0
|
||||||
|
if nearest_quarter >= 1
|
||||||
|
"🌕️"
|
||||||
|
elsif nearest_quarter >= 0.75
|
||||||
|
"🌔"
|
||||||
|
elsif nearest_quarter >= 0.5
|
||||||
|
"🌓"
|
||||||
|
elsif nearest_quarter >= 0.25
|
||||||
|
"🌒"
|
||||||
|
else
|
||||||
|
"🌑"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -777,8 +777,13 @@ function StyleExplanation() {
|
||||||
opacity="0.7"
|
opacity="0.7"
|
||||||
marginTop="2"
|
marginTop="2"
|
||||||
>
|
>
|
||||||
<Box as="a" href="/alt-styles" target="_blank" textDecoration="underline">
|
<Box
|
||||||
Alt Styles
|
as="a"
|
||||||
|
href="/rainbow-pool/styles"
|
||||||
|
target="_blank"
|
||||||
|
textDecoration="underline"
|
||||||
|
>
|
||||||
|
Pet Styles
|
||||||
</Box>{" "}
|
</Box>{" "}
|
||||||
are NC items that override the pet's appearance via the{" "}
|
are NC items that override the pet's appearance via the{" "}
|
||||||
<Box
|
<Box
|
||||||
|
@ -789,7 +794,7 @@ function StyleExplanation() {
|
||||||
>
|
>
|
||||||
Styling Chamber
|
Styling Chamber
|
||||||
</Box>
|
</Box>
|
||||||
. Not all items fit Alt Style pets. The pet's color doesn't have to match.
|
. Not all items fit all Pet Styles. The pet's color doesn't have to match.
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,49 +4,68 @@ class AltStyle < ApplicationRecord
|
||||||
belongs_to :species
|
belongs_to :species
|
||||||
belongs_to :color
|
belongs_to :color
|
||||||
|
|
||||||
has_many :parent_swf_asset_relationships, as: :parent
|
has_many :parent_swf_asset_relationships, as: :parent, dependent: :destroy
|
||||||
has_many :swf_assets, through: :parent_swf_asset_relationships
|
has_many :swf_assets, through: :parent_swf_asset_relationships
|
||||||
has_many :contributions, as: :contributed, inverse_of: :contributed
|
has_many :contributions, as: :contributed, inverse_of: :contributed
|
||||||
|
|
||||||
validates :body_id, presence: true
|
validates :body_id, presence: true
|
||||||
|
validates :series_name, presence: true, allow_nil: true
|
||||||
|
validates :thumbnail_url, presence: true
|
||||||
|
|
||||||
before_create :infer_series_name
|
before_validation :infer_thumbnail_url, unless: :thumbnail_url?
|
||||||
before_create :infer_thumbnail_url
|
|
||||||
|
|
||||||
scope :matching_name, ->(series_name, color_name, species_name) {
|
scope :matching_name, ->(series_name, color_name, species_name) {
|
||||||
color = Color.find_by_name!(color_name)
|
color = Color.find_by_name!(color_name)
|
||||||
species = Species.find_by_name!(species_name)
|
species = Species.find_by_name!(species_name)
|
||||||
where(series_name:, color_id: color.id, species_id: species.id)
|
where(series_name:, color_id: color.id, species_id: species.id)
|
||||||
}
|
}
|
||||||
|
scope :by_creation_date, -> {
|
||||||
|
order("DATE(created_at) DESC")
|
||||||
|
}
|
||||||
|
scope :unlabeled, -> { where(series_name: nil) }
|
||||||
|
scope :newest, -> { order(created_at: :desc) }
|
||||||
|
|
||||||
def name
|
def pet_name
|
||||||
I18n.translate('pet_types.human_name', color_human_name: color.human_name,
|
I18n.translate('pet_types.human_name', color_human_name: color.human_name,
|
||||||
species_human_name: species.human_name)
|
species_human_name: species.human_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
alias_method :name, :pet_name
|
||||||
|
|
||||||
# If the series_name hasn't yet been set manually by support staff, show the
|
# If the series_name hasn't yet been set manually by support staff, show the
|
||||||
# string "<New?>" instead. But it won't be searchable by that string—that is,
|
# string "<New?>" instead. But it won't be searchable by that string—that is,
|
||||||
# `fits:<New?>-faerie-draik` intentionally will not work, and the canonical
|
# `fits:<New?>-faerie-draik` intentionally will not work, and the canonical
|
||||||
# filter name will be `fits:alt-style-IDNUMBER`, instead.
|
# filter name will be `fits:alt-style-IDNUMBER`, instead.
|
||||||
def series_name
|
def series_name
|
||||||
self[:series_name] || "<New?>"
|
real_series_name || "<New?>"
|
||||||
|
end
|
||||||
|
|
||||||
|
def real_series_name=(new_series_name)
|
||||||
|
self[:series_name] = new_series_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def real_series_name
|
||||||
|
self[:series_name]
|
||||||
end
|
end
|
||||||
|
|
||||||
# You can use this to check whether `series_name` is returning the actual
|
# You can use this to check whether `series_name` is returning the actual
|
||||||
# value or its placeholder value.
|
# value or its placeholder value.
|
||||||
def has_real_series_name?
|
def real_series_name?
|
||||||
self[:series_name].present?
|
real_series_name.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def adjective_name
|
def adjective_name
|
||||||
"#{series_name} #{color.human_name}"
|
"#{series_name} #{color.human_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview_image_url
|
def full_name
|
||||||
swf_asset = swf_assets.first
|
"#{series_name} #{name}"
|
||||||
return nil if swf_asset.nil?
|
end
|
||||||
|
|
||||||
swf_asset.image_url
|
EMPTY_IMAGE_URL = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||||
|
def preview_image_url
|
||||||
|
# Use the image URL for the first asset. Or, fall back to an empty image.
|
||||||
|
swf_assets.first&.image_url || EMPTY_IMAGE_URL
|
||||||
end
|
end
|
||||||
|
|
||||||
# Given a list of items, return how they look on this alt style.
|
# Given a list of items, return how they look on this alt style.
|
||||||
|
@ -54,28 +73,6 @@ class AltStyle < ApplicationRecord
|
||||||
Item.appearances_for(items, self, ...)
|
Item.appearances_for(items, self, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
def biology=(biology)
|
|
||||||
# TODO: This is very similar to what `PetState` does, but like… much much
|
|
||||||
# more compact? Idk if I'm missing something, or if I was just that much
|
|
||||||
# more clueless back when I wrote it, lol 😅
|
|
||||||
self.swf_assets = biology.values.map do |asset_data|
|
|
||||||
SwfAsset.from_biology_data(self.body_id, asset_data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Until the end of 2024, assume new alt styles are from the "Nostalgic"
|
|
||||||
# series. That way, we can stop having to manually label them all as they
|
|
||||||
# come out and get modeled (TNT is prolific rn!), but we aren't gonna get too
|
|
||||||
# greedy and forget about this and use Nostalgic for some far-future thing,
|
|
||||||
# in ways that will certainly be fixable but would also be confusing and
|
|
||||||
# embarrassing.
|
|
||||||
NOSTALGIC_FINAL_DAY = Date.new(2024, 12, 31)
|
|
||||||
def infer_series_name
|
|
||||||
if !has_real_series_name? && Date.today <= NOSTALGIC_FINAL_DAY
|
|
||||||
self.series_name = "Nostalgic"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# At time of writing, most batches of Alt Styles thumbnails used a simple
|
# At time of writing, most batches of Alt Styles thumbnails used a simple
|
||||||
# pattern for the item thumbnail URL, but that's not always the case anymore.
|
# pattern for the item thumbnail URL, but that's not always the case anymore.
|
||||||
# For now, let's keep using this format as the default value when creating a
|
# For now, let's keep using this format as the default value when creating a
|
||||||
|
@ -85,7 +82,7 @@ class AltStyle < ApplicationRecord
|
||||||
)
|
)
|
||||||
DEFAULT_THUMBNAIL_URL = "https://images.neopets.com/items/mall_bg_circle.gif"
|
DEFAULT_THUMBNAIL_URL = "https://images.neopets.com/items/mall_bg_circle.gif"
|
||||||
def infer_thumbnail_url
|
def infer_thumbnail_url
|
||||||
if has_real_series_name?
|
if real_series_name?
|
||||||
self.thumbnail_url = THUMBNAIL_URL_TEMPLATE.expand(
|
self.thumbnail_url = THUMBNAIL_URL_TEMPLATE.expand(
|
||||||
series: series_name.gsub(/\s+/, '').downcase,
|
series: series_name.gsub(/\s+/, '').downcase,
|
||||||
color: color.name.gsub(/\s+/, '').downcase,
|
color: color.name.gsub(/\s+/, '').downcase,
|
||||||
|
@ -96,6 +93,10 @@ class AltStyle < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def real_thumbnail_url?
|
||||||
|
thumbnail_url != DEFAULT_THUMBNAIL_URL
|
||||||
|
end
|
||||||
|
|
||||||
# For convenience in the console!
|
# For convenience in the console!
|
||||||
def self.find_by_name(color_name, species_name)
|
def self.find_by_name(color_name, species_name)
|
||||||
color = Color.find_by_name(color_name)
|
color = Color.find_by_name(color_name)
|
||||||
|
|
|
@ -161,7 +161,7 @@ class AuthUser < AuthRecord
|
||||||
# means we can wrap it in a `with_timeout` block!)
|
# means we can wrap it in a `with_timeout` block!)
|
||||||
neopets_username = Sync do |task|
|
neopets_username = Sync do |task|
|
||||||
task.with_timeout(5) do
|
task.with_timeout(5) do
|
||||||
NeoPass.load_main_neopets_username(auth.credentials.token)
|
Neopets::NeoPass.load_main_neopets_username(auth.credentials.token)
|
||||||
end
|
end
|
||||||
rescue Async::TimeoutError
|
rescue Async::TimeoutError
|
||||||
nil # If the request times out, just move on!
|
nil # If the request times out, just move on!
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
class Color < ApplicationRecord
|
class Color < ApplicationRecord
|
||||||
has_many :pet_types
|
has_many :pet_types
|
||||||
|
has_many :alt_styles
|
||||||
|
|
||||||
scope :alphabetical, -> { order(:name) }
|
scope :alphabetical, -> { order(:name) }
|
||||||
scope :basic, -> { where(basic: true) }
|
scope :basic, -> { where(basic: true) }
|
||||||
scope :standard, -> { where(standard: true) }
|
scope :standard, -> { where(standard: true) }
|
||||||
scope :nonstandard, -> { where(standard: false) }
|
scope :nonstandard, -> { where(standard: false) }
|
||||||
scope :funny, -> { order(:prank) unless pranks_funny? }
|
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
|
@ -14,27 +14,23 @@ class Color < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def human_name
|
def human_name
|
||||||
if prank? && !Color.pranks_funny?
|
if name
|
||||||
unfunny_human_name + ' ' + I18n.translate('colors.prank_suffix')
|
name.split(' ').map { |word| word.capitalize }.join(' ')
|
||||||
else
|
else
|
||||||
unfunny_human_name
|
I18n.translate('colors.default_human_name')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
name? ? human_name : id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
def example_pet_type(preferred_species: nil)
|
def example_pet_type(preferred_species: nil)
|
||||||
preferred_species ||= Species.first
|
preferred_species ||= Species.first
|
||||||
pet_types.order([Arel.sql("species_id = ? DESC"), preferred_species.id],
|
pet_types.order([Arel.sql("species_id = ? DESC"), preferred_species.id],
|
||||||
"species_id ASC").first
|
"species_id ASC").first
|
||||||
end
|
end
|
||||||
|
|
||||||
def unfunny_human_name
|
|
||||||
if name
|
|
||||||
name.split(' ').map { |word| word.capitalize }.join(' ')
|
|
||||||
else
|
|
||||||
I18n.translate('colors.default_human_name')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def default_gender_presentation
|
def default_gender_presentation
|
||||||
if name.downcase.ends_with? "boy"
|
if name.downcase.ends_with? "boy"
|
||||||
:masc
|
:masc
|
||||||
|
@ -45,8 +41,7 @@ class Color < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.pranks_funny?
|
def self.param_to_id(param)
|
||||||
now = Time.now.in_time_zone('Pacific Time (US & Canada)')
|
param.match?(/\A\d+\Z/) ? param.to_i : find_by_name!(param).id
|
||||||
now.month == 4 && now.day == 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,16 +10,29 @@ class Item < ApplicationRecord
|
||||||
|
|
||||||
SwfAssetType = 'object'
|
SwfAssetType = 'object'
|
||||||
|
|
||||||
|
serialize :cached_compatible_body_ids, coder: Serializers::IntegerSet
|
||||||
|
serialize :cached_occupied_zone_ids, coder: Serializers::IntegerSet
|
||||||
|
|
||||||
has_many :closet_hangers
|
has_many :closet_hangers
|
||||||
has_one :contribution, :as => :contributed, :inverse_of => :contributed
|
has_one :contribution, as: :contributed, inverse_of: :contributed
|
||||||
has_one :nc_mall_record
|
has_one :nc_mall_record
|
||||||
has_many :parent_swf_asset_relationships, :as => :parent
|
has_many :parent_swf_asset_relationships, as: :parent
|
||||||
has_many :swf_assets, :through => :parent_swf_asset_relationships
|
has_many :swf_assets, through: :parent_swf_asset_relationships
|
||||||
belongs_to :dyeworks_base_item, class_name: "Item",
|
belongs_to :dyeworks_base_item, class_name: "Item",
|
||||||
default: -> { inferred_dyeworks_base_item }, optional: true
|
default: -> { inferred_dyeworks_base_item }, optional: true
|
||||||
has_many :dyeworks_variants, class_name: "Item",
|
has_many :dyeworks_variants, class_name: "Item",
|
||||||
inverse_of: :dyeworks_base_item
|
inverse_of: :dyeworks_base_item
|
||||||
|
|
||||||
|
# We require a name field. A number of other fields must be *specified*: they
|
||||||
|
# can't be nil, to help ensure we aren't forgetting any fields when importing
|
||||||
|
# items. But sometimes they happen to be blank (e.g. when TNT leaves an item
|
||||||
|
# description empty, oops), in which case we want to accept that reality!
|
||||||
|
validates_presence_of :name
|
||||||
|
validates :description, :thumbnail_url, :rarity, :price, :zones_restrict,
|
||||||
|
exclusion: {in: [nil], message: "must be specified"}
|
||||||
|
|
||||||
|
after_save :update_cached_fields,
|
||||||
|
if: :modeling_status_hint_previously_changed?
|
||||||
|
|
||||||
attr_writer :current_body_id, :owned, :wanted
|
attr_writer :current_body_id, :owned, :wanted
|
||||||
|
|
||||||
|
@ -60,39 +73,25 @@ class Item < ApplicationRecord
|
||||||
where('description NOT LIKE ?',
|
where('description NOT LIKE ?',
|
||||||
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
||||||
}
|
}
|
||||||
|
scope :is_modeled, -> {
|
||||||
|
where(cached_predicted_fully_modeled: true)
|
||||||
|
}
|
||||||
|
scope :is_not_modeled, -> {
|
||||||
|
where(cached_predicted_fully_modeled: false)
|
||||||
|
}
|
||||||
scope :occupies, ->(zone_label) {
|
scope :occupies, ->(zone_label) {
|
||||||
zone_ids = Zone.matching_label(zone_label).map(&:id)
|
Zone.matching_label(zone_label).
|
||||||
|
map { |z| occupies_zone_id(z.id) }.reduce(none, &:or)
|
||||||
# NOTE: In searches, this query performs much better using a subquery
|
|
||||||
# instead of joins! This is because, in the joins case, filtering by an
|
|
||||||
# `swf_assets` field but sorting by an `items` field causes the query
|
|
||||||
# planner to only be able to use an index for *one* of them. In this case,
|
|
||||||
# MySQL can use the `swf_assets`.`zone_id` index to get the item IDs for
|
|
||||||
# the subquery, then use the `items`.`name` index to sort them.
|
|
||||||
i = arel_table
|
|
||||||
psa = ParentSwfAssetRelationship.arel_table
|
|
||||||
sa = SwfAsset.arel_table
|
|
||||||
where(
|
|
||||||
ParentSwfAssetRelationship.joins(:swf_asset).
|
|
||||||
where(sa[:zone_id].in(zone_ids)).
|
|
||||||
where(psa[:parent_type].eq("Item")).
|
|
||||||
where(psa[:parent_id].eq(i[:id])).
|
|
||||||
arel.exists
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
scope :not_occupies, ->(zone_label) {
|
scope :not_occupies, ->(zone_label) {
|
||||||
zone_ids = Zone.matching_label(zone_label).map(&:id)
|
Zone.matching_label(zone_label).
|
||||||
i = Item.arel_table
|
map { |z| not_occupies_zone_id(z.id) }.reduce(all, &:and)
|
||||||
sa = SwfAsset.arel_table
|
}
|
||||||
# Querying for "has NO swf_assets matching these zone IDs" is trickier than
|
scope :occupies_zone_id, ->(zone_id) {
|
||||||
# the positive case! To do it, we GROUP_CONCAT the zone_ids together for
|
where("FIND_IN_SET(?, cached_occupied_zone_ids) > 0", zone_id)
|
||||||
# each item, then use FIND_IN_SET to search the result for each zone ID,
|
}
|
||||||
# and assert that it must not find a match. (This is uhh, not exactly fast,
|
scope :not_occupies_zone_id, ->(zone_id) {
|
||||||
# so it helps to have other tighter conditions applied first!)
|
where.not("FIND_IN_SET(?, cached_occupied_zone_ids) > 0", zone_id)
|
||||||
# TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
|
|
||||||
# performs any better? In Rails 5+ `left_outer_joins` is built in so!
|
|
||||||
condition = zone_ids.map { 'FIND_IN_SET(?, GROUP_CONCAT(zone_id)) = 0' }.join(' AND ')
|
|
||||||
joins(:swf_assets).group(i[:id]).having(condition, *zone_ids).distinct
|
|
||||||
}
|
}
|
||||||
scope :restricts, ->(zone_label) {
|
scope :restricts, ->(zone_label) {
|
||||||
zone_ids = Zone.matching_label(zone_label).map(&:id)
|
zone_ids = Zone.matching_label(zone_label).map(&:id)
|
||||||
|
@ -105,31 +104,12 @@ class Item < ApplicationRecord
|
||||||
where("NOT (#{condition})", *zone_ids)
|
where("NOT (#{condition})", *zone_ids)
|
||||||
}
|
}
|
||||||
scope :fits, ->(body_id) {
|
scope :fits, ->(body_id) {
|
||||||
joins(:swf_assets).where(swf_assets: {body_id: [body_id, 0]}).distinct
|
where("FIND_IN_SET(?, cached_compatible_body_ids) > 0", body_id).
|
||||||
|
or(where("FIND_IN_SET('0', cached_compatible_body_ids) > 0"))
|
||||||
}
|
}
|
||||||
scope :not_fits, ->(body_id) {
|
scope :not_fits, ->(body_id) {
|
||||||
i = Item.arel_table
|
where.not("FIND_IN_SET(?, cached_compatible_body_ids) > 0", body_id).
|
||||||
sa = SwfAsset.arel_table
|
and(where.not("FIND_IN_SET('0', cached_compatible_body_ids) > 0"))
|
||||||
# Querying for "has NO swf_assets matching these body IDs" is trickier than
|
|
||||||
# the positive case! To do it, we GROUP_CONCAT the body_ids together for
|
|
||||||
# each item, then use FIND_IN_SET to search the result for the body ID,
|
|
||||||
# and assert that it must not find a match. (This is uhh, not exactly fast,
|
|
||||||
# so it helps to have other tighter conditions applied first!)
|
|
||||||
#
|
|
||||||
# TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
|
|
||||||
# performs any better? In Rails 5+ `left_outer_joins` is built in so!
|
|
||||||
#
|
|
||||||
# NOTE: The `fits` and `not_fits` counts don't perfectly add up to the
|
|
||||||
# total number of items, 5 items aren't accounted for? I'm not going to
|
|
||||||
# bother looking into this, but one thing I notice is items with no assets
|
|
||||||
# somehow would not match either scope in this impl (but LEFT JOIN would!)
|
|
||||||
joins(:swf_assets).group(i[:id]).
|
|
||||||
having(
|
|
||||||
"FIND_IN_SET(?, GROUP_CONCAT(body_id)) = 0 AND " +
|
|
||||||
"FIND_IN_SET(0, GROUP_CONCAT(body_id)) = 0",
|
|
||||||
body_id
|
|
||||||
).
|
|
||||||
distinct
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def nc_trade_value
|
def nc_trade_value
|
||||||
|
@ -296,6 +276,23 @@ class Item < ApplicationRecord
|
||||||
restricted_zones + occupied_zones
|
restricted_zones + occupied_zones
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_cached_fields
|
||||||
|
# First, clear out some cached instance variables we use for performance,
|
||||||
|
# to ensure we recompute the latest values.
|
||||||
|
@predicted_body_ids = nil
|
||||||
|
@predicted_missing_body_ids = nil
|
||||||
|
|
||||||
|
# We also need to reload our associations, so they include any new records.
|
||||||
|
swf_assets.reload
|
||||||
|
|
||||||
|
# Finally, compute and save our cached fields.
|
||||||
|
self.cached_occupied_zone_ids = occupied_zone_ids
|
||||||
|
self.cached_compatible_body_ids = compatible_body_ids(use_cached: false)
|
||||||
|
self.cached_predicted_fully_modeled =
|
||||||
|
predicted_fully_modeled?(use_cached: false)
|
||||||
|
self.save!
|
||||||
|
end
|
||||||
|
|
||||||
def species_support_ids
|
def species_support_ids
|
||||||
@species_support_ids_array ||= read_attribute('species_support_ids').split(',').map(&:to_i) rescue nil
|
@species_support_ids_array ||= read_attribute('species_support_ids').split(',').map(&:to_i) rescue nil
|
||||||
end
|
end
|
||||||
|
@ -306,69 +303,82 @@ class Item < ApplicationRecord
|
||||||
write_attribute('species_support_ids', replacement)
|
write_attribute('species_support_ids', replacement)
|
||||||
end
|
end
|
||||||
|
|
||||||
def support_species?(species)
|
def modeling_hinted_done?
|
||||||
species_support_ids.blank? || species_support_ids.include?(species.id)
|
modeling_status_hint == "done" || modeling_status_hint == "glitchy"
|
||||||
end
|
|
||||||
|
|
||||||
def modeled_body_ids
|
|
||||||
@modeled_body_ids ||= swf_assets.select('DISTINCT body_id').map(&:body_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def modeled_color_ids
|
|
||||||
# Might be empty if modeled_body_ids is 0. But it's currently not called
|
|
||||||
# in that scenario, so, whatever.
|
|
||||||
@modeled_color_ids ||= PetType.select('DISTINCT color_id').
|
|
||||||
where(body_id: modeled_body_ids).
|
|
||||||
map(&:color_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def basic_body_ids
|
|
||||||
@basic_body_ids ||= begin
|
|
||||||
basic_color_ids ||= Color.select([:id]).basic.map(&:id)
|
|
||||||
PetType.select('DISTINCT body_id').
|
|
||||||
where(color_id: basic_color_ids).map(&:body_id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def predicted_body_ids
|
def predicted_body_ids
|
||||||
@predicted_body_ids ||= if modeled_body_ids.include?(0)
|
@predicted_body_ids ||= if modeling_hinted_done?
|
||||||
|
# If we've manually set this item to no longer report as needing modeling,
|
||||||
|
# predict that the current bodies are all of the compatible bodies.
|
||||||
|
compatible_body_ids
|
||||||
|
elsif compatible_body_ids.include?(0)
|
||||||
# Oh, look, it's already known to fit everybody! Sweet. We're done. (This
|
# Oh, look, it's already known to fit everybody! Sweet. We're done. (This
|
||||||
# isn't folded into the case below, in case this item somehow got a
|
# isn't folded into the case below, in case this item somehow got a
|
||||||
# body-specific and non-body-specific asset. In all the cases I've seen
|
# body-specific and non-body-specific asset. In all the cases I've seen
|
||||||
# it, that indicates a glitched item, but this method chooses to reflect
|
# it, that indicates a glitched item, but this method chooses to reflect
|
||||||
# behavior elsewhere in the app by saying that we can put this item on
|
# behavior elsewhere in the app by saying that we can put this item on
|
||||||
# anybody. (Heh. Any body.))
|
# anybody. (Heh. Any body.))
|
||||||
modeled_body_ids
|
compatible_body_ids
|
||||||
elsif modeled_body_ids.size == 1
|
elsif compatible_body_ids.size == 1
|
||||||
# This might just be a species-specific item. Let's be conservative in
|
# This might just be a species-specific item. Let's be conservative in
|
||||||
# our prediction, though we'll revise it if we see another body ID.
|
# our prediction, though we'll revise it if we see another body ID.
|
||||||
modeled_body_ids
|
compatible_body_ids
|
||||||
|
elsif compatible_body_ids.size == 0
|
||||||
|
# If somehow we have this item, but not any modeling data for it (weird!),
|
||||||
|
# consider it to fit all standard pet types until shown otherwise.
|
||||||
|
PetType.basic.released_before(released_at_estimate).
|
||||||
|
distinct.pluck(:body_id).sort
|
||||||
else
|
else
|
||||||
# If an item is worn by more than one body, then it must be wearable by
|
# First, find our compatible pet types, then pair each body ID with its
|
||||||
# all bodies of the same color. (To my knowledge, anyway. I'm not aware
|
# color. (As an optimization, we omit standard colors, other than the
|
||||||
# of any exceptions.) So, let's find those bodies by first finding those
|
# basic colors. We also flatten the basic colors into the single color
|
||||||
# colors.
|
# ID "basic", so we can treat them specially.)
|
||||||
basic_modeled_body_ids, nonbasic_modeled_body_ids = modeled_body_ids.
|
compatible_pairs = compatible_pet_types.joins(:color).
|
||||||
partition { |bi| basic_body_ids.include?(bi) }
|
merge(Color.nonstandard.or(Color.basic)).
|
||||||
|
distinct.pluck(
|
||||||
|
Arel.sql("IF(colors.basic, 'basic', colors.id)"), :body_id)
|
||||||
|
|
||||||
output = []
|
# Group colors by body, to help us find bodies unique to certain colors.
|
||||||
if basic_modeled_body_ids.present?
|
compatible_color_ids_by_body_id = {}.tap do |h|
|
||||||
output += basic_body_ids
|
compatible_pairs.each do |(color_id, body_id)|
|
||||||
|
h[body_id] ||= []
|
||||||
|
h[body_id] << color_id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if nonbasic_modeled_body_ids.present?
|
|
||||||
nonbasic_modeled_color_ids = PetType.select('DISTINCT color_id').
|
# Find non-basic colors with at least one unique compatible body. (This
|
||||||
where(body_id: nonbasic_modeled_body_ids).
|
# means we'll ignore e.g. the Maraquan Mynci, which has the same body as
|
||||||
map(&:color_id)
|
# the Blue Mynci, as not indicating Maraquan compatibility in general.)
|
||||||
output += PetType.select('DISTINCT body_id').
|
modelable_color_ids =
|
||||||
where(color_id: nonbasic_modeled_color_ids).
|
compatible_color_ids_by_body_id.
|
||||||
map(&:body_id)
|
filter { |k, v| v.size == 1 && v.first != "basic" }.
|
||||||
end
|
values.map(&:first).uniq
|
||||||
output
|
|
||||||
|
# We can model on basic pets (perhaps in addition to the above) if we
|
||||||
|
# find at least one compatible basic body that doesn't *also* fit any of
|
||||||
|
# the modelable colors we identified above.
|
||||||
|
basic_is_modelable =
|
||||||
|
compatible_color_ids_by_body_id.values.
|
||||||
|
any? { |v| v.include?("basic") && (v & modelable_color_ids).empty? }
|
||||||
|
|
||||||
|
# Filter to pet types that match the colors that seem compatible.
|
||||||
|
predicted_pet_types =
|
||||||
|
(basic_is_modelable ? PetType.basic : PetType.none).
|
||||||
|
or(PetType.where(color_id: modelable_color_ids))
|
||||||
|
|
||||||
|
# Only include species that were released when this item was. If we don't
|
||||||
|
# know our creation date (we don't have it for some old records), assume
|
||||||
|
# it's pretty old.
|
||||||
|
predicted_pet_types.merge! PetType.released_before(released_at_estimate)
|
||||||
|
|
||||||
|
# Get all body IDs for the pet types we decided are modelable.
|
||||||
|
predicted_pet_types.distinct.pluck(:body_id).sort
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def predicted_missing_body_ids
|
def predicted_missing_body_ids
|
||||||
@predicted_missing_body_ids ||= predicted_body_ids - modeled_body_ids
|
@predicted_missing_body_ids ||= predicted_body_ids - compatible_body_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
def predicted_missing_standard_body_ids_by_species_id
|
def predicted_missing_standard_body_ids_by_species_id
|
||||||
|
@ -388,9 +398,8 @@ class Item < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def predicted_missing_nonstandard_body_pet_types
|
def predicted_missing_nonstandard_body_pet_types
|
||||||
PetType.joins(:color).
|
body_ids = predicted_missing_body_ids - PetType.basic_body_ids
|
||||||
where(body_id: predicted_missing_body_ids - basic_body_ids,
|
PetType.joins(:color).where(body_id: body_ids, colors: {standard: false})
|
||||||
colors: {standard: false})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def predicted_missing_nonstandard_body_ids_by_species_by_color
|
def predicted_missing_nonstandard_body_ids_by_species_by_color
|
||||||
|
@ -415,12 +424,19 @@ class Item < ApplicationRecord
|
||||||
body_ids_by_species_by_color
|
body_ids_by_species_by_color
|
||||||
end
|
end
|
||||||
|
|
||||||
def predicted_fully_modeled?
|
def predicted_fully_modeled?(use_cached: true)
|
||||||
|
return cached_predicted_fully_modeled? if use_cached
|
||||||
predicted_missing_body_ids.empty?
|
predicted_missing_body_ids.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def predicted_modeled_ratio
|
def predicted_modeled_ratio
|
||||||
modeled_body_ids.size.to_f / predicted_body_ids.size
|
compatible_body_ids.size.to_f / predicted_body_ids.size
|
||||||
|
end
|
||||||
|
|
||||||
|
# We estimate the item's release time as either when we first saw it, or 2010
|
||||||
|
# if it's so old that we don't have a record.
|
||||||
|
def released_at_estimate
|
||||||
|
created_at || Time.new(2010)
|
||||||
end
|
end
|
||||||
|
|
||||||
def as_json(options={})
|
def as_json(options={})
|
||||||
|
@ -430,7 +446,9 @@ class Item < ApplicationRecord
|
||||||
}.merge(options))
|
}.merge(options))
|
||||||
end
|
end
|
||||||
|
|
||||||
def compatible_body_ids
|
def compatible_body_ids(use_cached: true)
|
||||||
|
return cached_compatible_body_ids if use_cached
|
||||||
|
|
||||||
swf_assets.map(&:body_id).uniq
|
swf_assets.map(&:body_id).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ class Item
|
||||||
)\z
|
)\z
|
||||||
}x
|
}x
|
||||||
def inferred_dyeworks_base_item
|
def inferred_dyeworks_base_item
|
||||||
name_match = name.match(DYEWORKS_NAME_PATTERN)
|
name_match = (name || "").match(DYEWORKS_NAME_PATTERN)
|
||||||
return nil if name_match.nil?
|
return nil if name_match.nil?
|
||||||
|
|
||||||
Item.find_by_name(name_match["base"])
|
Item.find_by_name(name_match["base"])
|
||||||
|
|
|
@ -132,6 +132,8 @@ class Item
|
||||||
is_positive ? Filter.is_np : Filter.is_not_np
|
is_positive ? Filter.is_np : Filter.is_not_np
|
||||||
when 'pb'
|
when 'pb'
|
||||||
is_positive ? Filter.is_pb : Filter.is_not_pb
|
is_positive ? Filter.is_pb : Filter.is_not_pb
|
||||||
|
when 'modeled'
|
||||||
|
is_positive ? Filter.is_modeled : Filter.is_not_modeled
|
||||||
else
|
else
|
||||||
raise_search_error "not_found.label", label: "is:#{value}"
|
raise_search_error "not_found.label", label: "is:#{value}"
|
||||||
end
|
end
|
||||||
|
@ -346,6 +348,14 @@ class Item
|
||||||
self.new Item.is_not_pb, '-is:pb'
|
self.new Item.is_not_pb, '-is:pb'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.is_modeled
|
||||||
|
self.new Item.is_modeled, 'is:modeled'
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.is_not_modeled
|
||||||
|
self.new Item.is_not_modeled, '-is:modeled'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Add quotes around the value, if needed.
|
# Add quotes around the value, if needed.
|
||||||
|
@ -367,7 +377,7 @@ class Item
|
||||||
# If the real series name has been set in the database by support
|
# If the real series name has been set in the database by support
|
||||||
# staff, use that for the canonical filter text for this alt style.
|
# staff, use that for the canonical filter text for this alt style.
|
||||||
# Otherwise, represent this alt style by ID.
|
# Otherwise, represent this alt style by ID.
|
||||||
if alt_style.has_real_series_name?
|
if alt_style.real_series_name?
|
||||||
series_name = alt_style.series_name.downcase
|
series_name = alt_style.series_name.downcase
|
||||||
color_name = alt_style.color.name.downcase
|
color_name = alt_style.color.name.downcase
|
||||||
species_name = alt_style.species.name.downcase
|
species_name = alt_style.species.name.downcase
|
||||||
|
|
|
@ -5,6 +5,9 @@ class ParentSwfAssetRelationship < ApplicationRecord
|
||||||
|
|
||||||
belongs_to :swf_asset
|
belongs_to :swf_asset
|
||||||
|
|
||||||
|
after_save :update_parent_cached_fields
|
||||||
|
after_destroy :update_parent_cached_fields
|
||||||
|
|
||||||
def item=(replacement)
|
def item=(replacement)
|
||||||
self.parent = replacement
|
self.parent = replacement
|
||||||
end
|
end
|
||||||
|
@ -16,4 +19,8 @@ class ParentSwfAssetRelationship < ApplicationRecord
|
||||||
def pet_state=(replacement)
|
def pet_state=(replacement)
|
||||||
self.parent = replacement
|
self.parent = replacement
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_parent_cached_fields
|
||||||
|
parent.try(:update_cached_fields)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,82 +1,20 @@
|
||||||
require 'rocketamf_extensions/remote_gateway'
|
|
||||||
require 'ostruct'
|
|
||||||
|
|
||||||
class Pet < ApplicationRecord
|
class Pet < ApplicationRecord
|
||||||
NEOPETS_URL_ORIGIN = ENV['NEOPETS_URL_ORIGIN'] || 'https://www.neopets.com'
|
|
||||||
GATEWAY_URL = NEOPETS_URL_ORIGIN + '/amfphp/gateway.php'
|
|
||||||
GATEWAY = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL)
|
|
||||||
CUSTOM_PET_SERVICE = GATEWAY.service('CustomPetService')
|
|
||||||
PET_SERVICE = GATEWAY.service('PetService')
|
|
||||||
|
|
||||||
belongs_to :pet_type
|
belongs_to :pet_type
|
||||||
|
|
||||||
attr_reader :items, :pet_state, :alt_style
|
attr_reader :items, :pet_state, :alt_style
|
||||||
|
|
||||||
scope :with_pet_type_color_ids, ->(color_ids) {
|
|
||||||
joins(:pet_type).where(PetType.arel_table[:id].in(color_ids))
|
|
||||||
}
|
|
||||||
|
|
||||||
def load!(timeout: nil)
|
def load!(timeout: nil)
|
||||||
viewer_data = self.class.fetch_viewer_data(name, timeout:)
|
raise ModelingDisabled unless Rails.configuration.modeling_enabled
|
||||||
use_viewer_data(viewer_data)
|
|
||||||
|
viewer_data_hash = Neopets::CustomPets.fetch_viewer_data(name, timeout:)
|
||||||
|
use_modeling_snapshot(ModelingSnapshot.new(viewer_data_hash))
|
||||||
end
|
end
|
||||||
|
|
||||||
def use_viewer_data(viewer_data)
|
def use_modeling_snapshot(snapshot)
|
||||||
pet_data = viewer_data[:custom_pet]
|
self.pet_type = snapshot.pet_type
|
||||||
|
@pet_state = snapshot.pet_state
|
||||||
raise UnexpectedDataFormat unless pet_data[:species_id]
|
@alt_style = snapshot.alt_style
|
||||||
raise UnexpectedDataFormat unless pet_data[:color_id]
|
@items = snapshot.items
|
||||||
raise UnexpectedDataFormat unless pet_data[:body_id]
|
|
||||||
|
|
||||||
has_alt_style = pet_data[:alt_style].present?
|
|
||||||
|
|
||||||
self.pet_type = PetType.find_or_initialize_by(
|
|
||||||
species_id: pet_data[:species_id].to_i,
|
|
||||||
color_id: pet_data[:color_id].to_i
|
|
||||||
)
|
|
||||||
|
|
||||||
begin
|
|
||||||
new_image_hash = Pet.fetch_image_hash(self.name)
|
|
||||||
rescue => error
|
|
||||||
Rails.logger.warn "Failed to load image hash: #{error.full_message}"
|
|
||||||
end
|
|
||||||
self.pet_type.image_hash = new_image_hash if new_image_hash.present?
|
|
||||||
|
|
||||||
# With an alt style, `body_id` in the biology data refers to the body ID of
|
|
||||||
# the *alt* style, not the usual pet type. (We have `original_biology` for
|
|
||||||
# *some* of the pet type's situation, but not it's body ID!)
|
|
||||||
#
|
|
||||||
# So, in the alt style case, don't update `body_id` - but if this is our
|
|
||||||
# first time seeing this pet type and it doesn't *have* a `body_id` yet,
|
|
||||||
# let's not be creating it without one. We'll need to model it without the
|
|
||||||
# alt style first. (I don't bother with a clear error message though 😅)
|
|
||||||
self.pet_type.body_id = pet_data[:body_id] unless has_alt_style
|
|
||||||
if self.pet_type.body_id.nil?
|
|
||||||
raise UnexpectedDataFormat,
|
|
||||||
"can't process alt style on first occurrence of pet type"
|
|
||||||
end
|
|
||||||
|
|
||||||
pet_state_biology = has_alt_style ? pet_data[:original_biology] :
|
|
||||||
pet_data[:biology_by_zone]
|
|
||||||
raise UnexpectedDataFormat if pet_state_biology.empty?
|
|
||||||
pet_state_biology[0] = nil # remove effects if present
|
|
||||||
@pet_state = self.pet_type.add_pet_state_from_biology! pet_state_biology
|
|
||||||
|
|
||||||
if has_alt_style
|
|
||||||
raise UnexpectedDataFormat unless pet_data[:alt_color]
|
|
||||||
raise UnexpectedDataFormat if pet_data[:biology_by_zone].empty?
|
|
||||||
|
|
||||||
@alt_style = AltStyle.find_or_initialize_by(id: pet_data[:alt_style].to_i)
|
|
||||||
@alt_style.assign_attributes(
|
|
||||||
color_id: pet_data[:alt_color].to_i,
|
|
||||||
species_id: pet_data[:species_id].to_i,
|
|
||||||
body_id: pet_data[:body_id].to_i,
|
|
||||||
biology: pet_data[:biology_by_zone],
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
@items = Item.collection_from_pet_type_and_registries(self.pet_type,
|
|
||||||
viewer_data[:object_info_registry], viewer_data[:object_asset_registry])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def wardrobe_query
|
def wardrobe_query
|
||||||
|
@ -87,6 +25,7 @@ class Pet < ApplicationRecord
|
||||||
pose: self.pet_state.pose,
|
pose: self.pet_state.pose,
|
||||||
state: self.pet_state.id,
|
state: self.pet_state.id,
|
||||||
objects: self.items.map(&:id),
|
objects: self.items.map(&:id),
|
||||||
|
style: self.alt_style ? self.alt_style.id : nil,
|
||||||
}.to_query
|
}.to_query
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -101,10 +40,7 @@ class Pet < ApplicationRecord
|
||||||
|
|
||||||
before_validation do
|
before_validation do
|
||||||
pet_type.save!
|
pet_type.save!
|
||||||
if @pet_state
|
@pet_state.save! if @pet_state
|
||||||
@pet_state.save!
|
|
||||||
@pet_state.handle_assets!
|
|
||||||
end
|
|
||||||
|
|
||||||
if @items
|
if @items
|
||||||
@items.each do |item|
|
@items.each do |item|
|
||||||
|
@ -124,60 +60,6 @@ class Pet < ApplicationRecord
|
||||||
pet
|
pet
|
||||||
end
|
end
|
||||||
|
|
||||||
# NOTE: Ideally pet requests shouldn't take this long, but Neopets can be
|
|
||||||
# slow sometimes! Since we're on the Falcon server, long timeouts shouldn't
|
|
||||||
# slow down the rest of the request queue, like it used to be in the past.
|
|
||||||
def self.fetch_viewer_data(name, timeout: 10)
|
|
||||||
request = CUSTOM_PET_SERVICE.action('getViewerData').request([name])
|
|
||||||
send_amfphp_request(request).tap do |data|
|
|
||||||
if data[:custom_pet][:name].blank?
|
|
||||||
raise PetNotFound, "Pet #{name.inspect} does not exist"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.fetch_metadata(name, timeout: 10)
|
|
||||||
# If this is an image hash "pet name", it has no metadata.
|
|
||||||
return nil if name.start_with?("@")
|
|
||||||
|
|
||||||
request = PET_SERVICE.action('getPet').request([name])
|
|
||||||
send_amfphp_request(request).tap do |data|
|
|
||||||
if data[:name].blank?
|
|
||||||
raise PetNotFound, "Pet #{name.inspect} does not exist"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Given a pet's name, load its image hash, for use in `pets.neopets.com`
|
|
||||||
# image URLs. (This corresponds to its current biology and items.)
|
|
||||||
def self.fetch_image_hash(name, timeout: 10)
|
|
||||||
# If this is an image hash "pet name", just take off the `@`!
|
|
||||||
return name[1..] if name.start_with?("@")
|
|
||||||
|
|
||||||
metadata = fetch_metadata(name, timeout:)
|
|
||||||
metadata[:hash]
|
|
||||||
end
|
|
||||||
|
|
||||||
class PetNotFound < RuntimeError;end
|
|
||||||
class DownloadError < RuntimeError;end
|
|
||||||
class UnexpectedDataFormat < RuntimeError;end
|
class UnexpectedDataFormat < RuntimeError;end
|
||||||
|
class ModelingDisabled < RuntimeError;end
|
||||||
private
|
|
||||||
|
|
||||||
# Send an AMFPHP request, re-raising errors as `Pet::DownloadError`.
|
|
||||||
# Return the response body as a `HashWithIndifferentAccess`.
|
|
||||||
def self.send_amfphp_request(request, timeout: 10)
|
|
||||||
begin
|
|
||||||
response_data = request.post(timeout: timeout, headers: {
|
|
||||||
"User-Agent" => Rails.configuration.user_agent_for_neopets,
|
|
||||||
})
|
|
||||||
rescue RocketAMFExtensions::RemoteGateway::AMFError => e
|
|
||||||
raise DownloadError, e.message
|
|
||||||
rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e
|
|
||||||
raise DownloadError, e.message, e.backtrace
|
|
||||||
end
|
|
||||||
|
|
||||||
HashWithIndifferentAccess.new(response_data)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
104
app/models/pet/modeling_snapshot.rb
Normal file
104
app/models/pet/modeling_snapshot.rb
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# A representation of a Neopets::CustomPets viewer data response, translated
|
||||||
|
# to DTI's database models!
|
||||||
|
class Pet::ModelingSnapshot
|
||||||
|
def initialize(viewer_data_hash)
|
||||||
|
@custom_pet = viewer_data_hash[:custom_pet]
|
||||||
|
@object_info_registry = viewer_data_hash[:object_info_registry]
|
||||||
|
@object_asset_registry = viewer_data_hash[:object_asset_registry]
|
||||||
|
end
|
||||||
|
|
||||||
|
def pet_type
|
||||||
|
@pet_type ||= begin
|
||||||
|
raise Pet::UnexpectedDataFormat unless @custom_pet[:species_id]
|
||||||
|
raise Pet::UnexpectedDataFormat unless @custom_pet[:color_id]
|
||||||
|
raise Pet::UnexpectedDataFormat unless @custom_pet[:body_id]
|
||||||
|
|
||||||
|
@custom_pet => {species_id:, color_id:}
|
||||||
|
PetType.find_or_initialize_by(species_id:, color_id:).tap do |pet_type|
|
||||||
|
# Apply the pet's body ID to the pet type, unless it's wearing an alt
|
||||||
|
# style, in which case ignore it, because it's the *alt style*'s body ID.
|
||||||
|
# (This can theoretically cause a problem saving a new pet type when
|
||||||
|
# there's an alt style too!)
|
||||||
|
pet_type.body_id = @custom_pet[:body_id] unless @custom_pet[:alt_style]
|
||||||
|
if pet_type.body_id.nil?
|
||||||
|
raise Pet::UnexpectedDataFormat,
|
||||||
|
"can't process alt style on first occurrence of pet type"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Try using this pet for the pet type's thumbnail, but don't worry
|
||||||
|
# if it fails.
|
||||||
|
begin
|
||||||
|
pet_type.consider_pet_image(@custom_pet[:name])
|
||||||
|
rescue => error
|
||||||
|
Rails.logger.warn "Failed to load pet image: #{error.full_message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pet_state
|
||||||
|
@pet_state ||= begin
|
||||||
|
swf_asset_ids = biology_assets.map(&:remote_id)
|
||||||
|
pet_type.pet_states.find_or_initialize_by(swf_asset_ids:).tap do |pet_state|
|
||||||
|
pet_state.swf_assets = biology_assets
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def alt_style
|
||||||
|
@alt_style ||= begin
|
||||||
|
return nil unless @custom_pet[:alt_style]
|
||||||
|
raise Pet::UnexpectedDataFormat unless @custom_pet[:alt_color]
|
||||||
|
|
||||||
|
id = @custom_pet[:alt_style].to_i
|
||||||
|
AltStyle.find_or_initialize_by(id:).tap do |alt_style|
|
||||||
|
alt_style.assign_attributes(
|
||||||
|
color_id: @custom_pet[:alt_color].to_i,
|
||||||
|
species_id: @custom_pet[:species_id].to_i,
|
||||||
|
body_id: @custom_pet[:body_id].to_i,
|
||||||
|
swf_assets: alt_style_assets,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def items
|
||||||
|
@items ||= Item.collection_from_pet_type_and_registries(
|
||||||
|
pet_type, @object_info_registry, @object_asset_registry
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def biology_assets
|
||||||
|
@biology_assets ||= begin
|
||||||
|
biology = @custom_pet[:alt_style].present? ?
|
||||||
|
@custom_pet[:original_biology] :
|
||||||
|
@custom_pet[:biology_by_zone]
|
||||||
|
assets_from_biology(biology)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def item_assets_for(item_id)
|
||||||
|
all_infos = @object_asset_registry.values
|
||||||
|
infos = all_infos.select { |a| a[:obj_info_id].to_i == item_id.to_i }
|
||||||
|
infos.map do |asset_data|
|
||||||
|
remote_id = asset_data[:asset_id].to_i
|
||||||
|
SwfAsset.find_or_initialize_by(type: "object", remote_id:).tap do |swf_asset|
|
||||||
|
swf_asset.origin_pet_type = pet_type
|
||||||
|
swf_asset.origin_object_data = asset_data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def alt_style_assets
|
||||||
|
raise Pet::UnexpectedDataFormat if @custom_pet[:biology_by_zone].empty?
|
||||||
|
assets_from_biology(@custom_pet[:biology_by_zone])
|
||||||
|
end
|
||||||
|
|
||||||
|
def assets_from_biology(biology)
|
||||||
|
raise Pet::UnexpectedDataFormat if biology.empty?
|
||||||
|
body_id = @custom_pet[:body_id].to_i
|
||||||
|
biology.values.map { |b| SwfAsset.from_biology_data(body_id, b) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,21 +1,22 @@
|
||||||
class PetState < ApplicationRecord
|
class PetState < ApplicationRecord
|
||||||
SwfAssetType = 'biology'
|
SwfAssetType = 'biology'
|
||||||
|
|
||||||
|
MAIN_POSES = %w(HAPPY_FEM HAPPY_MASC SAD_FEM SAD_MASC SICK_FEM SICK_MASC)
|
||||||
|
|
||||||
has_many :contributions, :as => :contributed,
|
has_many :contributions, :as => :contributed,
|
||||||
:inverse_of => :contributed # in case of duplicates being merged
|
:inverse_of => :contributed # in case of duplicates being merged
|
||||||
has_many :outfits
|
has_many :outfits
|
||||||
has_many :parent_swf_asset_relationships, :as => :parent,
|
has_many :parent_swf_asset_relationships, :as => :parent
|
||||||
:autosave => false
|
|
||||||
has_many :swf_assets, :through => :parent_swf_asset_relationships
|
has_many :swf_assets, :through => :parent_swf_asset_relationships
|
||||||
|
|
||||||
|
serialize :swf_asset_ids, coder: Serializers::IntegerSet, type: Array
|
||||||
|
|
||||||
belongs_to :pet_type
|
belongs_to :pet_type
|
||||||
|
|
||||||
delegate :color, to: :pet_type
|
delegate :species_id, :species, :color_id, :color, to: :pet_type
|
||||||
|
|
||||||
alias_method :swf_asset_ids_from_association, :swf_asset_ids
|
alias_method :swf_asset_ids_from_association, :swf_asset_ids
|
||||||
|
|
||||||
attr_writer :parent_swf_asset_relationships_to_update
|
|
||||||
|
|
||||||
# A simple ordering that tries to bring reliable pet states to the front.
|
# A simple ordering that tries to bring reliable pet states to the front.
|
||||||
scope :emotion_order, -> {
|
scope :emotion_order, -> {
|
||||||
order(Arel.sql(
|
order(Arel.sql(
|
||||||
|
@ -71,50 +72,25 @@ class PetState < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reassign_children_to!(main_pet_state)
|
# TODO: More and more, wanting to refactor poses…
|
||||||
self.contributions.each do |contribution|
|
def pose=(pose)
|
||||||
contribution.contributed = main_pet_state
|
case pose
|
||||||
contribution.save
|
when "UNKNOWN"
|
||||||
end
|
label_pose nil, nil, unconverted: nil, labeled: false
|
||||||
self.outfits.each do |outfit|
|
when "HAPPY_MASC"
|
||||||
outfit.pet_state = main_pet_state
|
label_pose 1, false
|
||||||
outfit.save
|
when "HAPPY_FEM"
|
||||||
end
|
label_pose 1, true
|
||||||
ParentSwfAssetRelationship.where(ParentSwfAssetRelationship.arel_table[:parent_id].eq(self.id)).delete_all
|
when "SAD_MASC"
|
||||||
end
|
label_pose 2, false
|
||||||
|
when "SAD_FEM"
|
||||||
def reassign_duplicates!
|
label_pose 2, true
|
||||||
raise "This may only be applied to pet states that represent many duplicate entries" unless duplicate_ids
|
when "SICK_MASC"
|
||||||
pet_states = duplicate_ids.split(',').map do |id|
|
label_pose 4, false
|
||||||
PetState.find(id.to_i)
|
when "SICK_FEM"
|
||||||
end
|
label_pose 4, true
|
||||||
main_pet_state = pet_states.shift
|
when "UNCONVERTED"
|
||||||
pet_states.each do |pet_state|
|
label_pose nil, nil, unconverted: true
|
||||||
pet_state.reassign_children_to!(main_pet_state)
|
|
||||||
pet_state.destroy
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def sort_swf_asset_ids!
|
|
||||||
self.swf_asset_ids = swf_asset_ids_array.sort.join(',')
|
|
||||||
end
|
|
||||||
|
|
||||||
def swf_asset_ids
|
|
||||||
self['swf_asset_ids']
|
|
||||||
end
|
|
||||||
|
|
||||||
def swf_asset_ids_array
|
|
||||||
swf_asset_ids.split(',').map(&:to_i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def swf_asset_ids=(ids)
|
|
||||||
self['swf_asset_ids'] = ids
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_assets!
|
|
||||||
@parent_swf_asset_relationships_to_update.each do |rel|
|
|
||||||
rel.swf_asset.save!
|
|
||||||
rel.save!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -122,58 +98,40 @@ class PetState < ApplicationRecord
|
||||||
"#{id}-#{pose.split('_').map(&:capitalize).join('-')}"
|
"#{id}-#{pose.split('_').map(&:capitalize).join('-')}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.from_pet_type_and_biology_info(pet_type, info)
|
# Because our column is named `swf_asset_ids`, we need to ensure writes to
|
||||||
swf_asset_ids = []
|
# it go to the attribute, and not the thing ActiveRecord does of finding the
|
||||||
info.each do |zone_id, asset_info|
|
# relevant `swf_assets`.
|
||||||
if zone_id.present? && asset_info
|
# TODO: Consider renaming the column to `cached_swf_asset_ids`?
|
||||||
swf_asset_ids << asset_info[:part_id].to_i
|
def swf_asset_ids=(new_swf_asset_ids)
|
||||||
end
|
write_attribute(:swf_asset_ids, new_swf_asset_ids)
|
||||||
end
|
end
|
||||||
swf_asset_ids_str = swf_asset_ids.sort.join(',')
|
|
||||||
if pet_type.new_record?
|
private
|
||||||
pet_state = self.new :swf_asset_ids => swf_asset_ids_str
|
|
||||||
else
|
# A helper for the `pose=` method.
|
||||||
pet_state = self.find_or_initialize_by(
|
def label_pose(mood_id, female, unconverted: false, labeled: true)
|
||||||
pet_type_id: pet_type.id,
|
self.labeled = labeled
|
||||||
swf_asset_ids: swf_asset_ids_str
|
self.mood_id = mood_id
|
||||||
)
|
self.female = female
|
||||||
end
|
self.unconverted = unconverted
|
||||||
existing_swf_assets = SwfAsset.biology_assets.includes(:zone).
|
end
|
||||||
where(remote_id: swf_asset_ids)
|
|
||||||
existing_swf_assets_by_id = {}
|
def self.last_updated_key
|
||||||
existing_swf_assets.each do |swf_asset|
|
PetState.maximum(:updated_at)
|
||||||
existing_swf_assets_by_id[swf_asset.remote_id] = swf_asset
|
end
|
||||||
end
|
|
||||||
existing_relationships_by_swf_asset_id = {}
|
def self.all_supported_poses
|
||||||
unless pet_state.new_record?
|
Rails.cache.fetch("PetState.all_supported_poses #{last_updated_key}") do
|
||||||
pet_state.parent_swf_asset_relationships.each do |relationship|
|
{}.tap do |h|
|
||||||
existing_relationships_by_swf_asset_id[relationship.swf_asset_id] = relationship
|
includes(:pet_type).find_each do |pet_state|
|
||||||
end
|
h[pet_state.species_id] ||= {}
|
||||||
end
|
h[pet_state.species_id][pet_state.color_id] ||= []
|
||||||
pet_state.pet_type = pet_type # save the second case from having to look it up by ID
|
h[pet_state.species_id][pet_state.color_id] << pet_state.pose
|
||||||
relationships = []
|
|
||||||
info.each do |zone_id, asset_info|
|
|
||||||
if zone_id.present? && asset_info
|
|
||||||
swf_asset_id = asset_info[:part_id].to_i
|
|
||||||
swf_asset = existing_swf_assets_by_id[swf_asset_id]
|
|
||||||
unless swf_asset
|
|
||||||
swf_asset = SwfAsset.new
|
|
||||||
swf_asset.remote_id = swf_asset_id
|
|
||||||
end
|
end
|
||||||
swf_asset.origin_biology_data = asset_info
|
|
||||||
swf_asset.origin_pet_type = pet_type
|
h.values.map(&:values).flatten(1).each(&:uniq!).each(&:sort!)
|
||||||
relationship = existing_relationships_by_swf_asset_id[swf_asset.id]
|
|
||||||
unless relationship
|
|
||||||
relationship ||= ParentSwfAssetRelationship.new
|
|
||||||
relationship.parent = pet_state
|
|
||||||
relationship.swf_asset_id = swf_asset.id
|
|
||||||
end
|
|
||||||
relationship.swf_asset = swf_asset
|
|
||||||
relationships << relationship
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
pet_state.parent_swf_asset_relationships_to_update = relationships
|
|
||||||
pet_state
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,6 @@ class PetType < ApplicationRecord
|
||||||
species = Species.find_by_name!(species_name)
|
species = Species.find_by_name!(species_name)
|
||||||
where(color_id: color.id, species_id: species.id)
|
where(color_id: color.id, species_id: species.id)
|
||||||
}
|
}
|
||||||
scope :matching_name_param, ->(name_param) {
|
|
||||||
color_name, _, species_name = name_param.rpartition("-")
|
|
||||||
matching_name(color_name, species_name)
|
|
||||||
}
|
|
||||||
scope :preferring_species, ->(species_id) {
|
scope :preferring_species, ->(species_id) {
|
||||||
joins(:species).order([Arel.sql("species_id = ? DESC"), species_id])
|
joins(:species).order([Arel.sql("species_id = ? DESC"), species_id])
|
||||||
}
|
}
|
||||||
|
@ -30,6 +26,10 @@ class PetType < ApplicationRecord
|
||||||
merge(Species.order(name: :asc)).
|
merge(Species.order(name: :asc)).
|
||||||
merge(Color.order(basic: :desc, standard: :desc, name: :asc))
|
merge(Color.order(basic: :desc, standard: :desc, name: :asc))
|
||||||
}
|
}
|
||||||
|
scope :released_before, ->(time) {
|
||||||
|
# We use DTI's creation timestamp as an estimate of when it was released.
|
||||||
|
where('created_at <= ?', time)
|
||||||
|
}
|
||||||
|
|
||||||
def self.random_basic_per_species(species_ids)
|
def self.random_basic_per_species(species_ids)
|
||||||
random_pet_types = []
|
random_pet_types = []
|
||||||
|
@ -57,6 +57,14 @@ class PetType < ApplicationRecord
|
||||||
basic_image_hash || self['image_hash'] || 'deadbeef'
|
basic_image_hash || self['image_hash'] || 'deadbeef'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def consider_pet_image(pet_name)
|
||||||
|
# If we already have a basic image hash, don't worry about it!
|
||||||
|
return if basic_image_hash?
|
||||||
|
|
||||||
|
# Otherwise, use this as the new image hash for this pet type.
|
||||||
|
self.image_hash = Neopets::CustomPets.fetch_image_hash(pet_name)
|
||||||
|
end
|
||||||
|
|
||||||
def possibly_new_color
|
def possibly_new_color
|
||||||
self.color || Color.new(id: self.color_id)
|
self.color || Color.new(id: self.color_id)
|
||||||
end
|
end
|
||||||
|
@ -71,11 +79,6 @@ class PetType < ApplicationRecord
|
||||||
species_human_name: possibly_new_species.human_name)
|
species_human_name: possibly_new_species.human_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_pet_state_from_biology!(biology)
|
|
||||||
pet_state = PetState.from_pet_type_and_biology_info(self, biology)
|
|
||||||
pet_state
|
|
||||||
end
|
|
||||||
|
|
||||||
def canonical_pet_state
|
def canonical_pet_state
|
||||||
# For consistency (randomness is always scary!), we use the PetType ID to
|
# For consistency (randomness is always scary!), we use the PetType ID to
|
||||||
# determine which gender to prefer, if it's not built into the color. That
|
# determine which gender to prefer, if it's not built into the color. That
|
||||||
|
@ -113,7 +116,37 @@ class PetType < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_param
|
def to_param
|
||||||
"#{color.human_name}-#{species.human_name}"
|
"#{possibly_new_color.to_param}-#{possibly_new_species.to_param}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def fully_labeled?
|
||||||
|
num_missing_poses == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def num_poses
|
||||||
|
all_poses = pet_states.map(&:pose)
|
||||||
|
PetState::MAIN_POSES.count { |pose| all_poses.include? pose }
|
||||||
|
end
|
||||||
|
|
||||||
|
def num_missing_poses
|
||||||
|
PetState::MAIN_POSES.count - num_poses
|
||||||
|
end
|
||||||
|
|
||||||
|
def num_unlabeled_states
|
||||||
|
pet_states.count { |ps| ps.pose == "UNKNOWN" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.find_by_param!(param)
|
||||||
|
raise ActiveRecord::RecordNotFound unless param.include?("-")
|
||||||
|
color_param, _, species_param = param.rpartition("-")
|
||||||
|
where(
|
||||||
|
color_id: Color.param_to_id(color_param),
|
||||||
|
species_id: Species.param_to_id(species_param),
|
||||||
|
).first!
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.basic_body_ids
|
||||||
|
PetType.basic.distinct.pluck(:body_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.all_by_ids_or_children(ids, pet_states)
|
def self.all_by_ids_or_children(ids, pet_states)
|
||||||
|
@ -135,7 +168,5 @@ class PetType < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class DownloadError < Exception;end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,10 @@ class Species < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
name? ? human_name : id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
# Given a list of body IDs, return a hash from body ID to Species.
|
# Given a list of body IDs, return a hash from body ID to Species.
|
||||||
# (We assume that each body ID belongs to just one species; if not, which
|
# (We assume that each body ID belongs to just one species; if not, which
|
||||||
# species we return for that body ID is undefined.)
|
# species we return for that body ID is undefined.)
|
||||||
|
@ -26,4 +30,8 @@ class Species < ApplicationRecord
|
||||||
to_h { |s| [s.id, s] }
|
to_h { |s| [s.id, s] }
|
||||||
species_ids_by_body_id.transform_values { |id| species_by_id[id] }
|
species_ids_by_body_id.transform_values { |id| species_by_id[id] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.param_to_id(param)
|
||||||
|
param.match?(/\A\d+\Z/) ? param.to_i : find_by_name!(param).id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,8 +2,6 @@ require 'addressable/template'
|
||||||
require 'async'
|
require 'async'
|
||||||
require 'async/barrier'
|
require 'async/barrier'
|
||||||
require 'async/semaphore'
|
require 'async/semaphore'
|
||||||
require 'fileutils'
|
|
||||||
require 'uri'
|
|
||||||
|
|
||||||
class SwfAsset < ApplicationRecord
|
class SwfAsset < ApplicationRecord
|
||||||
# We use the `type` column to mean something other than what Rails means!
|
# We use the `type` column to mean something other than what Rails means!
|
||||||
|
@ -322,14 +320,6 @@ class SwfAsset < ApplicationRecord
|
||||||
swf_asset
|
swf_asset
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.from_wardrobe_link_params(ids)
|
|
||||||
where((
|
|
||||||
arel_table[:remote_id].in(ids[:biology]).and(arel_table[:type].eq('biology'))
|
|
||||||
).or(
|
|
||||||
arel_table[:remote_id].in(ids[:object]).and(arel_table[:type].eq('object'))
|
|
||||||
))
|
|
||||||
end
|
|
||||||
|
|
||||||
# Given a list of SWF assets, ensure all of their manifests are loaded, with
|
# Given a list of SWF assets, ensure all of their manifests are loaded, with
|
||||||
# fast concurrent execution!
|
# fast concurrent execution!
|
||||||
def self.preload_manifests(swf_assets)
|
def self.preload_manifests(swf_assets)
|
||||||
|
@ -373,6 +363,4 @@ class SwfAsset < ApplicationRecord
|
||||||
# linked to it, meaning that it's probably wearable by all bodies.
|
# linked to it, meaning that it's probably wearable by all bodies.
|
||||||
self.body_id = 0 if !@body_id_overridden && (!self.body_specific? || (!self.new_record? && self.body_id_changed?))
|
self.body_id = 0 if !@body_id_overridden && (!self.body_specific? || (!self.new_record? && self.body_id_changed?))
|
||||||
end
|
end
|
||||||
|
|
||||||
class DownloadError < Exception;end
|
|
||||||
end
|
end
|
||||||
|
|
67
app/services/neopets/custom_pets.rb
Normal file
67
app/services/neopets/custom_pets.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
require 'rocketamf_extensions/remote_gateway'
|
||||||
|
|
||||||
|
module Neopets::CustomPets
|
||||||
|
GATEWAY_URL =
|
||||||
|
Addressable::URI.parse(Rails.configuration.neopets_origin) +
|
||||||
|
'/amfphp/gateway.php'
|
||||||
|
GATEWAY = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL)
|
||||||
|
CUSTOM_PET_SERVICE = GATEWAY.service('CustomPetService')
|
||||||
|
PET_SERVICE = GATEWAY.service('PetService')
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# NOTE: Ideally pet requests shouldn't take this long, but Neopets can be
|
||||||
|
# slow sometimes! Since we're on the Falcon server, long timeouts shouldn't
|
||||||
|
# slow down the rest of the request queue, like it used to be in the past.
|
||||||
|
def fetch_viewer_data(name, timeout: 10)
|
||||||
|
request = CUSTOM_PET_SERVICE.action('getViewerData').request([name])
|
||||||
|
send_amfphp_request(request).tap do |data|
|
||||||
|
if data[:custom_pet][:name].blank?
|
||||||
|
raise PetNotFound, "Pet #{name.inspect} does not exist"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_metadata(name, timeout: 10)
|
||||||
|
# If this is an image hash "pet name", it has no metadata.
|
||||||
|
return nil if name.start_with?("@")
|
||||||
|
|
||||||
|
request = PET_SERVICE.action('getPet').request([name])
|
||||||
|
send_amfphp_request(request).tap do |data|
|
||||||
|
if data[:name].blank?
|
||||||
|
raise PetNotFound, "Pet #{name.inspect} does not exist"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Given a pet's name, load its image hash, for use in `pets.neopets.com`
|
||||||
|
# image URLs. (This corresponds to its current biology and items.)
|
||||||
|
def fetch_image_hash(name, timeout: 10)
|
||||||
|
# If this is an image hash "pet name", just take off the `@`!
|
||||||
|
return name[1..] if name.start_with?("@")
|
||||||
|
|
||||||
|
metadata = fetch_metadata(name, timeout:)
|
||||||
|
metadata[:hash]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Send an AMFPHP request, re-raising errors as `DownloadError`.
|
||||||
|
# Return the response body as a `HashWithIndifferentAccess`.
|
||||||
|
def send_amfphp_request(request, timeout: 10)
|
||||||
|
begin
|
||||||
|
response_data = request.post(timeout: timeout, headers: {
|
||||||
|
"User-Agent" => Rails.configuration.user_agent_for_neopets,
|
||||||
|
})
|
||||||
|
rescue RocketAMFExtensions::RemoteGateway::AMFError => e
|
||||||
|
raise DownloadError, e.message
|
||||||
|
rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e
|
||||||
|
raise DownloadError, e.message, e.backtrace
|
||||||
|
end
|
||||||
|
|
||||||
|
HashWithIndifferentAccess.new(response_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PetNotFound < RuntimeError;end
|
||||||
|
class DownloadError < RuntimeError;end
|
||||||
|
end
|
|
@ -1,7 +1,7 @@
|
||||||
require "addressable/template"
|
require "addressable/template"
|
||||||
require "async/http/internet/instance"
|
require "async/http/internet/instance"
|
||||||
|
|
||||||
module NCMall
|
module Neopets::NCMall
|
||||||
# Share a pool of persistent connections, rather than reconnecting on
|
# Share a pool of persistent connections, rather than reconnecting on
|
||||||
# each request. (This library does that automatically!)
|
# each request. (This library does that automatically!)
|
||||||
INTERNET = Async::HTTP::Internet.instance
|
INTERNET = Async::HTTP::Internet.instance
|
||||||
|
@ -45,6 +45,37 @@ module NCMall
|
||||||
uniq
|
uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
|
STYLING_STUDIO_URL = "https://www.neopets.com/np-templates/ajax/stylingstudio/studio.php"
|
||||||
|
def self.load_styles(species_id:, neologin:)
|
||||||
|
Sync do
|
||||||
|
INTERNET.post(
|
||||||
|
STYLING_STUDIO_URL,
|
||||||
|
headers: [
|
||||||
|
["User-Agent", Rails.configuration.user_agent_for_neopets],
|
||||||
|
["Content-Type", "application/x-www-form-urlencoded"],
|
||||||
|
["Cookie", "neologin=#{neologin}"],
|
||||||
|
["X-Requested-With", "XMLHttpRequest"],
|
||||||
|
],
|
||||||
|
body: {tab: 1, mode: "getStyles", species: species_id}.to_query,
|
||||||
|
) do |response|
|
||||||
|
if response.status != 200
|
||||||
|
raise ResponseNotOK.new(response.status),
|
||||||
|
"expected status 200 but got #{response.status} (#{STYLING_STUDIO_URL})"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
data = JSON.parse(response.read).deep_symbolize_keys
|
||||||
|
|
||||||
|
# HACK: styles is a hash, unless it's empty, in which case it's an
|
||||||
|
# array? Weird. Normalize this by converting to hash.
|
||||||
|
data.fetch(:styles).to_h.values
|
||||||
|
rescue JSON::ParserError, KeyError
|
||||||
|
raise UnexpectedResponseFormat
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def self.load_page_by_url(url)
|
def self.load_page_by_url(url)
|
|
@ -2,7 +2,7 @@ require "async/http/internet/instance"
|
||||||
|
|
||||||
# While most of our NeoPass logic is built into Devise -> OmniAuth -> OIDC
|
# While most of our NeoPass logic is built into Devise -> OmniAuth -> OIDC
|
||||||
# OmniAuth plugin, NeoPass also offers some supplemental APIs that we use here.
|
# OmniAuth plugin, NeoPass also offers some supplemental APIs that we use here.
|
||||||
module NeoPass
|
module Neopets::NeoPass
|
||||||
# Share a pool of persistent connections, rather than reconnecting on
|
# Share a pool of persistent connections, rather than reconnecting on
|
||||||
# each request. (This library does that automatically!)
|
# each request. (This library does that automatically!)
|
||||||
INTERNET = Async::HTTP::Internet.instance
|
INTERNET = Async::HTTP::Internet.instance
|
|
@ -1,4 +1,12 @@
|
||||||
%li.alt-style
|
%li
|
||||||
= link_to alt_style.preview_image_url do
|
= link_to view_or_edit_alt_style_url(alt_style) do
|
||||||
= image_tag alt_style.thumbnail_url, class: 'alt-style-thumbnail'
|
= image_tag alt_style.preview_image_url, class: "preview", loading: "lazy"
|
||||||
.alt-style-name= alt_style.name
|
.name
|
||||||
|
%span= alt_style.series_name
|
||||||
|
%span= alt_style.pet_name
|
||||||
|
.info
|
||||||
|
%p
|
||||||
|
Added
|
||||||
|
= time_tag alt_style.created_at,
|
||||||
|
title: alt_style.created_at.to_formatted_s(:long_nst) do
|
||||||
|
= time_with_only_month_if_old alt_style.created_at
|
40
app/views/alt_styles/edit.html.haml
Normal file
40
app/views/alt_styles/edit.html.haml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
- title @alt_style.full_name
|
||||||
|
- use_responsive_design
|
||||||
|
|
||||||
|
%ol.breadcrumbs
|
||||||
|
%li= link_to "Alt Styles", alt_styles_path
|
||||||
|
%li
|
||||||
|
= link_to @alt_style.color.human_name,
|
||||||
|
alt_styles_path(color: @alt_style.color.human_name)
|
||||||
|
%li{"data-relation-to-prev": "sibling"}
|
||||||
|
= link_to @alt_style.species.human_name,
|
||||||
|
alt_styles_path(species: @alt_style.species.human_name)
|
||||||
|
%li= @alt_style.series_name
|
||||||
|
|
||||||
|
= image_tag @alt_style.preview_image_url, class: "alt-style-preview"
|
||||||
|
|
||||||
|
= form_with model: @alt_style, class: "support-form" do |f|
|
||||||
|
- if @alt_style.errors.any?
|
||||||
|
%p
|
||||||
|
Could not save:
|
||||||
|
%ul.errors
|
||||||
|
- @alt_style.errors.each do |error|
|
||||||
|
%li= error.full_message
|
||||||
|
%fieldset
|
||||||
|
= f.label :real_series_name, "Series"
|
||||||
|
= f.text_field :real_series_name, autofocus: !@alt_style.real_series_name?
|
||||||
|
= f.label :thumbnail_url, "Thumbnail"
|
||||||
|
.thumbnail-field
|
||||||
|
- if @alt_style.thumbnail_url?
|
||||||
|
= image_tag @alt_style.thumbnail_url
|
||||||
|
= f.url_field :thumbnail_url
|
||||||
|
.actions
|
||||||
|
= f.submit "Save changes"
|
||||||
|
%label{title: "If checked, takes you to the next unlabeled pet style, if any. Useful for labeling in bulk!"}
|
||||||
|
= check_box_tag "next", "unlabeled-style",
|
||||||
|
checked: params[:next] == "unlabeled-style"
|
||||||
|
Then: Go to unlabeled style
|
||||||
|
|
||||||
|
- content_for :stylesheets do
|
||||||
|
= stylesheet_link_tag "application/breadcrumbs", "application/support-form"
|
||||||
|
= page_stylesheet_link_tag "alt_styles/edit"
|
|
@ -1,18 +1,46 @@
|
||||||
- title "Styling Studio"
|
- title "NC Pet Styles"
|
||||||
|
- use_responsive_design
|
||||||
|
|
||||||
%p
|
%ul.breadcrumbs
|
||||||
Here's all the new NC Pet Styles we have! They're available in the app too,
|
%li= link_to "Rainbow Pool", pet_types_path
|
||||||
by opening the emotion picker and clicking the "Styles" tab.
|
%li Pet Styles
|
||||||
|
|
||||||
%p
|
:markdown
|
||||||
If you have an Alt Style we don't, please model it by entering your pet's
|
Pet Styles drastically change the appearance of your pet! They're [available
|
||||||
|
in the NC Mall][1], or via "NC Trading". Some of them are "Nostalgic",
|
||||||
|
meaning they're reminiscent of classic Neopets designs from long ago—and some
|
||||||
|
are brand new!
|
||||||
|
|
||||||
|
Pet Styles only fit pets of the same species—but the *color* of the pet
|
||||||
|
doesn't matter! A Blue Acara can wear the "Nostalgic Faerie Acara" Pet Style.
|
||||||
|
|
||||||
|
Only some items fit pets wearing Pet Styles: mostly Backgrounds, Foregrounds,
|
||||||
|
and other items that aren't designed to fit a specific body shape.
|
||||||
|
|
||||||
|
If you have a Pet Style we don't, please model it by entering your pet's
|
||||||
name on the homepage! Thank you! 💖
|
name on the homepage! Thank you! 💖
|
||||||
|
|
||||||
%p
|
[1]: https://www.neopets.com/mall/stylingstudio/
|
||||||
Also, heads-up: Because our system can only collect "item data" for normal
|
|
||||||
wearable items, there's not a great way for us to get style tokens onto
|
|
||||||
tradelists… this may change someday, but probably not soon, sorry!
|
|
||||||
|
|
||||||
- @alt_styles.group_by(&:species).each do |species, species_styles|
|
= form_with url: alt_styles_path, method: :get,
|
||||||
%h2.alt-styles-header= species.human_name
|
class: "rainbow-pool-filters" do |f|
|
||||||
%ul.alt-styles-list= render partial: "alt_style", collection: species_styles
|
%fieldset
|
||||||
|
%legend Filter by:
|
||||||
|
= f.select :series, @all_series_names,
|
||||||
|
selected: @series_name, include_blank: "Style…"
|
||||||
|
= f.select :color, @all_color_names,
|
||||||
|
selected: @color&.human_name, include_blank: "Color…"
|
||||||
|
= f.select :species, @all_species_names,
|
||||||
|
selected: @species&.human_name, include_blank: "Species…"
|
||||||
|
= f.submit "Go", name: nil
|
||||||
|
|
||||||
|
= will_paginate @alt_styles, class: "rainbow-pool-pagination"
|
||||||
|
|
||||||
|
%ul.rainbow-pool-list= render @alt_styles
|
||||||
|
|
||||||
|
= will_paginate @alt_styles, class: "rainbow-pool-pagination"
|
||||||
|
|
||||||
|
- content_for :stylesheets do
|
||||||
|
= stylesheet_link_tag "application/breadcrumbs"
|
||||||
|
= stylesheet_link_tag "application/rainbow-pool"
|
||||||
|
= page_stylesheet_link_tag "alt_styles/index"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
%outfit-viewer
|
- html_options = {} unless defined? html_options
|
||||||
|
= content_tag "outfit-viewer", **html_options do
|
||||||
.loading-indicator= render partial: "hanger_spinner"
|
.loading-indicator= render partial: "hanger_spinner"
|
||||||
|
|
||||||
%label.play-pause-button{title: "Pause/play animations"}
|
%label.play-pause-button{title: "Pause/play animations"}
|
||||||
|
|
|
@ -46,6 +46,8 @@
|
||||||
= link_to t('items.show.closet_hangers.button'),
|
= link_to t('items.show.closet_hangers.button'),
|
||||||
user_closet_hangers_path(current_user),
|
user_closet_hangers_path(current_user),
|
||||||
class: 'user-lists-form-opener'
|
class: 'user-lists-form-opener'
|
||||||
|
- if support_staff?
|
||||||
|
= link_to "Edit", edit_item_path(item)
|
||||||
|
|
||||||
- if user_signed_in?
|
- if user_signed_in?
|
||||||
= form_tag update_quantities_user_item_closet_hangers_path(user_id: current_user, item_id: item), method: :put, class: 'user-lists-form', hidden: item_header_user_lists_form_state != "open" do
|
= form_tag update_quantities_user_item_closet_hangers_path(user_id: current_user, item_id: item), method: :put, class: 'user-lists-form', hidden: item_header_user_lists_form_state != "open" do
|
||||||
|
|
59
app/views/items/edit.html.haml
Normal file
59
app/views/items/edit.html.haml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
- title "Editing \"#{@item.name}\""
|
||||||
|
- use_responsive_design
|
||||||
|
|
||||||
|
%h1#title Editing "#{@item.name}"
|
||||||
|
|
||||||
|
:markdown
|
||||||
|
Heads up: the modeling process controls some of these fields by default! If
|
||||||
|
you change something, but it doesn't match what we're seeing on Neopets.com,
|
||||||
|
it will probably be reverted automatically when someone models it.
|
||||||
|
|
||||||
|
= form_with model: @item, class: "support-form" do |f|
|
||||||
|
- if @item.errors.any?
|
||||||
|
%p
|
||||||
|
Could not save:
|
||||||
|
%ul.errors
|
||||||
|
- @item.errors.each do |error|
|
||||||
|
%li= error.full_message
|
||||||
|
%fieldset
|
||||||
|
= f.label :name
|
||||||
|
= f.text_field :name
|
||||||
|
= f.label :thumbnail_url, "Thumbnail"
|
||||||
|
.thumbnail-field
|
||||||
|
- if @item.thumbnail_url?
|
||||||
|
= image_tag @item.thumbnail_url
|
||||||
|
= f.url_field :thumbnail_url
|
||||||
|
= f.label :description
|
||||||
|
= f.text_field :description
|
||||||
|
.field-name Item kind
|
||||||
|
.radio-field
|
||||||
|
%label{title: "NC items generally have a rarity value of 500.\nPaintbrush items generally contain a special message in the description."}
|
||||||
|
= f.radio_button :is_manually_nc, false
|
||||||
|
Automatic: Based on rarity and description
|
||||||
|
%label{title: "Use this when Neopets releases an NC item, but labels the rarity as something other than 500, usually by mistake."}
|
||||||
|
= f.radio_button :is_manually_nc, true
|
||||||
|
Manually NC: From the NC Mall, but not r500
|
||||||
|
.field-name Modeling status
|
||||||
|
.radio-field
|
||||||
|
%label{title: "If we fit two or more species of a standard color, assume we also fit the other standard-color pets that were released at the time.\nRepeat for special colors like Baby and Maraquan."}
|
||||||
|
= f.radio_button :modeling_status_hint, ""
|
||||||
|
Automatic: Fits 2+ species → Should fit all
|
||||||
|
%label{title: "Use this when e.g. there simply is no Acara version of the item."}
|
||||||
|
= f.radio_button :modeling_status_hint, "done"
|
||||||
|
Done: Neopets.com is missing some models
|
||||||
|
%label{title: "Use this when e.g. this fits the Blue Vandagyre even though it's a Maraquan item.\nBehaves identically to Done, but helps us remember why we did this!"}
|
||||||
|
= f.radio_button :modeling_status_hint, "glitchy"
|
||||||
|
Glitchy: Neopets.com has <em>too many</em> models
|
||||||
|
.field-name Body fit
|
||||||
|
.radio-field
|
||||||
|
%label{title: "When an asset in a zone like Background is modeled, assume it fits all pets the same, and assign it body ID \#0.\nOtherwise, assume it fits only the kind of pet it was modeled on."}
|
||||||
|
= f.radio_button :explicitly_body_specific, false
|
||||||
|
Automatic: Some zones fit all species
|
||||||
|
%label{title: "Use this when an item uses a generally-universal zone like Static, but is body-specific regardless. \"Encased in Ice\" is one example.\nThis prevents these uncommon items from breaking every time they're modeled."}
|
||||||
|
= f.radio_button :explicitly_body_specific, true
|
||||||
|
Body-specific: Fits all species differently
|
||||||
|
.actions
|
||||||
|
= f.submit "Save changes"
|
||||||
|
|
||||||
|
- content_for :stylesheets do
|
||||||
|
= page_stylesheet_link_tag "application/support-form"
|
|
@ -34,7 +34,7 @@
|
||||||
%span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️
|
%span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️
|
||||||
|
|
||||||
= select_tag "preview[color_id]",
|
= select_tag "preview[color_id]",
|
||||||
options_from_collection_for_select(Color.funny.alphabetical,
|
options_from_collection_for_select(Color.alphabetical,
|
||||||
"id", "human_name", @selected_preview_pet_type.color_id)
|
"id", "human_name", @selected_preview_pet_type.color_id)
|
||||||
= select_tag "preview[species_id]",
|
= select_tag "preview[species_id]",
|
||||||
options_from_collection_for_select(Species.alphabetical,
|
options_from_collection_for_select(Species.alphabetical,
|
||||||
|
@ -70,9 +70,10 @@
|
||||||
%li<
|
%li<
|
||||||
= zone.label
|
= zone.label
|
||||||
- if item_zone_partial_fit? appearances_in_zone, @all_appearances
|
- if item_zone_partial_fit? appearances_in_zone, @all_appearances
|
||||||
|
= " "
|
||||||
%span.zone-species-info{
|
%span.zone-species-info{
|
||||||
title: item_zone_species_list(appearances_in_zone)
|
title: item_zone_species_list(appearances_in_zone)
|
||||||
}
|
}<
|
||||||
(#{appearances_in_zone.size} species)
|
(#{appearances_in_zone.size} species)
|
||||||
- else
|
- else
|
||||||
%span.no-zones (None)
|
%span.no-zones (None)
|
||||||
|
|
|
@ -29,6 +29,26 @@
|
||||||
= render 'analytics'
|
= render 'analytics'
|
||||||
%body{:class => body_class}
|
%body{:class => body_class}
|
||||||
#container
|
#container
|
||||||
|
%nav#main-nav
|
||||||
|
- if home_link?
|
||||||
|
%a#home-link{:href => root_path}
|
||||||
|
%span= t 'app_name'
|
||||||
|
|
||||||
|
#userbar
|
||||||
|
- if user_signed_in?
|
||||||
|
%span
|
||||||
|
= t '.userbar.greeting', :user_name => current_user.name
|
||||||
|
= userbar_contributions_summary(current_user)
|
||||||
|
= link_to t('.userbar.items'), user_closet_hangers_path(current_user), :id => 'userbar-items-link'
|
||||||
|
= link_to t('.userbar.outfits'), current_user_outfits_path
|
||||||
|
= link_to t('.userbar.settings'), edit_auth_user_path
|
||||||
|
= button_to t('.userbar.logout'), destroy_auth_user_session_path, method: :delete,
|
||||||
|
params: {return_to: request.fullpath}
|
||||||
|
- else
|
||||||
|
= link_to auth_user_sign_in_path_with_return_to,
|
||||||
|
id: 'userbar-log-in', "data-turbo-prefetch": false do
|
||||||
|
%span= t('.userbar.login')
|
||||||
|
|
||||||
= yield :before_title
|
= yield :before_title
|
||||||
= render 'announcement'
|
= render 'announcement'
|
||||||
- if content_for?(:title) && show_title_header?
|
- if content_for?(:title) && show_title_header?
|
||||||
|
@ -41,25 +61,6 @@
|
||||||
- else
|
- else
|
||||||
= yield
|
= yield
|
||||||
|
|
||||||
- if home_link?
|
|
||||||
%a#home-link{:href => root_path}
|
|
||||||
%span= t 'app_name'
|
|
||||||
|
|
||||||
#userbar
|
|
||||||
- if user_signed_in?
|
|
||||||
%span
|
|
||||||
= t '.userbar.greeting', :user_name => current_user.name
|
|
||||||
= userbar_contributions_summary(current_user)
|
|
||||||
= link_to t('.userbar.items'), user_closet_hangers_path(current_user), :id => 'userbar-items-link'
|
|
||||||
= link_to t('.userbar.outfits'), current_user_outfits_path
|
|
||||||
= link_to t('.userbar.settings'), edit_auth_user_path
|
|
||||||
= button_to t('.userbar.logout'), destroy_auth_user_session_path, method: :delete,
|
|
||||||
params: {return_to: request.fullpath}
|
|
||||||
- else
|
|
||||||
= link_to auth_user_sign_in_path_with_return_to,
|
|
||||||
id: 'userbar-log-in', "data-turbo-prefetch": false do
|
|
||||||
%span= t('.userbar.login')
|
|
||||||
|
|
||||||
#footer
|
#footer
|
||||||
= form_tag choose_locale_path, :id => 'locale-form' do
|
= form_tag choose_locale_path, :id => 'locale-form' do
|
||||||
= hidden_field_tag 'return_to', request.fullpath
|
= hidden_field_tag 'return_to', request.fullpath
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
= link_to t('.footer.terms', date: terms_updated_timestamp),
|
= link_to t('.footer.terms', date: terms_updated_timestamp),
|
||||||
terms_path
|
terms_path
|
||||||
%li= link_to t('.footer.blog'), "https://blog.openneo.net/"
|
%li= link_to t('.footer.blog'), "https://blog.openneo.net/"
|
||||||
|
%li= link_to t('modeling_hub'), bulk_pets_path
|
||||||
|
|
||||||
%div
|
%div
|
||||||
#{t('.footer.contact')}:
|
#{t('.footer.contact')}:
|
||||||
|
|
|
@ -6,20 +6,19 @@
|
||||||
|
|
||||||
- if show_announcement?
|
- if show_announcement?
|
||||||
%section.announcement
|
%section.announcement
|
||||||
= image_tag "about/announcement-broom.png", width: 70, height: 70,
|
= image_tag "/images/error-grundo.png", width: 70, height: 70,
|
||||||
srcset: {"about/announcement-broom@2x.png": "2x"},
|
srcset: {"/images/error-grundo.png": "2x"}
|
||||||
class: "neopass-thumbnail"
|
|
||||||
.content
|
.content
|
||||||
%p
|
%p
|
||||||
%strong
|
%strong
|
||||||
= link_to "State of DTI: 2024!",
|
Oops, sorry for the bugs recently!
|
||||||
"https://blog.openneo.net/2024/09/20/state-of-dti-2024.html"
|
For the first time in One Million Years, we made some changes to our
|
||||||
Here's what we've been up to this year! We talk a bit about the
|
modeling code—and it looks like we goofed it, and started gradually
|
||||||
cleanups, the partnerships, and the future!
|
losing some data!
|
||||||
%p
|
%p
|
||||||
The themes are stability, simplicity, and sustainability. We've been
|
We've restored a backup from before we made these changes, so most
|
||||||
online for 15 years now, and we're gonna keep doing our best to keep
|
everything is back in order! None of your personal data was affected.
|
||||||
DTI here for a long time to come!
|
Sorry for the disruption, and hope everyone is doing okay! 💜
|
||||||
|
|
||||||
#outfit-forms
|
#outfit-forms
|
||||||
#pet-preview
|
#pet-preview
|
||||||
|
@ -68,16 +67,18 @@
|
||||||
= submit_tag t('.infinite_closet.item_search.submit')
|
= submit_tag t('.infinite_closet.item_search.submit')
|
||||||
|
|
||||||
%li
|
%li
|
||||||
%h3= link_to t('modeling_hub'), bulk_pets_path
|
%h3= link_to t('rainbow_pool'), pet_types_path
|
||||||
= link_to bulk_pets_path do
|
= link_to pet_types_path do
|
||||||
= image_tag 'https://images.neopets.com/items/mall_ac_garland_spotlight.gif'
|
= image_tag 'rainbow_pool.png'
|
||||||
.section-info
|
.section-info
|
||||||
%strong= t '.modeling_hub.tagline'
|
%strong= t('.rainbow_pool.tagline')
|
||||||
%p= t '.modeling_hub.description'
|
%p= t('.rainbow_pool.description')
|
||||||
= form_tag load_pet_path, method: 'POST' do
|
= form_with url: pet_types_path, method: 'GET' do |form|
|
||||||
= pet_name_tag placeholder: t('.modeling_hub.load_pet.placeholder'),
|
= form.select :color, @colors.map(&:human_name),
|
||||||
required: true
|
include_blank: t('.rainbow_pool.filters.color')
|
||||||
= submit_tag t('.modeling_hub.load_pet.submit')
|
= form.select :species, @species.map(&:human_name),
|
||||||
|
include_blank: t('.rainbow_pool.filters.species')
|
||||||
|
= form.submit t('.rainbow_pool.filters.submit'), name: nil
|
||||||
|
|
||||||
- if @latest_contribution # will be nil for a fresh copy of the site ;P
|
- if @latest_contribution # will be nil for a fresh copy of the site ;P
|
||||||
#latest-contribution
|
#latest-contribution
|
||||||
|
@ -90,15 +91,14 @@
|
||||||
%h3= t '.newest_items.unmodeled.header'
|
%h3= t '.newest_items.unmodeled.header'
|
||||||
%ul#newest-unmodeled-items
|
%ul#newest-unmodeled-items
|
||||||
- @newest_unmodeled_items.each do |item|
|
- @newest_unmodeled_items.each do |item|
|
||||||
- cache "items/#{item.id} modeling_progress locale=#{I18n.locale} updated_at=#{item.updated_at.to_i}" do
|
%li{'data-item-id' => item.id}
|
||||||
%li{'data-item-id' => item.id}
|
= link_to image_tag(item.thumbnail_url), item, :class => 'image-link'
|
||||||
= link_to image_tag(item.thumbnail_url), item, :class => 'image-link'
|
= link_to item, :class => 'header' do
|
||||||
= link_to item, :class => 'header' do
|
%h2= item.name
|
||||||
%h2= item.name
|
%span.meter{style: "width: #{@newest_unmodeled_items_predicted_modeled_ratio[item]*100}%"}
|
||||||
%span.meter{style: "width: #{@newest_unmodeled_items_predicted_modeled_ratio[item]*100}%"}
|
.missing-bodies
|
||||||
.missing-bodies
|
= render_predicted_missing_species_by_color(@newest_unmodeled_items_predicted_missing_species_by_color[item])
|
||||||
= render_predicted_missing_species_by_color(@newest_unmodeled_items_predicted_missing_species_by_color[item])
|
.models
|
||||||
.models
|
|
||||||
- if @newest_modeled_items.present?
|
- if @newest_modeled_items.present?
|
||||||
%h3= t '.newest_items.modeled.header'
|
%h3= t '.newest_items.modeled.header'
|
||||||
%ul#newest-modeled-items
|
%ul#newest-modeled-items
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
%li
|
%li
|
||||||
= link_to [pet_state.pet_type, pet_state] do
|
= link_to useful_pet_state_path(pet_state.pet_type, pet_state) do
|
||||||
= outfit_viewer pet_state:
|
= outfit_viewer pet_state:, class: "preview"
|
||||||
.name= pose_name pet_state.pose
|
.name= pose_name pet_state.pose
|
||||||
- if pet_state.glitched?
|
- if pet_state.glitched?
|
||||||
%span.glitched{title: "Glitched"} 👾
|
%span.glitched{title: "Glitched"} 👾
|
||||||
|
|
48
app/views/pet_states/edit.html.haml
Normal file
48
app/views/pet_states/edit.html.haml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
- title "#{@pet_type.human_name}: #{pose_name @pet_state.pose}"
|
||||||
|
- use_responsive_design
|
||||||
|
|
||||||
|
%ol.breadcrumbs
|
||||||
|
%li
|
||||||
|
= link_to "Rainbow Pool", pet_types_path
|
||||||
|
%li
|
||||||
|
= link_to @pet_type.possibly_new_color.human_name,
|
||||||
|
pet_types_path(color: @pet_type.possibly_new_color.human_name)
|
||||||
|
%li{"data-relation-to-prev": "sibling"}
|
||||||
|
= link_to @pet_type.possibly_new_species.human_name,
|
||||||
|
pet_types_path(species: @pet_type.possibly_new_species.human_name)
|
||||||
|
%li
|
||||||
|
= link_to "Appearances", @pet_type
|
||||||
|
%li
|
||||||
|
\##{@pet_state.id}
|
||||||
|
|
||||||
|
= outfit_viewer pet_state: @pet_state
|
||||||
|
|
||||||
|
= form_with model: [@pet_type, @pet_state] do |f|
|
||||||
|
- if @pet_state.errors.any?
|
||||||
|
%p
|
||||||
|
Could not save:
|
||||||
|
%ul.errors
|
||||||
|
- @pet_state.errors.each do |error|
|
||||||
|
%li= error.full_message
|
||||||
|
%dl
|
||||||
|
%dt Pose
|
||||||
|
%dd
|
||||||
|
%ul.pose-options
|
||||||
|
- pose_options.each do |pose|
|
||||||
|
%li
|
||||||
|
%label
|
||||||
|
= f.radio_button :pose, pose
|
||||||
|
= pose_name pose
|
||||||
|
%dt Glitched?
|
||||||
|
%dd
|
||||||
|
= f.select :glitched, [["✅ Not marked as Glitched", false],
|
||||||
|
["👾 Yes, it's bad news bonko'd", true]]
|
||||||
|
= f.submit "Save"
|
||||||
|
|
||||||
|
- content_for :stylesheets do
|
||||||
|
= stylesheet_link_tag "application/breadcrumbs"
|
||||||
|
= stylesheet_link_tag "application/outfit-viewer"
|
||||||
|
= page_stylesheet_link_tag "pet_states/edit"
|
||||||
|
|
||||||
|
- content_for :javascripts do
|
||||||
|
= javascript_include_tag "outfit-viewer"
|
|
@ -1,36 +0,0 @@
|
||||||
- title "#{@pet_type.human_name}: #{pose_name @pet_state.pose} [\##{@pet_state.id}]"
|
|
||||||
- use_responsive_design
|
|
||||||
|
|
||||||
= outfit_viewer pet_state: @pet_state
|
|
||||||
|
|
||||||
%dl
|
|
||||||
%dt{title: "Pose usually affects just the eyes and mouth. Neopets " +
|
|
||||||
"genders these as Male/Female, but I don't like those " +
|
|
||||||
"terms for like… it's just eyelashes! Sheesh!"}
|
|
||||||
Pose
|
|
||||||
%dd
|
|
||||||
= pose_name @pet_state.pose
|
|
||||||
- if @pet_state.pose == "UNCONVERTED"
|
|
||||||
(Retired, replaced by #{link_to "Alt Styles", alt_styles_path})
|
|
||||||
|
|
||||||
%dt{title: "This is our own internal ID number, nothing to do with " +
|
|
||||||
"Neopets's official data."}
|
|
||||||
DTI ID
|
|
||||||
%dd= @pet_state.id
|
|
||||||
|
|
||||||
%dt{title: "When we notice a form looks wrong, we mark it Glitched, to " +
|
|
||||||
"tell our systems to prefer other forms for this pose instead."}
|
|
||||||
Glitched?
|
|
||||||
%dd
|
|
||||||
- if @pet_state.glitched?
|
|
||||||
👾 Yes, it's bad news bonko'd
|
|
||||||
- else
|
|
||||||
✅ Not marked as Glitched
|
|
||||||
|
|
||||||
- content_for :stylesheets do
|
|
||||||
= stylesheet_link_tag "application/hanger-spinner"
|
|
||||||
= stylesheet_link_tag "application/outfit-viewer"
|
|
||||||
= stylesheet_link_tag "pet_states/show"
|
|
||||||
|
|
||||||
- content_for :javascripts do
|
|
||||||
= javascript_include_tag "outfit-viewer", async: true
|
|
|
@ -1,4 +1,21 @@
|
||||||
%li
|
%li
|
||||||
= link_to pet_type do
|
= link_to pet_type do
|
||||||
= pet_type_image pet_type, :happy, :thumb
|
= pet_type_image pet_type, :happy, :thumb, class: "preview"
|
||||||
.name= pet_type.human_name
|
.name= pet_type.human_name
|
||||||
|
.info
|
||||||
|
- if support_staff?
|
||||||
|
%p
|
||||||
|
- if pet_type.num_unlabeled_states > 0
|
||||||
|
%span{title: "Unlabeled states"}
|
||||||
|
❓️ #{pet_type.num_unlabeled_states} +
|
||||||
|
%span{title: "Labeled main poses"}
|
||||||
|
- if pet_type.fully_labeled?
|
||||||
|
✅ #{pet_type.num_poses}/#{pet_type.num_poses}
|
||||||
|
- else
|
||||||
|
= moon_progress pet_type.num_poses, pet_type.num_poses + pet_type.num_missing_poses
|
||||||
|
#{pet_type.num_poses}/#{pet_type.num_poses + pet_type.num_missing_poses}
|
||||||
|
%p
|
||||||
|
Added
|
||||||
|
= time_tag pet_type.created_at,
|
||||||
|
title: pet_type.created_at.to_formatted_s(:long_nst) do
|
||||||
|
= time_with_only_month_if_old pet_type.created_at
|
||||||
|
|
|
@ -1,18 +1,28 @@
|
||||||
- title "Rainbow Pool"
|
- title "Rainbow Pool"
|
||||||
- use_responsive_design
|
- use_responsive_design
|
||||||
|
|
||||||
= form_with method: :get, class: "pet-filters" do |form|
|
:markdown
|
||||||
%fieldset
|
Welcome, welcome! These are all the colors and species of pet we've seen
|
||||||
%legend Filter by:
|
before. We have [NC Pet Styles][1], too!
|
||||||
= form.select :color, @color_names, selected: @selected_color&.human_name, include_blank: "Color…"
|
|
||||||
= form.select :species, @species_names, selected: @selected_species&.human_name, include_blank: "Species…"
|
|
||||||
= form.submit "Go"
|
|
||||||
|
|
||||||
= will_paginate @pet_types
|
If you've seen a new kind of pet, you can enter its name on the homepage to
|
||||||
|
show us! Thank you so much 💖
|
||||||
|
|
||||||
%ui.pet-types= render @pet_types
|
[1]: #{alt_styles_path}
|
||||||
|
|
||||||
= will_paginate @pet_types
|
= form_with method: :get, class: "rainbow-pool-filters" do |form|
|
||||||
|
%fieldset
|
||||||
|
%legend Filter by:
|
||||||
|
= form.select :color, @color_names, selected: @selected_color&.human_name, include_blank: "Color…"
|
||||||
|
= form.select :species, @species_names, selected: @selected_species&.human_name, include_blank: "Species…"
|
||||||
|
= form.submit "Go", name: nil
|
||||||
|
|
||||||
|
- if @pet_types.present?
|
||||||
|
= will_paginate @pet_types, class: "rainbow-pool-pagination"
|
||||||
|
%ui.rainbow-pool-list= render @pet_types
|
||||||
|
= will_paginate @pet_types, class: "rainbow-pool-pagination"
|
||||||
|
- else
|
||||||
|
%p.rainbow-pool-no-results No matching pets found!
|
||||||
|
|
||||||
- content_for :stylesheets do
|
- content_for :stylesheets do
|
||||||
= stylesheet_link_tag "pet_types/index"
|
= stylesheet_link_tag "application/rainbow-pool"
|
||||||
|
|
|
@ -1,19 +1,56 @@
|
||||||
- title "#{@pet_type.human_name}"
|
- title "#{@pet_type.human_name}"
|
||||||
- use_responsive_design
|
- use_responsive_design
|
||||||
|
|
||||||
%ul.pet-states
|
%ol.breadcrumbs
|
||||||
|
%li
|
||||||
|
= link_to "Rainbow Pool", pet_types_path
|
||||||
|
%li
|
||||||
|
= link_to @pet_type.possibly_new_color.human_name,
|
||||||
|
pet_types_path(color: @pet_type.possibly_new_color.human_name)
|
||||||
|
%li{"data-relation-to-prev": "sibling"}
|
||||||
|
= link_to @pet_type.possibly_new_species.human_name,
|
||||||
|
pet_types_path(species: @pet_type.possibly_new_species.human_name)
|
||||||
|
%li
|
||||||
|
Appearances
|
||||||
|
|
||||||
|
%p
|
||||||
|
These are the various appearances we've seen for this pet! We've hand-labeled
|
||||||
|
them by their emotion and their gender expression, as best we can.
|
||||||
|
|
||||||
|
%p
|
||||||
|
If you've seen another kind of #{@pet_type.human_name}, you can enter its
|
||||||
|
name on the homepage to show us! Thank you 💖
|
||||||
|
|
||||||
|
- if @pet_states[:canonical].any?(&:glitched?)
|
||||||
|
%p
|
||||||
|
Some of these appearances are marked as "glitched", but it's still the
|
||||||
|
best sample we have. If someone models an unglitched alternative for us,
|
||||||
|
we'll use that instead!
|
||||||
|
|
||||||
|
%ul.rainbow-pool-list
|
||||||
= render @pet_states[:canonical]
|
= render @pet_states[:canonical]
|
||||||
|
|
||||||
- if @pet_states[:other].present?
|
- if @pet_states[:other].present?
|
||||||
%details
|
%h3 Other appearances
|
||||||
%summary Other
|
|
||||||
%ul.pet-states
|
%p
|
||||||
= render @pet_states[:other]
|
These are some other appearances we've seen over time!
|
||||||
|
- if @pet_states[:other].any?(&:labeled?)
|
||||||
|
The labeled appearances here don't appear in the outfit editor by
|
||||||
|
default anymore, because they've been replaced by better alternatives.
|
||||||
|
- unless @pet_states[:other].all?(&:labeled?)
|
||||||
|
The unlabeled appearances here <em>might</em> be what we show in the
|
||||||
|
outfit editor later, once we have the chance to label them.
|
||||||
|
|
||||||
|
%ul.rainbow-pool-list
|
||||||
|
= render @pet_states[:other]
|
||||||
|
|
||||||
- content_for :stylesheets do
|
- content_for :stylesheets do
|
||||||
|
= stylesheet_link_tag "application/breadcrumbs"
|
||||||
= stylesheet_link_tag "application/hanger-spinner"
|
= stylesheet_link_tag "application/hanger-spinner"
|
||||||
= stylesheet_link_tag "application/outfit-viewer"
|
= stylesheet_link_tag "application/outfit-viewer"
|
||||||
= stylesheet_link_tag "pet_types/show"
|
= stylesheet_link_tag "application/rainbow-pool"
|
||||||
|
= page_stylesheet_link_tag "pet_types/show"
|
||||||
|
|
||||||
- content_for :javascripts do
|
- content_for :javascripts do
|
||||||
= javascript_include_tag "outfit-viewer", async: true
|
= javascript_include_tag "outfit-viewer", async: true
|
||||||
|
|
|
@ -72,6 +72,12 @@ module OpenneoImpressItems
|
||||||
# version number, etc. So let's only send this to Neopets systems, where it
|
# version number, etc. So let's only send this to Neopets systems, where it
|
||||||
# should hopefully be clear who we are from context!
|
# should hopefully be clear who we are from context!
|
||||||
config.user_agent_for_neopets = "Dress to Impress"
|
config.user_agent_for_neopets = "Dress to Impress"
|
||||||
|
|
||||||
|
# Use the usual Neopets.com, unless we have an override. (At times, we've
|
||||||
|
# used this in collaboration with TNT to address the server directly,
|
||||||
|
# instead of through the CDN.)
|
||||||
|
config.neopets_origin =
|
||||||
|
ENV.fetch('NEOPETS_URL_ORIGIN', 'https://www.neopets.com')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,10 @@ Rails.application.configure do
|
||||||
# Allow connections on Vagrant's private network.
|
# Allow connections on Vagrant's private network.
|
||||||
config.web_console.permissions = '10.0.2.2'
|
config.web_console.permissions = '10.0.2.2'
|
||||||
|
|
||||||
|
# Allow pets to model new data. (If modeling is ever broken, disable this in
|
||||||
|
# production while we fix it!)
|
||||||
|
config.modeling_enabled = true
|
||||||
|
|
||||||
# Use a local copy of Impress 2020, presumably running on port 4000. (Can
|
# Use a local copy of Impress 2020, presumably running on port 4000. (Can
|
||||||
# override this with the IMPRESS_2020_ORIGIN environment variable!)
|
# override this with the IMPRESS_2020_ORIGIN environment variable!)
|
||||||
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
||||||
|
|
|
@ -122,6 +122,10 @@ Rails.application.configure do
|
||||||
# Skip DNS rebinding protection for the default health check endpoint.
|
# Skip DNS rebinding protection for the default health check endpoint.
|
||||||
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
|
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
|
||||||
|
|
||||||
|
# Allow pets to model new data. (If modeling is ever broken, disable this
|
||||||
|
# here while we fix it!)
|
||||||
|
config.modeling_enabled = true
|
||||||
|
|
||||||
# Use the live copy of Impress 2020. (Can override this with the
|
# Use the live copy of Impress 2020. (Can override this with the
|
||||||
# IMPRESS_2020_ORIGIN environment variable!)
|
# IMPRESS_2020_ORIGIN environment variable!)
|
||||||
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
||||||
|
|
|
@ -62,6 +62,10 @@ Rails.application.configure do
|
||||||
# Raise error when a before_action's only/except options reference missing actions
|
# Raise error when a before_action's only/except options reference missing actions
|
||||||
config.action_controller.raise_on_missing_callback_actions = true
|
config.action_controller.raise_on_missing_callback_actions = true
|
||||||
|
|
||||||
|
# Allow pets to model new data. (If modeling is ever broken, disable this in
|
||||||
|
# production while we fix it!)
|
||||||
|
config.modeling_enabled = true
|
||||||
|
|
||||||
# Use a local copy of Impress 2020, presumably running on port 4000. (Can
|
# Use a local copy of Impress 2020, presumably running on port 4000. (Can
|
||||||
# override this with the IMPRESS_2020_ORIGIN environment variable!)
|
# override this with the IMPRESS_2020_ORIGIN environment variable!)
|
||||||
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
||||||
|
|
|
@ -1 +1,5 @@
|
||||||
Date::DATE_FORMATS[:month_and_day] = "%B %e"
|
Date::DATE_FORMATS[:month_and_day] = "%B %e"
|
||||||
|
Time::DATE_FORMATS[:long_nst] = lambda { |time|
|
||||||
|
time.in_time_zone("Pacific Time (US & Canada)").
|
||||||
|
to_formatted_s(:long) + " NST"
|
||||||
|
}
|
||||||
|
|
|
@ -19,10 +19,10 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
# Teach Zeitwerk that `RocketAMF` is what to expect in `lib/rocketamf`.
|
# Teach Zeitwerk that `RocketAMF` is what to expect in `lib/rocketamf`.
|
||||||
inflect.acronym "RocketAMF"
|
inflect.acronym "RocketAMF"
|
||||||
|
|
||||||
# Teach Zeitwerk that `NeoPass` is what to expect in `app/services/neopass.rb`.
|
# Teach Zeitwerk that `NeoPass` is what to expect in `neopass.rb`.
|
||||||
inflect.acronym "NeoPass"
|
inflect.acronym "NeoPass"
|
||||||
|
|
||||||
# Teach Zeitwerk that "NCMall" is what to expect in `app/services/nc_mall.rb`.
|
# Teach Zeitwerk that "NCMall" is what to expect in `nc_mall.rb`.
|
||||||
# (We do this by teaching it the word "NC".)
|
# (We do this by teaching it the word "NC".)
|
||||||
inflect.acronym "NC"
|
inflect.acronym "NC"
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,7 +35,8 @@ en-MEEP:
|
||||||
terms: Terms of Use (meeped Sep 2022)
|
terms: Terms of Use (meeped Sep 2022)
|
||||||
contact: Meeptact
|
contact: Meeptact
|
||||||
email: Questions, comments, meepits
|
email: Questions, comments, meepits
|
||||||
copyright: Images © 1999–%{year} World of Neopets, Inc. All Rights Reserved.
|
copyright:
|
||||||
|
Images © 1999–%{year} World of Neopets, Inc. All Rights Reserved.
|
||||||
Used With Permission. Meep.
|
Used With Permission. Meep.
|
||||||
|
|
||||||
items:
|
items:
|
||||||
|
@ -553,14 +554,6 @@ en-MEEP:
|
||||||
item_search:
|
item_search:
|
||||||
placeholder: meep an item…
|
placeholder: meep an item…
|
||||||
submit: meep
|
submit: meep
|
||||||
modeling_hub:
|
|
||||||
tagline: Found somemeep?
|
|
||||||
description:
|
|
||||||
Meep a pet's meep here and we'll meep a meep of what it's wearing.
|
|
||||||
Thanks so meep!
|
|
||||||
load_pet:
|
|
||||||
placeholder: meep a pet…
|
|
||||||
submit: meep
|
|
||||||
latest_contribution:
|
latest_contribution:
|
||||||
header: Contribumeeps
|
header: Contribumeeps
|
||||||
description_html: "%{user_link} meeped us %{contributed_description}.
|
description_html: "%{user_link} meeped us %{contributed_description}.
|
||||||
|
@ -634,10 +627,6 @@ en-MEEP:
|
||||||
|
|
||||||
load:
|
load:
|
||||||
not_found: We couldn't meep a pet by that name. Is it meeped correctly?
|
not_found: We couldn't meep a pet by that name. Is it meeped correctly?
|
||||||
asset_download_error:
|
|
||||||
We meeped the pet and what it's wearing, but couldn't meep the
|
|
||||||
associated meepia files. Maybe Neopets is down, or changed their
|
|
||||||
firewall rules? Please meep again later!
|
|
||||||
pet_download_error:
|
pet_download_error:
|
||||||
We couldn't meep to Neopets to meep up the pet. Maybe they're down.
|
We couldn't meep to Neopets to meep up the pet. Maybe they're down.
|
||||||
Please try meep later!
|
Please try meep later!
|
||||||
|
|
|
@ -4,6 +4,7 @@ en:
|
||||||
your_items: Your Items
|
your_items: Your Items
|
||||||
infinite_closet: Infinite Closet
|
infinite_closet: Infinite Closet
|
||||||
modeling_hub: Modeling Hub
|
modeling_hub: Modeling Hub
|
||||||
|
rainbow_pool: Rainbow Pool
|
||||||
locale_name: English
|
locale_name: English
|
||||||
|
|
||||||
activerecord:
|
activerecord:
|
||||||
|
@ -32,11 +33,12 @@ en:
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
source_code: Source Code
|
source_code: Source Code
|
||||||
terms: Terms of Use (updated %{date})
|
terms: Terms of Use (%{date})
|
||||||
blog: Blog
|
blog: Blog
|
||||||
contact: Contact
|
contact: Contact
|
||||||
email: Questions, comments, bugs
|
email: Questions, comments, bugs
|
||||||
copyright: Images © 1999–%{year} World of Neopets, Inc. All Rights Reserved.
|
copyright:
|
||||||
|
Images © 1999–%{year} World of Neopets, Inc. All Rights Reserved.
|
||||||
Used With Permission
|
Used With Permission
|
||||||
|
|
||||||
items:
|
items:
|
||||||
|
@ -167,8 +169,7 @@ en:
|
||||||
submit: Save
|
submit: Save
|
||||||
edit: Edit
|
edit: Edit
|
||||||
delete: Delete
|
delete: Delete
|
||||||
delete_confirmation:
|
delete_confirmation: Are you sure you want to delete "%{list_name}"?
|
||||||
Are you sure you want to delete "%{list_name}"?
|
|
||||||
If you do, we'll delete all the items in it, too.
|
If you do, we'll delete all the items in it, too.
|
||||||
remove_all:
|
remove_all:
|
||||||
confirm: "Remove all items from this list?"
|
confirm: "Remove all items from this list?"
|
||||||
|
@ -215,7 +216,6 @@ en:
|
||||||
|
|
||||||
colors:
|
colors:
|
||||||
default_human_name: (a new color)
|
default_human_name: (a new color)
|
||||||
prank_suffix: (fake)
|
|
||||||
|
|
||||||
contributions:
|
contributions:
|
||||||
contributed_description:
|
contributed_description:
|
||||||
|
@ -229,7 +229,7 @@ en:
|
||||||
swf_asset_html: "%{item_description} on a new body type"
|
swf_asset_html: "%{item_description} on a new body type"
|
||||||
pet_type_html: "%{pet_type_description} for the first time"
|
pet_type_html: "%{pet_type_description} for the first time"
|
||||||
pet_state_html: "a new pose for %{pet_type_description}"
|
pet_state_html: "a new pose for %{pet_type_description}"
|
||||||
alt_style_html: "a new Alt Style of the %{alt_style_name}"
|
alt_style_html: "a new NC Style of the %{alt_style_name}"
|
||||||
|
|
||||||
contribution:
|
contribution:
|
||||||
description_html: "%{user_link} showed us %{contributed_description}"
|
description_html: "%{user_link} showed us %{contributed_description}"
|
||||||
|
@ -678,14 +678,15 @@ en:
|
||||||
item_search:
|
item_search:
|
||||||
placeholder: find an item…
|
placeholder: find an item…
|
||||||
submit: search
|
submit: search
|
||||||
modeling_hub:
|
rainbow_pool:
|
||||||
tagline: Found something?
|
tagline: Explore your options!
|
||||||
description:
|
description:
|
||||||
Enter a pet's name here and we'll keep a copy of what it's wearing.
|
Browse the colors you can paint your pets, and the "style" options
|
||||||
Thanks so much!
|
from the NC Mall!
|
||||||
load_pet:
|
filters:
|
||||||
placeholder: model a pet…
|
species: Species…
|
||||||
submit: submit
|
color: Color…
|
||||||
|
submit: Go
|
||||||
latest_contribution:
|
latest_contribution:
|
||||||
header: Contributions
|
header: Contributions
|
||||||
description_html: "%{user_link} showed us %{contributed_description}.
|
description_html: "%{user_link} showed us %{contributed_description}.
|
||||||
|
@ -756,10 +757,6 @@ en:
|
||||||
|
|
||||||
load:
|
load:
|
||||||
not_found: We couldn't find a pet by that name. Is it spelled correctly?
|
not_found: We couldn't find a pet by that name. Is it spelled correctly?
|
||||||
asset_download_error:
|
|
||||||
We found the pet and what it's wearing, but couldn't download the
|
|
||||||
associated media files. Maybe Neopets is down, or changed their
|
|
||||||
firewall rules? Please try again later!
|
|
||||||
pet_download_error:
|
pet_download_error:
|
||||||
We couldn't connect to Neopets to look up the pet. Maybe they're down.
|
We couldn't connect to Neopets to look up the pet. Maybe they're down.
|
||||||
Please try again later!
|
Please try again later!
|
||||||
|
|
|
@ -116,8 +116,7 @@ es:
|
||||||
submit: Guardar
|
submit: Guardar
|
||||||
edit: Editar
|
edit: Editar
|
||||||
delete: Eliminar
|
delete: Eliminar
|
||||||
delete_confirmation:
|
delete_confirmation: ¿Estás seguro/a que quieres eliminar la lista "%{list_name}"?
|
||||||
¿Estás seguro/a que quieres eliminar la lista "%{list_name}"?
|
|
||||||
empty: Esta lista está vacía.
|
empty: Esta lista está vacía.
|
||||||
edit:
|
edit:
|
||||||
title: Editando la lista "%{list_name}"
|
title: Editando la lista "%{list_name}"
|
||||||
|
@ -453,12 +452,6 @@ es:
|
||||||
item_search:
|
item_search:
|
||||||
placeholder: buscar un objeto...
|
placeholder: buscar un objeto...
|
||||||
submit: buscar
|
submit: buscar
|
||||||
modeling_hub:
|
|
||||||
tagline: ¿Has encontrado algo?
|
|
||||||
description: Si no encuentras un objeto y sabes de un pet que lo lleve ¡Escribe su nombre aquí!
|
|
||||||
load_pet:
|
|
||||||
placeholder: desfilar con un pet...
|
|
||||||
submit: enviar
|
|
||||||
latest_contribution:
|
latest_contribution:
|
||||||
header: Contribuciones
|
header: Contribuciones
|
||||||
description_html: "%{user_link} nos ha mostrado %{contributed_description}. ¡Muchas gracias, %{user_link}!"
|
description_html: "%{user_link} nos ha mostrado %{contributed_description}. ¡Muchas gracias, %{user_link}!"
|
||||||
|
@ -503,7 +496,6 @@ es:
|
||||||
submission_success: "%{points} puntos"
|
submission_success: "%{points} puntos"
|
||||||
load:
|
load:
|
||||||
not_found: No hemos podido encontrar a un pet con ese nombre. ¿Lo has escrito correctamente?
|
not_found: No hemos podido encontrar a un pet con ese nombre. ¿Lo has escrito correctamente?
|
||||||
asset_download_error: Hemos encontrado el pet que intentas vestir, pero no hemos podido descargar las imágenes que lo asocian. Posiblemente Neopets está caído. ¡Por favor inténtalo de nuevo más tarde!
|
|
||||||
pet_download_error: No hemos podido conectar con Neopets para ver tu pet. Posiblemente el servidor de Neopets se ha caído. ¡Por favor inténtalo de nuevo más tarde!
|
pet_download_error: No hemos podido conectar con Neopets para ver tu pet. Posiblemente el servidor de Neopets se ha caído. ¡Por favor inténtalo de nuevo más tarde!
|
||||||
users:
|
users:
|
||||||
index:
|
index:
|
||||||
|
|
|
@ -114,8 +114,7 @@ pt:
|
||||||
submit: Salvar
|
submit: Salvar
|
||||||
edit: Editar
|
edit: Editar
|
||||||
delete: Excluir
|
delete: Excluir
|
||||||
delete_confirmation:
|
delete_confirmation: Você tem certeza que deseja excluir "%{list_name}"?
|
||||||
Você tem certeza que deseja excluir "%{list_name}"?
|
|
||||||
empty: Esta lista está vazia.
|
empty: Esta lista está vazia.
|
||||||
edit:
|
edit:
|
||||||
title: Editando lista "%{list_name}"
|
title: Editando lista "%{list_name}"
|
||||||
|
@ -449,12 +448,6 @@ pt:
|
||||||
item_search:
|
item_search:
|
||||||
placeholder: Procurar um item…
|
placeholder: Procurar um item…
|
||||||
submit: Vai!
|
submit: Vai!
|
||||||
modeling_hub:
|
|
||||||
tagline: Encontrou alguma coisa?
|
|
||||||
description: Digite o nome do pet aqui e nós vamos copiar o que ele está vestindo. Muito Obrigado.
|
|
||||||
load_pet:
|
|
||||||
placeholder: modele um pet…
|
|
||||||
submit: Enviar
|
|
||||||
latest_contribution:
|
latest_contribution:
|
||||||
header: Contribuições
|
header: Contribuições
|
||||||
description_html: "%{user_link} nos mostrou %{contributed_description}. Obrigado, %{user_link}!"
|
description_html: "%{user_link} nos mostrou %{contributed_description}. Obrigado, %{user_link}!"
|
||||||
|
@ -497,7 +490,6 @@ pt:
|
||||||
submission_success: "%{points} pontos"
|
submission_success: "%{points} pontos"
|
||||||
load:
|
load:
|
||||||
not_found: Não foi possível achar um pet com esse nome. Está escrito corretamente?
|
not_found: Não foi possível achar um pet com esse nome. Está escrito corretamente?
|
||||||
asset_download_error: Nós achamos o pet e o que ele está vestindo, mas não foi possível baixar os dados. Talvez Neopets esteja fora do ar. Por favor tente mais tarde!
|
|
||||||
pet_download_error: Nós não conseguimos conectar ao Neopets para achar o pet. Talvez eles estejam fora do ar. Por favor tente mais tarde!
|
pet_download_error: Nós não conseguimos conectar ao Neopets para achar o pet. Talvez eles estejam fora do ar. Por favor tente mais tarde!
|
||||||
users:
|
users:
|
||||||
index:
|
index:
|
||||||
|
|
|
@ -19,7 +19,7 @@ OpenneoImpressItems::Application.routes.draw do
|
||||||
get '/users/current-user/outfits', to: redirect('/your-outfits')
|
get '/users/current-user/outfits', to: redirect('/your-outfits')
|
||||||
|
|
||||||
# Our customization data! Both the item pages, and JSON API endpoints.
|
# Our customization data! Both the item pages, and JSON API endpoints.
|
||||||
resources :items, :only => [:index, :show] do
|
resources :items, only: [:index, :show, :edit, :update] do
|
||||||
resources :trades, path: 'trades/:type', controller: 'item_trades',
|
resources :trades, path: 'trades/:type', controller: 'item_trades',
|
||||||
only: [:index], constraints: {type: /offering|seeking/}
|
only: [:index], constraints: {type: /offering|seeking/}
|
||||||
|
|
||||||
|
@ -35,12 +35,16 @@ OpenneoImpressItems::Application.routes.draw do
|
||||||
end
|
end
|
||||||
resources :alt_styles, path: 'alt-styles', only: [:index]
|
resources :alt_styles, path: 'alt-styles', only: [:index]
|
||||||
end
|
end
|
||||||
resources :alt_styles, path: 'alt-styles', only: [:index]
|
|
||||||
resources :swf_assets, path: 'swf-assets', only: [:show]
|
resources :swf_assets, path: 'swf-assets', only: [:show]
|
||||||
|
scope "rainbow-pool" do
|
||||||
|
resources :alt_styles, path: 'alt-styles', only: [:index, :edit, :update],
|
||||||
|
path: 'styles'
|
||||||
|
end
|
||||||
resources :pet_types, path: 'rainbow-pool', param: "name",
|
resources :pet_types, path: 'rainbow-pool', param: "name",
|
||||||
only: [:index, :show] do
|
only: [:index, :show] do
|
||||||
resources :pet_states, only: [:show], path: "forms"
|
resources :pet_states, only: [:edit, :update], path: "appearances"
|
||||||
end
|
end
|
||||||
|
get '/alt-styles', to: redirect('/rainbow-pool/styles')
|
||||||
|
|
||||||
# Loading and modeling pets!
|
# Loading and modeling pets!
|
||||||
post '/pets/load' => 'pets#load', :as => :load_pet
|
post '/pets/load' => 'pets#load', :as => :load_pet
|
||||||
|
|
5
db/migrate/20240928022359_remove_prank_from_colors.rb
Normal file
5
db/migrate/20240928022359_remove_prank_from_colors.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class RemovePrankFromColors < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
remove_column "colors", "prank", :boolean, default: false, null: false
|
||||||
|
end
|
||||||
|
end
|
16
db/migrate/20241001052510_add_cached_fields_to_items.rb
Normal file
16
db/migrate/20241001052510_add_cached_fields_to_items.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
class AddCachedFieldsToItems < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :items, :cached_occupied_zone_ids, :string, null: false, default: ""
|
||||||
|
add_column :items, :cached_compatible_body_ids, :text, null: false, default: ""
|
||||||
|
|
||||||
|
reversible do |direction|
|
||||||
|
direction.up do
|
||||||
|
puts "Updating cached item fields for all items…"
|
||||||
|
Item.includes(:swf_assets).find_in_batches.with_index do |items, batch|
|
||||||
|
puts "Updating item batch ##{batch+1}…"
|
||||||
|
items.each(&:update_cached_fields)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
class AllowNullInItemsCachedFields < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
# This is a bit more compatible with ActiveRecord's `serialize` utility,
|
||||||
|
# which seems pretty insistent that empty arrays should be saved as `NULL`,
|
||||||
|
# rather than the empty string our serializer would return if called :(
|
||||||
|
change_column_null :items, :cached_compatible_body_ids, true
|
||||||
|
change_column_null :items, :cached_occupied_zone_ids, true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddTimestampsToPetStates < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_timestamps :pet_states, null: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
class IncreasePetTypeColorIdAndSpeciesIdLimit < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
reversible do |direction|
|
||||||
|
change_table :pet_types do |t|
|
||||||
|
direction.up do
|
||||||
|
t.change :color_id, :integer, null: false
|
||||||
|
t.change :species_id, :integer, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
direction.down do
|
||||||
|
t.change :color_id, :integer, limit: 1, null: false
|
||||||
|
t.change :species_id, :integer, limit: 1, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
db/migrate/20241116041926_increase_id_limits.rb
Normal file
21
db/migrate/20241116041926_increase_id_limits.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
class IncreaseIdLimits < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
reversible do |direction|
|
||||||
|
direction.up do
|
||||||
|
change_column :parents_swf_assets, :parent_id, :integer, null: false
|
||||||
|
change_column :parents_swf_assets, :swf_asset_id, :integer, null: false
|
||||||
|
change_column :pet_states, :pet_type_id, :integer, null: false
|
||||||
|
change_column :pets, :pet_type_id, :integer, null: false
|
||||||
|
change_column :swf_assets, :zone_id, :integer, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
direction.down do
|
||||||
|
change_column :parents_swf_assets, :parent_id, :integer, limit: 3, null: false
|
||||||
|
change_column :parents_swf_assets, :swf_asset_id, :integer, limit: 3, null: false
|
||||||
|
change_column :pet_states, :pet_type_id, :integer, limit: 3, null: false
|
||||||
|
change_column :pets, :pet_type_id, :integer, limit: 3, null: false
|
||||||
|
change_column :swf_assets, :zone_id, :integer, limit: 1, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,16 @@
|
||||||
|
class AddCachedPredictedFullyModeledToItems < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :items, :cached_predicted_fully_modeled, :boolean,
|
||||||
|
default: false, null: false
|
||||||
|
|
||||||
|
reversible do |direction|
|
||||||
|
direction.up do
|
||||||
|
puts "Updating cached item fields for all items…"
|
||||||
|
Item.includes(:swf_assets).find_in_batches.with_index do |items, batch|
|
||||||
|
puts "Updating item batch ##{batch+1}…"
|
||||||
|
items.each(&:update_cached_fields)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_04_08_120359) do
|
ActiveRecord::Schema[7.2].define(version: 2024_04_08_120359) do
|
||||||
create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
|
create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
|
||||||
t.string "name", limit: 30, null: false
|
t.string "name", limit: 30, null: false
|
||||||
t.string "encrypted_password", limit: 64
|
t.string "encrypted_password", limit: 64
|
||||||
|
@ -37,5 +37,4 @@ ActiveRecord::Schema[7.1].define(version: 2024_04_08_120359) do
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
|
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
24
db/schema.rb
24
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
|
||||||
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "species_id", null: false
|
t.integer "species_id", null: false
|
||||||
t.integer "color_id", null: false
|
t.integer "color_id", null: false
|
||||||
|
@ -75,7 +75,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
||||||
create_table "colors", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "colors", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.boolean "basic"
|
t.boolean "basic"
|
||||||
t.boolean "standard"
|
t.boolean "standard"
|
||||||
t.boolean "prank", default: false, null: false
|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "pb_item_name"
|
t.string "pb_item_name"
|
||||||
t.string "pb_item_thumbnail_url"
|
t.string "pb_item_thumbnail_url"
|
||||||
|
@ -138,6 +137,9 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
||||||
t.text "description", size: :medium, null: false
|
t.text "description", size: :medium, null: false
|
||||||
t.string "rarity", default: "", null: false
|
t.string "rarity", default: "", null: false
|
||||||
t.integer "dyeworks_base_item_id"
|
t.integer "dyeworks_base_item_id"
|
||||||
|
t.string "cached_occupied_zone_ids", default: ""
|
||||||
|
t.text "cached_compatible_body_ids", default: ""
|
||||||
|
t.boolean "cached_predicted_fully_modeled", default: false, null: false
|
||||||
t.index ["dyeworks_base_item_id"], name: "index_items_on_dyeworks_base_item_id"
|
t.index ["dyeworks_base_item_id"], name: "index_items_on_dyeworks_base_item_id"
|
||||||
t.index ["modeling_status_hint", "created_at", "id"], name: "items_modeling_status_hint_and_created_at_and_id"
|
t.index ["modeling_status_hint", "created_at", "id"], name: "items_modeling_status_hint_and_created_at_and_id"
|
||||||
t.index ["modeling_status_hint", "created_at"], name: "items_modeling_status_hint_and_created_at"
|
t.index ["modeling_status_hint", "created_at"], name: "items_modeling_status_hint_and_created_at"
|
||||||
|
@ -155,7 +157,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "modeling_logs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "modeling_logs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false
|
t.datetime "created_at", precision: nil, default: -> { "current_timestamp()" }, null: false
|
||||||
t.text "log_json", size: :long, null: false
|
t.text "log_json", size: :long, null: false
|
||||||
t.string "pet_name", limit: 128, null: false
|
t.string "pet_name", limit: 128, null: false
|
||||||
end
|
end
|
||||||
|
@ -195,8 +197,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "parents_swf_assets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "parents_swf_assets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "parent_id", limit: 3, null: false
|
t.integer "parent_id", null: false
|
||||||
t.integer "swf_asset_id", limit: 3, null: false
|
t.integer "swf_asset_id", null: false
|
||||||
t.string "parent_type", limit: 8, null: false
|
t.string "parent_type", limit: 8, null: false
|
||||||
t.index ["parent_id", "parent_type"], name: "index_parents_swf_assets_on_parent_id_and_parent_type"
|
t.index ["parent_id", "parent_type"], name: "index_parents_swf_assets_on_parent_id_and_parent_type"
|
||||||
t.index ["parent_id", "swf_asset_id"], name: "unique_parents_swf_assets", unique: true
|
t.index ["parent_id", "swf_asset_id"], name: "unique_parents_swf_assets", unique: true
|
||||||
|
@ -210,7 +212,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "pet_states", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "pet_states", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "pet_type_id", limit: 3, null: false
|
t.integer "pet_type_id", null: false
|
||||||
t.text "swf_asset_ids", size: :medium, null: false
|
t.text "swf_asset_ids", size: :medium, null: false
|
||||||
t.boolean "female"
|
t.boolean "female"
|
||||||
t.integer "mood_id"
|
t.integer "mood_id"
|
||||||
|
@ -218,12 +220,14 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
||||||
t.boolean "labeled", default: false, null: false
|
t.boolean "labeled", default: false, null: false
|
||||||
t.boolean "glitched", default: false, null: false
|
t.boolean "glitched", default: false, null: false
|
||||||
t.string "artist_neopets_username"
|
t.string "artist_neopets_username"
|
||||||
|
t.datetime "created_at"
|
||||||
|
t.datetime "updated_at"
|
||||||
t.index ["pet_type_id"], name: "pet_states_pet_type_id"
|
t.index ["pet_type_id"], name: "pet_states_pet_type_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "pet_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "pet_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "color_id", limit: 1, null: false
|
t.integer "color_id", null: false
|
||||||
t.integer "species_id", limit: 1, null: false
|
t.integer "species_id", null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.integer "body_id", limit: 2, null: false
|
t.integer "body_id", limit: 2, null: false
|
||||||
t.string "image_hash", limit: 8
|
t.string "image_hash", limit: 8
|
||||||
|
@ -237,7 +241,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
||||||
|
|
||||||
create_table "pets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "pets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.string "name", limit: 20, null: false
|
t.string "name", limit: 20, null: false
|
||||||
t.integer "pet_type_id", limit: 3, null: false
|
t.integer "pet_type_id", null: false
|
||||||
t.index ["name"], name: "pets_name", unique: true
|
t.index ["name"], name: "pets_name", unique: true
|
||||||
t.index ["pet_type_id"], name: "pets_pet_type_id"
|
t.index ["pet_type_id"], name: "pets_pet_type_id"
|
||||||
end
|
end
|
||||||
|
@ -250,7 +254,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_16_001002) do
|
||||||
t.string "type", limit: 7, null: false
|
t.string "type", limit: 7, null: false
|
||||||
t.integer "remote_id", limit: 3, null: false
|
t.integer "remote_id", limit: 3, null: false
|
||||||
t.text "url", size: :long, null: false
|
t.text "url", size: :long, null: false
|
||||||
t.integer "zone_id", limit: 1, null: false
|
t.integer "zone_id", null: false
|
||||||
t.text "zones_restrict", size: :medium, null: false
|
t.text "zones_restrict", size: :medium, null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.integer "body_id", limit: 2, null: false
|
t.integer "body_id", limit: 2, null: false
|
||||||
|
|
|
@ -442,13 +442,21 @@
|
||||||
mode: "755"
|
mode: "755"
|
||||||
state: directory
|
state: directory
|
||||||
|
|
||||||
- name: Create 10min cron job to run `rails nc_mall:sync`
|
- name: Remove 10min cron job to run `rails nc_mall:sync`
|
||||||
become_user: impress
|
become_user: impress
|
||||||
cron:
|
cron:
|
||||||
|
state: absent
|
||||||
name: "Impress: sync NC Mall data"
|
name: "Impress: sync NC Mall data"
|
||||||
minute: "*/10"
|
minute: "*/10"
|
||||||
job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails nc_mall:sync'"
|
job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails nc_mall:sync'"
|
||||||
|
|
||||||
|
- name: Create 10min cron job to run `rails neopets:import:nc_mall`
|
||||||
|
become_user: impress
|
||||||
|
cron:
|
||||||
|
name: "Impress: import NC Mall data"
|
||||||
|
minute: "*/10"
|
||||||
|
job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails neopets:import:nc_mall'"
|
||||||
|
|
||||||
- name: Create weekly cron job to run `rails public_data:commit`
|
- name: Create weekly cron job to run `rails public_data:commit`
|
||||||
become_user: impress
|
become_user: impress
|
||||||
cron:
|
cron:
|
||||||
|
|
11
lib/serializers/integer_set.rb
Normal file
11
lib/serializers/integer_set.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module Serializers
|
||||||
|
module IntegerSet
|
||||||
|
def self.dump(array)
|
||||||
|
array.sort.join(",")
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load(string)
|
||||||
|
(string || "").split(",").map(&:to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
12
lib/tasks/items.rake
Normal file
12
lib/tasks/items.rake
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
namespace :items do
|
||||||
|
desc "Update cached fields for all items (useful if logic changes)"
|
||||||
|
task :update_cached_fields => :environment do
|
||||||
|
puts "Updating cached item fields for all items…"
|
||||||
|
Item.includes(:swf_assets).find_in_batches.with_index do |items, batch|
|
||||||
|
puts "Updating item batch ##{batch+1}…"
|
||||||
|
Item.transaction do
|
||||||
|
items.each(&:update_cached_fields)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
lib/tasks/neopets/import.rake
Normal file
31
lib/tasks/neopets/import.rake
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
module Neologin
|
||||||
|
def self.cookie
|
||||||
|
raise "must run neopets:import:neologin first" if @cookie.nil?
|
||||||
|
@cookie
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.cookie?
|
||||||
|
@cookie.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.cookie=(new_cookie)
|
||||||
|
@cookie = new_cookie
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :neopets do
|
||||||
|
task :import => [
|
||||||
|
"neopets:import:neologin",
|
||||||
|
"neopets:import:nc_mall",
|
||||||
|
"neopets:import:rainbow_pool",
|
||||||
|
"neopets:import:styling_studio",
|
||||||
|
]
|
||||||
|
|
||||||
|
namespace :import do
|
||||||
|
task :neologin do
|
||||||
|
unless Neologin.cookie?
|
||||||
|
Neologin.cookie = STDIN.getpass("Neologin cookie: ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,9 +1,11 @@
|
||||||
namespace :nc_mall do
|
namespace "neopets:import" do
|
||||||
desc "Sync our NCMallRecord table with the live NC Mall"
|
desc "Sync our NCMallRecord table with the live NC Mall"
|
||||||
task :sync => :environment do
|
task :nc_mall => :environment do
|
||||||
# Log to STDOUT.
|
# Log to STDOUT.
|
||||||
Rails.logger = Logger.new(STDOUT)
|
Rails.logger = Logger.new(STDOUT)
|
||||||
|
|
||||||
|
puts "Importing from NC Mall…"
|
||||||
|
|
||||||
# First, load all records of what's being sold in the live NC Mall. We load
|
# First, load all records of what's being sold in the live NC Mall. We load
|
||||||
# the homepage and all pages linked from the main document, and extract the
|
# the homepage and all pages linked from the main document, and extract the
|
||||||
# items from each. (We also de-duplicate the items, which is important
|
# items from each. (We also de-duplicate the items, which is important
|
||||||
|
@ -77,17 +79,17 @@ end
|
||||||
def load_all_nc_mall_pages
|
def load_all_nc_mall_pages
|
||||||
Sync do
|
Sync do
|
||||||
# First, start loading the homepage.
|
# First, start loading the homepage.
|
||||||
homepage_task = Async { NCMall.load_home_page }
|
homepage_task = Async { Neopets::NCMall.load_home_page }
|
||||||
|
|
||||||
# Next, load the page links for different categories etc.
|
# Next, load the page links for different categories etc.
|
||||||
links = NCMall.load_page_links
|
links = Neopets::NCMall.load_page_links
|
||||||
|
|
||||||
# Next, load the linked pages, 10 at a time.
|
# Next, load the linked pages, 10 at a time.
|
||||||
barrier = Async::Barrier.new
|
barrier = Async::Barrier.new
|
||||||
semaphore = Async::Semaphore.new(10, parent: barrier)
|
semaphore = Async::Semaphore.new(10, parent: barrier)
|
||||||
begin
|
begin
|
||||||
linked_page_tasks = links.map do |link|
|
linked_page_tasks = links.map do |link|
|
||||||
semaphore.async { NCMall.load_page link[:type], link[:cat] }
|
semaphore.async { Neopets::NCMall.load_page link[:type], link[:cat] }
|
||||||
end
|
end
|
||||||
barrier.wait # Load all the pages.
|
barrier.wait # Load all the pages.
|
||||||
ensure
|
ensure
|
|
@ -1,10 +1,10 @@
|
||||||
require "addressable/template"
|
require "addressable/template"
|
||||||
require "async/http/internet/instance"
|
require "async/http/internet/instance"
|
||||||
|
|
||||||
namespace :rainbow_pool do
|
namespace "neopets:import" do
|
||||||
desc "Import all basic image hashes from the Rainbow Pool, onto PetTypes"
|
desc "Import all basic image hashes from the Rainbow Pool, onto PetTypes"
|
||||||
task :import => :environment do
|
task :rainbow_pool => ["neopets:import:neologin", :environment] do
|
||||||
neologin = STDIN.getpass("Neologin cookie: ")
|
puts "Importing from Rainbow Pool…"
|
||||||
|
|
||||||
all_pet_types = PetType.all.to_a
|
all_pet_types = PetType.all.to_a
|
||||||
all_pet_types_by_species_id_and_color_id = all_pet_types.
|
all_pet_types_by_species_id_and_color_id = all_pet_types.
|
||||||
|
@ -16,7 +16,7 @@ namespace :rainbow_pool do
|
||||||
Species.order(:name).each do |species|
|
Species.order(:name).each do |species|
|
||||||
begin
|
begin
|
||||||
hashes_by_color_name = RainbowPool.load_hashes_for_species(
|
hashes_by_color_name = RainbowPool.load_hashes_for_species(
|
||||||
species.id, neologin)
|
species.id, Neologin.cookie)
|
||||||
rescue => error
|
rescue => error
|
||||||
puts "Failed to load #{species.name} page, skipping: #{error.message}"
|
puts "Failed to load #{species.name} page, skipping: #{error.message}"
|
||||||
next
|
next
|
87
lib/tasks/neopets/import/styling_studio.rake
Normal file
87
lib/tasks/neopets/import/styling_studio.rake
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
namespace "neopets:import" do
|
||||||
|
desc "Import alt style info from the NC Styling Studio"
|
||||||
|
task :styling_studio => ["neopets:import:neologin", :environment] do
|
||||||
|
puts "Importing from Styling Studio…"
|
||||||
|
|
||||||
|
all_species = Species.order(:name).to_a
|
||||||
|
|
||||||
|
# Load 10 species pages from the NC Mall at a time.
|
||||||
|
barrier = Async::Barrier.new
|
||||||
|
semaphore = Async::Semaphore.new(10, parent: barrier)
|
||||||
|
styles_by_species_id = {}
|
||||||
|
Sync do
|
||||||
|
num_loaded = 0
|
||||||
|
num_total = all_species.size
|
||||||
|
print "0/#{num_total} species loaded"
|
||||||
|
|
||||||
|
all_species.each do |species|
|
||||||
|
semaphore.async {
|
||||||
|
begin
|
||||||
|
styles_by_species_id[species.id] = Neopets::NCMall.load_styles(
|
||||||
|
species_id: species.id,
|
||||||
|
neologin: Neologin.cookie,
|
||||||
|
)
|
||||||
|
rescue => error
|
||||||
|
puts "\n⚠️ Error loading for #{species.human_name}, skipping: #{error.message}"
|
||||||
|
end
|
||||||
|
num_loaded += 1
|
||||||
|
print "\r#{num_loaded}/#{num_total} species loaded"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Wait until all tasks are done.
|
||||||
|
barrier.wait
|
||||||
|
ensure
|
||||||
|
barrier.stop # If something goes wrong, clean up all tasks.
|
||||||
|
end
|
||||||
|
print "\n"
|
||||||
|
|
||||||
|
style_ids = styles_by_species_id.values.flatten(1).map { |s| s[:oii] }
|
||||||
|
style_records_by_id =
|
||||||
|
AltStyle.where(id: style_ids).to_h { |as| [as.id, as] }
|
||||||
|
|
||||||
|
all_species.each do |species|
|
||||||
|
styles = styles_by_species_id[species.id]
|
||||||
|
next if styles.nil?
|
||||||
|
|
||||||
|
counts = {changed: 0, unchanged: 0, skipped: 0}
|
||||||
|
styles.each do |style|
|
||||||
|
record = style_records_by_id[style[:oii]]
|
||||||
|
label = "#{style[:name]} (#{style[:oii]})"
|
||||||
|
if record.nil?
|
||||||
|
puts "⚠️ [#{label}]: Not modeled yet, skipping"
|
||||||
|
counts[:skipped] += 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
if !record.real_thumbnail_url?
|
||||||
|
record.thumbnail_url = style[:image]
|
||||||
|
puts "✅ [#{label}]: Thumbnail URL is now #{style[:image].inspect}"
|
||||||
|
elsif record.thumbnail_url != style[:image]
|
||||||
|
puts "⚠️ [#{label}: Thumbnail URL may have changed, handle manually? " +
|
||||||
|
"#{record.thumbnail_url.inspect} -> #{style[:image].inspect}"
|
||||||
|
end
|
||||||
|
|
||||||
|
new_series_name = style[:name].match(/\A\S+/)[0] # first word
|
||||||
|
if !record.real_series_name?
|
||||||
|
record.series_name = new_series_name
|
||||||
|
puts "✅ [#{label}]: Series name is now #{new_series_name.inspect}"
|
||||||
|
elsif record.series_name != new_series_name
|
||||||
|
puts "⚠️ [#{label}: Series name may have changed, handle manually? " +
|
||||||
|
"#{record.series_name.inspect} -> #{new_series_name.inspect}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if record.changed?
|
||||||
|
counts[:changed] += 1
|
||||||
|
else
|
||||||
|
counts[:unchanged] += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
record.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{species.human_name}: #{counts[:changed]} changed, " +
|
||||||
|
"#{counts[:unchanged]} unchanged, #{counts[:skipped]} skipped"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,8 @@
|
||||||
namespace :pets do
|
namespace :pets do
|
||||||
desc "Load a pet's viewer data"
|
desc "Load a pet's viewer data"
|
||||||
task :load, [:name] => [:environment] do |task, args|
|
task :load, [:name] => [:environment] do |task, args|
|
||||||
pp Pet.fetch_viewer_data(args[:name])
|
viewer_data = Neopets::CustomPets.fetch_viewer_data(args[:name])
|
||||||
|
puts JSON.pretty_generate(viewer_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Find pets that were, last we saw, of the given color and species"
|
desc "Find pets that were, last we saw, of the given color and species"
|
||||||
|
@ -10,7 +11,7 @@ namespace :pets do
|
||||||
pt = PetType.matching_name(args.color_name, args.species_name).first!
|
pt = PetType.matching_name(args.color_name, args.species_name).first!
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
abort "Could not find pet type for " +
|
abort "Could not find pet type for " +
|
||||||
"#{args.color_name} #{args.species_name}"
|
"#{args.color_name} #{args.species_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
limit = ENV.fetch("LIMIT", 10)
|
limit = ENV.fetch("LIMIT", 10)
|
||||||
|
|
28
spec/fixtures/colors.yml
vendored
Normal file
28
spec/fixtures/colors.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
blue:
|
||||||
|
id: 8
|
||||||
|
name: blue
|
||||||
|
basic: true
|
||||||
|
green:
|
||||||
|
id: 34
|
||||||
|
name: green
|
||||||
|
basic: true
|
||||||
|
maraquan:
|
||||||
|
id: 44
|
||||||
|
name: maraquan
|
||||||
|
standard: false
|
||||||
|
purple:
|
||||||
|
id: 57
|
||||||
|
name: purple
|
||||||
|
red:
|
||||||
|
id: 61
|
||||||
|
name: red
|
||||||
|
basic: true
|
||||||
|
robot:
|
||||||
|
id: 62
|
||||||
|
name: robot
|
||||||
|
striped:
|
||||||
|
id: 77
|
||||||
|
name: striped
|
||||||
|
swamp_gas:
|
||||||
|
id: 93
|
||||||
|
name: "swamp gas"
|
30
spec/fixtures/items.yml
vendored
Normal file
30
spec/fixtures/items.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
straw_hat:
|
||||||
|
id: 58
|
||||||
|
name: Straw Hat
|
||||||
|
description: "This straw hat will keep the sun out of your pets eyes in
|
||||||
|
bright sunlight."
|
||||||
|
thumbnail_url: https://images.neopets.com/items/straw-hat.gif
|
||||||
|
type: Clothes
|
||||||
|
category: Clothes
|
||||||
|
rarity: Very Rare
|
||||||
|
rarity_index: 90
|
||||||
|
price: 376
|
||||||
|
weight_lbs: 1
|
||||||
|
zones_restrict: "0000000000000000000000000001000000001010000000000000"
|
||||||
|
species_support_ids: "35"
|
||||||
|
created_at: "2011-03-28T14:33:36-07:00"
|
||||||
|
|
||||||
|
birthday_bg:
|
||||||
|
id: 89876
|
||||||
|
name: Birthday Bash Background
|
||||||
|
description: This place is all set for a brilliant birthday bash!
|
||||||
|
thumbnail_url: https://images.neopets.com/items/9a4gd6g6c0.gif
|
||||||
|
type: none
|
||||||
|
category: None
|
||||||
|
rarity: Special
|
||||||
|
rarity_index: 101
|
||||||
|
price: 0
|
||||||
|
weight_lbs: 1
|
||||||
|
zones_restrict: "0000000000000000000000000000000000000000000000000000"
|
||||||
|
species_support_ids: ""
|
||||||
|
created_at: "2024-11-15T18:15:22-08:00"
|
19
spec/fixtures/pet_types.yml
vendored
Normal file
19
spec/fixtures/pet_types.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
blue_acara:
|
||||||
|
color_id: 8
|
||||||
|
species_id: 1
|
||||||
|
body_id: 123
|
||||||
|
|
||||||
|
newcolor_acara:
|
||||||
|
color_id: 123
|
||||||
|
species_id: 1
|
||||||
|
body_id: 123
|
||||||
|
|
||||||
|
blue_newspecies:
|
||||||
|
color_id: 8
|
||||||
|
species_id: 456
|
||||||
|
body_id: 123
|
||||||
|
|
||||||
|
newcolor_newspecies:
|
||||||
|
color_id: 123
|
||||||
|
species_id: 456
|
||||||
|
body_id: 123
|
18
spec/fixtures/species.yml
vendored
Normal file
18
spec/fixtures/species.yml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
acara:
|
||||||
|
id: 1
|
||||||
|
name: acara
|
||||||
|
blumaroo:
|
||||||
|
id: 3
|
||||||
|
name: blumaroo
|
||||||
|
chia:
|
||||||
|
id: 7
|
||||||
|
name: chia
|
||||||
|
jetsam:
|
||||||
|
id: 20
|
||||||
|
name: jetsam
|
||||||
|
mynci:
|
||||||
|
id: 35
|
||||||
|
name: mynci
|
||||||
|
vandagyre:
|
||||||
|
id: 55
|
||||||
|
name: vandagyre
|
282
spec/fixtures/zones.yml
vendored
Normal file
282
spec/fixtures/zones.yml
vendored
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
music:
|
||||||
|
id: 1
|
||||||
|
depth: 1
|
||||||
|
type_id: 4
|
||||||
|
label: Music
|
||||||
|
plain_label: music
|
||||||
|
soundeffects:
|
||||||
|
id: 2
|
||||||
|
depth: 2
|
||||||
|
type_id: 4
|
||||||
|
label: Sound Effects
|
||||||
|
plain_label: soundeffects
|
||||||
|
background:
|
||||||
|
id: 3
|
||||||
|
depth: 3
|
||||||
|
type_id: 3
|
||||||
|
label: Background
|
||||||
|
plain_label: background
|
||||||
|
biologyeffects:
|
||||||
|
id: 4
|
||||||
|
depth: 6
|
||||||
|
type_id: 1
|
||||||
|
label: Biology Effects
|
||||||
|
plain_label: biologyeffects
|
||||||
|
hindbiology:
|
||||||
|
id: 5
|
||||||
|
depth: 7
|
||||||
|
type_id: 1
|
||||||
|
label: Hind Biology
|
||||||
|
plain_label: hindbiology
|
||||||
|
markings:
|
||||||
|
id: 31
|
||||||
|
depth: 35
|
||||||
|
type_id: 2
|
||||||
|
label: Markings
|
||||||
|
plain_label: markings
|
||||||
|
hinddisease:
|
||||||
|
id: 7
|
||||||
|
depth: 9
|
||||||
|
type_id: 1
|
||||||
|
label: Hind Disease
|
||||||
|
plain_label: hinddisease
|
||||||
|
hindcover:
|
||||||
|
id: 8
|
||||||
|
depth: 10
|
||||||
|
type_id: 2
|
||||||
|
label: Hind Cover
|
||||||
|
plain_label: hindcover
|
||||||
|
hindtransientbiology:
|
||||||
|
id: 9
|
||||||
|
depth: 11
|
||||||
|
type_id: 1
|
||||||
|
label: Hind Transient Biology
|
||||||
|
plain_label: hindtransientbiology
|
||||||
|
hinddrippings:
|
||||||
|
id: 10
|
||||||
|
depth: 12
|
||||||
|
type_id: 1
|
||||||
|
label: Hind Drippings
|
||||||
|
plain_label: hinddrippings
|
||||||
|
backpack:
|
||||||
|
id: 11
|
||||||
|
depth: 13
|
||||||
|
type_id: 2
|
||||||
|
label: Backpack
|
||||||
|
plain_label: backpack
|
||||||
|
wingstransientbiology:
|
||||||
|
id: 12
|
||||||
|
depth: 14
|
||||||
|
type_id: 1
|
||||||
|
label: Wings Transient Biology
|
||||||
|
plain_label: wingstransientbiology
|
||||||
|
wings:
|
||||||
|
id: 13
|
||||||
|
depth: 15
|
||||||
|
type_id: 2
|
||||||
|
label: Wings
|
||||||
|
plain_label: wings
|
||||||
|
hairback:
|
||||||
|
id: 14
|
||||||
|
depth: 17
|
||||||
|
type_id: 1
|
||||||
|
label: Hair Back
|
||||||
|
plain_label: hairback
|
||||||
|
body:
|
||||||
|
id: 15
|
||||||
|
depth: 18
|
||||||
|
type_id: 1
|
||||||
|
label: Body
|
||||||
|
plain_label: body
|
||||||
|
bodydisease:
|
||||||
|
id: 17
|
||||||
|
depth: 20
|
||||||
|
type_id: 1
|
||||||
|
label: Body Disease
|
||||||
|
plain_label: bodydisease
|
||||||
|
feettransientbiology:
|
||||||
|
id: 18
|
||||||
|
depth: 21
|
||||||
|
type_id: 1
|
||||||
|
label: Feet Transient Biology
|
||||||
|
plain_label: feettransientbiology
|
||||||
|
shoes:
|
||||||
|
id: 19
|
||||||
|
depth: 22
|
||||||
|
type_id: 2
|
||||||
|
label: Shoes
|
||||||
|
plain_label: shoes
|
||||||
|
lowerbodytransientbiology:
|
||||||
|
id: 20
|
||||||
|
depth: 23
|
||||||
|
type_id: 1
|
||||||
|
label: Lower-body Transient Biology
|
||||||
|
plain_label: lowerbodytransientbiology
|
||||||
|
trousers:
|
||||||
|
id: 21
|
||||||
|
depth: 24
|
||||||
|
type_id: 2
|
||||||
|
label: Trousers
|
||||||
|
plain_label: trousers
|
||||||
|
upperbodytransientbiology:
|
||||||
|
id: 22
|
||||||
|
depth: 25
|
||||||
|
type_id: 1
|
||||||
|
label: Upper-body Transient Biology
|
||||||
|
plain_label: upperbodytransientbiology
|
||||||
|
shirtdress:
|
||||||
|
id: 23
|
||||||
|
depth: 26
|
||||||
|
type_id: 2
|
||||||
|
label: Shirt/Dress
|
||||||
|
plain_label: shirtdress
|
||||||
|
necklace:
|
||||||
|
id: 24
|
||||||
|
depth: 28
|
||||||
|
type_id: 2
|
||||||
|
label: Necklace
|
||||||
|
plain_label: necklace
|
||||||
|
gloves:
|
||||||
|
id: 25
|
||||||
|
depth: 29
|
||||||
|
type_id: 2
|
||||||
|
label: Gloves
|
||||||
|
plain_label: gloves
|
||||||
|
jacket:
|
||||||
|
id: 26
|
||||||
|
depth: 30
|
||||||
|
type_id: 2
|
||||||
|
label: Jacket
|
||||||
|
plain_label: jacket
|
||||||
|
collar:
|
||||||
|
id: 27
|
||||||
|
depth: 31
|
||||||
|
type_id: 2
|
||||||
|
label: Collar
|
||||||
|
plain_label: collar
|
||||||
|
bodydrippings:
|
||||||
|
id: 28
|
||||||
|
depth: 32
|
||||||
|
type_id: 1
|
||||||
|
label: Body Drippings
|
||||||
|
plain_label: bodydrippings
|
||||||
|
ruff:
|
||||||
|
id: 29
|
||||||
|
depth: 33
|
||||||
|
type_id: 1
|
||||||
|
label: Ruff
|
||||||
|
plain_label: ruff
|
||||||
|
head:
|
||||||
|
id: 30
|
||||||
|
depth: 34
|
||||||
|
type_id: 1
|
||||||
|
label: Head
|
||||||
|
plain_label: head
|
||||||
|
headdisease:
|
||||||
|
id: 32
|
||||||
|
depth: 36
|
||||||
|
type_id: 1
|
||||||
|
label: Head Disease
|
||||||
|
plain_label: headdisease
|
||||||
|
eyes:
|
||||||
|
id: 33
|
||||||
|
depth: 37
|
||||||
|
type_id: 1
|
||||||
|
label: Eyes
|
||||||
|
plain_label: eyes
|
||||||
|
mouth:
|
||||||
|
id: 34
|
||||||
|
depth: 38
|
||||||
|
type_id: 1
|
||||||
|
label: Mouth
|
||||||
|
plain_label: mouth
|
||||||
|
glasses:
|
||||||
|
id: 35
|
||||||
|
depth: 41
|
||||||
|
type_id: 2
|
||||||
|
label: Glasses
|
||||||
|
plain_label: glasses
|
||||||
|
earrings:
|
||||||
|
id: 41
|
||||||
|
depth: 45
|
||||||
|
type_id: 2
|
||||||
|
label: Earrings
|
||||||
|
plain_label: earrings
|
||||||
|
hairfront:
|
||||||
|
id: 37
|
||||||
|
depth: 40
|
||||||
|
type_id: 1
|
||||||
|
label: Hair Front
|
||||||
|
plain_label: hairfront
|
||||||
|
headtransientbiology:
|
||||||
|
id: 38
|
||||||
|
depth: 42
|
||||||
|
type_id: 1
|
||||||
|
label: Head Transient Biology
|
||||||
|
plain_label: headtransientbiology
|
||||||
|
headdrippings:
|
||||||
|
id: 39
|
||||||
|
depth: 43
|
||||||
|
type_id: 1
|
||||||
|
label: Head Drippings
|
||||||
|
plain_label: headdrippings
|
||||||
|
hat:
|
||||||
|
id: 50
|
||||||
|
depth: 16
|
||||||
|
type_id: 2
|
||||||
|
label: Hat
|
||||||
|
plain_label: hat
|
||||||
|
righthanditem:
|
||||||
|
id: 49
|
||||||
|
depth: 5
|
||||||
|
type_id: 2
|
||||||
|
label: Right-hand Item
|
||||||
|
plain_label: righthanditem
|
||||||
|
lefthanditem:
|
||||||
|
id: 43
|
||||||
|
depth: 47
|
||||||
|
type_id: 2
|
||||||
|
label: Left-hand Item
|
||||||
|
plain_label: lefthanditem
|
||||||
|
higherforegrounditem:
|
||||||
|
id: 44
|
||||||
|
depth: 49
|
||||||
|
type_id: 3
|
||||||
|
label: Higher Foreground Item
|
||||||
|
plain_label: higherforegrounditem
|
||||||
|
lowerforegrounditem:
|
||||||
|
id: 45
|
||||||
|
depth: 50
|
||||||
|
type_id: 3
|
||||||
|
label: Lower Foreground Item
|
||||||
|
plain_label: lowerforegrounditem
|
||||||
|
static:
|
||||||
|
id: 46
|
||||||
|
depth: 48
|
||||||
|
type_id: 2
|
||||||
|
label: Static
|
||||||
|
plain_label: static
|
||||||
|
thoughtbubble:
|
||||||
|
id: 47
|
||||||
|
depth: 51
|
||||||
|
type_id: 3
|
||||||
|
label: Thought Bubble
|
||||||
|
plain_label: thoughtbubble
|
||||||
|
backgrounditem:
|
||||||
|
id: 48
|
||||||
|
depth: 4
|
||||||
|
type_id: 3
|
||||||
|
label: Background Item
|
||||||
|
plain_label: backgrounditem
|
||||||
|
belt:
|
||||||
|
id: 51
|
||||||
|
depth: 27
|
||||||
|
type_id: 2
|
||||||
|
label: Belt
|
||||||
|
plain_label: belt
|
||||||
|
foreground:
|
||||||
|
id: 52
|
||||||
|
depth: 52
|
||||||
|
type_id: 3
|
||||||
|
label: Foreground
|
||||||
|
plain_label: foreground
|
38
spec/models/color_spec.rb
Normal file
38
spec/models/color_spec.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
require_relative '../rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Color do
|
||||||
|
fixtures :colors
|
||||||
|
|
||||||
|
describe '#to_param' do
|
||||||
|
it("uses name when possible") do
|
||||||
|
expect(colors(:blue).to_param).to eq "Blue"
|
||||||
|
end
|
||||||
|
|
||||||
|
it("uses spaces for multi-word names") do
|
||||||
|
expect(colors(:swamp_gas).to_param).to eq "Swamp Gas"
|
||||||
|
end
|
||||||
|
|
||||||
|
it("uses IDs for new colors") do
|
||||||
|
expect(Color.new(id: 12345).to_param).to eq "12345"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".param_to_id" do
|
||||||
|
it("looks up by name") do
|
||||||
|
expect(Color.param_to_id("blue")).to eq colors(:blue).id
|
||||||
|
end
|
||||||
|
|
||||||
|
it("is case-insensitive for name") do
|
||||||
|
expect(Color.param_to_id("bLUe")).to eq colors(:blue).id
|
||||||
|
end
|
||||||
|
|
||||||
|
it("returns ID when the param is just a number, even if it doesn't exist") do
|
||||||
|
expect(Color.param_to_id("123456")).to eq 123456
|
||||||
|
end
|
||||||
|
|
||||||
|
it("raises RecordNotFound if no name matches") do
|
||||||
|
expect { Color.param_to_id("nonexistant") }.
|
||||||
|
to raise_error ActiveRecord::RecordNotFound
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
276
spec/models/item_spec.rb
Normal file
276
spec/models/item_spec.rb
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
require_relative '../rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Item do
|
||||||
|
fixtures :items, :colors, :species, :zones
|
||||||
|
|
||||||
|
context "modeling status:" do
|
||||||
|
# Rather than using fixtures of real-world data, we create very specific
|
||||||
|
# pet types, to be able to create small encapsulated test cases where there
|
||||||
|
# are only a few bodies.
|
||||||
|
#
|
||||||
|
# We create some basic color pet types, and some Maraquan pet types—and,
|
||||||
|
# just like irl, the Maraquan Mynci has the same body as the basic Mynci.
|
||||||
|
#
|
||||||
|
# These pet types default to an early creation date of 2005, except the
|
||||||
|
# Vandagyre, which was released in 2014.
|
||||||
|
before do
|
||||||
|
PetType.destroy_all # Make sure no leftovers from e.g. PetType's spec!
|
||||||
|
|
||||||
|
build_pt(colors(:blue), species(:acara), body_id: 1).save!
|
||||||
|
build_pt(colors(:red), species(:acara), body_id: 1).save!
|
||||||
|
build_pt(colors(:blue), species(:blumaroo), body_id: 2).save!
|
||||||
|
build_pt(colors(:green), species(:chia), body_id: 3).save!
|
||||||
|
build_pt(colors(:red), species(:mynci), body_id: 4).save!
|
||||||
|
build_pt(colors(:blue), species(:vandagyre), body_id: 5).tap do |pt|
|
||||||
|
pt.created_at = Date.new(2014, 11, 14)
|
||||||
|
pt.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
build_pt(colors(:maraquan), species(:acara), body_id: 11).save!
|
||||||
|
build_pt(colors(:maraquan), species(:blumaroo), body_id: 12).save!
|
||||||
|
build_pt(colors(:maraquan), species(:chia), body_id: 13).save!
|
||||||
|
build_pt(colors(:maraquan), species(:mynci), body_id: 4).save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_pt(color, species, body_id:)
|
||||||
|
PetType.new(color:, species:, body_id:, created_at: Time.new(2005))
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_item_asset(zone, body_id:)
|
||||||
|
@remote_id = (@remote_id || 0) + 1
|
||||||
|
url = "https://images.neopets.example/#{@remote_id}.swf"
|
||||||
|
SwfAsset.new(type: "object", remote_id: @remote_id, url:,
|
||||||
|
zones_restrict: "", zone:, body_id:)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples "a fully-modeled item" do
|
||||||
|
it("is considered fully modeled") { should be_predicted_fully_modeled }
|
||||||
|
it("predicts no more compatible bodies") do
|
||||||
|
expect(item.predicted_missing_body_ids).to be_empty
|
||||||
|
end
|
||||||
|
it("appears in Item.is_modeled") do
|
||||||
|
expect(Item.is_modeled.find_by_id(item.id)).to be_present
|
||||||
|
end
|
||||||
|
it("does not appear in Item.is_not_modeled") do
|
||||||
|
expect(Item.is_not_modeled.find_by_id(item.id)).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples "a not-fully-modeled item" do
|
||||||
|
it("is not fully modeled") { should_not be_predicted_fully_modeled }
|
||||||
|
it("does not appear in Item.is_modeled") do
|
||||||
|
expect(Item.is_modeled.find_by_id(item.id)).to be_nil
|
||||||
|
end
|
||||||
|
it("appears in Item.is_not_modeled") do
|
||||||
|
expect(Item.is_not_modeled.find_by_id(item.id)).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item without any modeling data" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
it_behaves_like "a not-fully-modeled item"
|
||||||
|
it("has no compatible body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to be_empty
|
||||||
|
end
|
||||||
|
it("predicts all standard bodies are compatible") do
|
||||||
|
expect(item.predicted_missing_body_ids).to contain_exactly(
|
||||||
|
1, 2, 3, 4, 5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item with one species modeled" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("has one compatible body ID") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item with two species modeled" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a not-fully-modeled item"
|
||||||
|
it("has two compatible body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||||
|
end
|
||||||
|
it("predicts remaining standard bodies are compatible") do
|
||||||
|
expect(item.predicted_missing_body_ids).to contain_exactly(3, 4, 5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item with all standard species modeled" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 3)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 4)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("is compatible with all standard body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(1, 2, 3, 4, 5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item that fits all pets the same" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:background), body_id: 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("is compatible with all bodies (body ID = 0)") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item with one Maraquan pet modeled" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("has one compatible body ID") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(11)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item with two Maraquan pets modeled" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 12)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a not-fully-modeled item"
|
||||||
|
it("has two compatible body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(11, 12)
|
||||||
|
end
|
||||||
|
it("predicts remaining Maraquan body IDs are compatible") do
|
||||||
|
expect(item.predicted_missing_body_ids).to contain_exactly(13, 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item with all Maraquan species modeled" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 12)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 13)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 4)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("is compatible with all Maraquan body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(11, 12, 13, 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "a pre-Vandagyre item without any modeling data" do
|
||||||
|
subject(:item) { items(:straw_hat) }
|
||||||
|
|
||||||
|
it_behaves_like "a not-fully-modeled item"
|
||||||
|
it("has no compatible body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to be_empty
|
||||||
|
end
|
||||||
|
it("predicts all standard bodies except Vandagyre are compatible") do
|
||||||
|
expect(item.predicted_missing_body_ids).to contain_exactly(1, 2, 3, 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Skipping "pre-Vanda with one species modeled", because it's identical.
|
||||||
|
|
||||||
|
describe "a pre-Vandagyre item with two species modeled" do
|
||||||
|
subject(:item) { items(:straw_hat) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a not-fully-modeled item"
|
||||||
|
it("has two compatible body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||||
|
end
|
||||||
|
it("predicts remaining standard bodies (sans Vandagyre) are compatible") do
|
||||||
|
expect(item.predicted_missing_body_ids).to contain_exactly(3, 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "a pre-Vandagyre item with all other standard species modeled" do
|
||||||
|
subject(:item) { items(:straw_hat) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 3)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 4)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("is compatible with all non-Vandagyre standard body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(1, 2, 3, 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item without any modeling data, but hinted as done" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before { item.update!(modeling_status_hint: :done) }
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("has no compatible body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item with two species modeled, but hinted as done" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||||
|
item.update!(modeling_status_hint: :done)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("has two compatible body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "an item with two species modeled, but hinted as glitchy" do
|
||||||
|
subject(:item) { items(:birthday_bg) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||||
|
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||||
|
item.update!(modeling_status_hint: :glitchy)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like "a fully-modeled item"
|
||||||
|
it("has two compatible body IDs") do
|
||||||
|
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue