Compare commits

...

15 commits

Author SHA1 Message Date
9838301712 Put the new item preview in a Turbo frame
Nice, gotta say, this is a pretty neat way of making things feel more
app-y! There's some missing pieces here about like, loading state etc,
but the vibes are pretty good, and the implementation was dead-easy!
2024-07-01 14:57:03 -07:00
cf1d28dd62 [WIP] Add species/color picker for simplified item page preview
Still a lot missing here, like choosing the right default for Baby etc
items, and saving the user's preferences. But it's a start!
2024-07-01 14:57:03 -07:00
d9902c6afa [WIP] Start replacing item page preview with simpler HTML-based version
Just stripping out the big React component, and having Rails output it!

There's a lot of work rn in extracting the Impress 2020 dependency from
the `wardrobe-2020` React app, and I'm just curious to see if we can
simplify it at all by pulling this stuff *way* back to basics, and
deleting the item page part of `wardrobe-2020` altogether.

In this draft, we regress a lot of functionality: it just shows the
item on a Blue Acara, with no ability to change it! I'm gonna play with
putting more of that back in.

I also haven't actually removed any of the item page React code; I just
stopped calling it. That can be a cleanup for another time, once we're
confident in this experiment!
2024-07-01 14:57:02 -07:00
a70b70be7d Merge remote-tracking branch 'origin/main' 2024-07-01 14:56:08 -07:00
d5752eac2a Copy edits for Dyeworks in Item Getting Guide
I think the parens are silly now that this paragraph is just kinda all
bonus clarification info anyway. And I wanted to explain the cost
computation for the potions, and highlight the bundle thing!
2024-06-20 14:40:09 -07:00
302c116c8f Don't color PB shop/trade buttons purple in Item Getting Guide
I think it's clearer to just keep the purple meaning "NC", and in
particular "bulk NC Mall purchase"
2024-06-20 14:20:06 -07:00
568c30fa90 Wider tables for longer item names in Item Getting Guide
If the item names are long, it helps to give them more room to breathe!
Whereas if they're short, it looks silly and makes it harder to scan
the table.

Just an extra bit of help for e.g. Dyeworks items with long names!
2024-06-20 14:16:51 -07:00
b137eed4c4 Oops, handle date parsing errors in Dyeworks logic
Huh, I thought I'd tried some invalid dates and they gave me
*surprising* output instead of raising an error. Well, maybe it can do
both, depending on exactly the nature of the unexpected input?

In any case, I found that a bad month name like "UwU" raised an error.
So, let's catch it if so!
2024-06-20 14:08:40 -07:00
965725f9e9 Oops, fix silly bug in Dyeworks Owls date parsing
Oh right, if I assume "date in the past means it's for next year", then
that means that, when the date *does pass*, we won't realize it!

e.g. if Owls says "Dyeable Thru July 15", then on July 14 we'll parse
that as July 15, 2024; but on July 16 we'll parse it as July 16, 2025,
and so we'll think it's *still* dyeable. Under this logic, it's
actually impossible for a limited Dyeworks date to *ever* be in the
past, I think!

I think 3 months is a good compromise: it gives Owls plenty of time to
update, but allows for events that could last as long as 9 months into
the future, if I'm doing my math right.
2024-06-20 14:05:00 -07:00
341a8dd89c Disallow text wrapping in the "Total" cell in Item Getting Guide
The table layout algo can get a bit funky about how it assigns extra
space, I want to encourage things like "Total: 5 items" etc not to
wrap, esp in the Dyeworks case where it's quite long!
2024-06-20 13:55:04 -07:00
3d6abc84dd Layout tweaks to Dyeworks in Item Getting Guide
There's more and more going on in here! Let's omit the base item name,
increase the table width a bit in this case, and tweak the rest a bit
while we're here.
2024-06-20 13:50:48 -07:00
cec29682c4 Add NC Trades button to Dyeworks in Item Getting Guide 2024-06-20 13:50:04 -07:00
589d728c76 Add clearer Dyeworks explainer
I uhhh literally didn't know Dyeworks was a gacha system until Kaye
from the Owls team told me lmao

I should maybe uhh read more guides instead of assuming I've osmosed
things correctly oops!
2024-06-20 13:21:56 -07:00
2e3d5d2020 Vaguer potions info for Dyeworks in Item Getting Guide 2024-06-20 13:14:51 -07:00
97abd6e438 Add probabilities to Dyeworks items in Item Getting Guide
I'm gonna better explain the gacha nature, I'm doing this part first!
2024-06-20 12:54:39 -07:00
9 changed files with 234 additions and 110 deletions

View file

@ -37,3 +37,34 @@ body.items-show
.nc-icon
height: 16px
width: 16px
outfit-viewer
position: relative
display: block
width: 300px
height: 300px
border: 1px solid $module-border-color
border-radius: 1em
overflow: hidden
margin: 0 auto .75em
outfit-layer
display: block
position: absolute
inset: 0
img
width: 100%
height: 100%
.species-color-picker
.error-icon
cursor: help
margin-right: .25em
&[data-is-valid="false"]
select
border-color: $error-border-color
color: $error-color

View file

@ -43,6 +43,7 @@
/* When item names get long, don't let the buttons wrap to give the
* item names more space. The names should wrap more instead! */
text-wrap: nowrap
margin: .25em
tbody
tr
@ -55,14 +56,12 @@
th
text-align: left
.name-cell
text-wrap: nowrap
.thumbnail-cell img
outline: 1px solid $soft-border-color
.actions-cell
button, a.button
/* Bootstrap's Purple 600 */
+awesome-button-color(#59359a)
tr[data-item-owned]
color: #aaa
@ -115,6 +114,15 @@
text-decoration-line: underline
text-decoration-style: dotted
.actions-cell
button, a.button
&[data-action-kind=bulk-nc-mall]
/* Bootstrap's Purple 600 */
+awesome-button-color(#59359a)
&[data-complexity="high"]
width: 70%
/* For wearable items that belong to a specific set that all come together,
* like a Paint Brush. */
&[data-group-type="bundle"]

View file

@ -82,6 +82,17 @@ class ItemsController < ApplicationController
group_by_owned
@current_user_quantities = current_user.item_quantities_for(@item)
end
@selected_preview_pet_type = load_selected_preview_pet_type
@preview_pet_type = load_preview_pet_type
@item_layers = @item.appearance_for(
@preview_pet_type, swf_asset_includes: [:zone]
).swf_assets
@pet_layers = @preview_pet_type.canonical_pet_state.swf_assets.
includes(:zone)
@preview_error = validate_preview
end
format.gif do
@ -189,6 +200,38 @@ class ItemsController < ApplicationController
end
end
def load_selected_preview_pet_type
color_id = params.dig(:preview, :color_id)
species_id = params.dig(:preview, :species_id)
return load_default_preview_pet_type if color_id.nil? || species_id.nil?
PetType.find_or_initialize_by(color_id:, species_id:)
end
def load_preview_pet_type
if @selected_preview_pet_type.persisted?
@selected_preview_pet_type
else
load_default_preview_pet_type
end
end
def load_default_preview_pet_type
PetType.find_by_color_id_and_species_id(
Color.find_by_name("Blue"),
Species.find_by_name("Acara"),
)
end
def validate_preview
if @selected_preview_pet_type.new_record?
:pet_type_does_not_exist
elsif @item_layers.empty?
:no_item_data
end
end
def search_error(e)
@items = []
@query = params[:q]

View file

@ -194,50 +194,34 @@ module ItemsHelper
end
def dyeworks_nc_total_for(items)
dyeworks_items_nc_total_for(items) + dyeworks_potions_nc_total(items.size)
end
def dyeworks_items_nc_total_for(items)
nc_total_for items.map(&:dyeworks_base_item)
end
def dyeworks_potions_nc_total(num_items)
dyeworks_potions_nc_breakdown(num_items)[:nc_total]
def dyeworks_average_num_potions_for(items)
# Compute the number of expected potions for each (inverse of the odds),
# sum them, then round up.
items.map { |i| 1 / i.dyeworks_odds }.sum.ceil
end
def dyeworks_potions_nc_summary(num_items)
dyeworks_potions_nc_breakdown(num_items)[:summary]
def dyeworks_estimated_potions_cost_for(items)
# NOTE: You could do bundles too, but let's just keep it simple.
dyeworks_average_num_potions_for(items) * 125
end
def dyeworks_potions_nc_breakdown(num_items)
nc_total = 0
summaries = []
def complexity_for(items)
max_name_length = items.map(&:name).map(&:length).max
max_name_length >= 40 ? "high" : "low"
end
# For every 10 potions, buy a 10-Bundle for 900 NC.
while num_items >= 10
nc_total += 900
summaries << "10-Bundle (900 NC)"
num_items -= 10
def probability(p)
case p
when 1
"100%"
when 0
"0%"
else
"#{p.numerator} in #{p.denominator}"
end
# For every remaining 5 potions, buy a 5-Bundle for 500 NC.
while num_items >= 5
nc_total += 500
summaries << "5-Bundle (500 NC)"
num_items -= 5
end
# For every remaining potion, buy each directly for 125 NC.
if num_items >= 1
nc_total += num_items * 125
summaries << "#{pluralize num_items, "potion"} (#{num_items * 125} NC)"
num_items = 0
end
summaries << "0 NC" if summaries.empty?
summary = summaries.join(", ")
{nc_total:, summary:}
end
private
@ -260,5 +244,15 @@ module ItemsHelper
def item_header_user_lists_form_state
cookies.fetch("DTIItemPageUserListsFormState", "closed")
end
def outfit_viewer_layers(swf_assets)
swf_assets.map { |a| outfit_viewer_layer(a) }.join("\n").html_safe
end
def outfit_viewer_layer(swf_asset)
content_tag "outfit-layer", style: "z-index: #{swf_asset.zone.depth}" do
image_tag swf_asset.image_url, alt: ""
end
end
end

View file

@ -1,15 +1,2 @@
import React from "react";
import ReactDOM from "react-dom";
import { AppProvider, ItemPageOutfitPreview } from "./wardrobe-2020";
const rootNode = document.querySelector("#outfit-preview-root");
const itemId = rootNode.getAttribute("data-item-id");
// TODO: Use the new React 18 APIs instead!
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
<AppProvider>
<ItemPageOutfitPreview itemId={itemId} />
</AppProvider>,
rootNode,
);
// eslint-disable-next-line no-console
console.log("OwO!");

View file

@ -17,6 +17,8 @@ class Item < ApplicationRecord
has_many :swf_assets, :through => :parent_swf_asset_relationships
belongs_to :dyeworks_base_item, class_name: "Item",
default: -> { inferred_dyeworks_base_item }, optional: true
has_many :dyeworks_variants, class_name: "Item",
inverse_of: :dyeworks_base_item
attr_writer :current_body_id, :owned, :wanted
@ -593,6 +595,10 @@ class Item < ApplicationRecord
end
end
def appearance_for(target, ...)
Item.appearances_for([id], target, ...)[id]
end
# Given a list of item IDs, return how they look on the given target (either
# a pet type or an alt style).
def self.appearances_for(item_ids, target, swf_asset_includes: [])

View file

@ -70,19 +70,39 @@ class Item
match(DYEWORKS_LIMITED_FINAL_DATE_PATTERN)
return nil if match.nil?
# Parse this "<Month> <Day>" date as the *next* such date: parse it as
# this year at first, then add a year if it turns out to be in the past.
# Parse this "<Month> <Day>" date as the *next* such date, with some
# wiggle room for the possibility that it recently passed and Owls hasn't
# updated yet: parse it as this year at first, then add a year if that
# turns out to be more than 3 months ago. (That way, if it's currently
# December 2024, then events ending in Jan will be read as Jan 2025, and
# events ending in Nov will be read as Nov 2024.)
#
# NOTE: This could return strange results if the Owls date contains
# something surprising! But the heuristic nature helps with e.g.
# flexibility if they abbreviate months, so let's lean into `Date.parse`.
match => {month:, day:}
date = Date.parse("#{month} #{day}, #{Date.today.year}")
date += 1.year if date < Date.today
begin
match => {month:, day:}
date = Date.parse("#{month} #{day}, #{Date.today.year}")
date += 1.year if date < Date.today - 3.months
rescue Date::Error
Rails.logger.warn "Could not parse Dyeworks final date: " +
"#{nc_trade_value.value_text.inspect}"
return nil
end
date
end
# The probability of getting this item when dyeing the base item.
def dyeworks_odds
return nil unless dyeworks?
num_variants = dyeworks_base_item.dyeworks_variants.count
raise "Item's Dyeworks base has *no* variants??" if num_variants < 1
Rational(1, num_variants)
end
# Infer what base item this Dyeworks item probably relates to, based on
# their names. We only use this when a new item is modeled to initialize
# the `dyeworks_base_item` relationship in the database; after that, we

View file

@ -13,7 +13,27 @@
how we handle zones. Until then, these items will be <em>very</em> buggy,
sorry!
#outfit-preview-root{'data-item-id': @item.id}
= turbo_frame_tag "item-preview" do
%outfit-viewer
%outfit-pet-appearance
= outfit_viewer_layers @pet_layers
%outfit-item-appearance
= outfit_viewer_layers @item_layers
= form_for item_path(@item), method: :get, class: "species-color-picker",
data: {"is-valid": @preview_error.nil?} do |f|
- if @preview_error == :pet_type_does_not_exist
%span.error-icon{title: "We haven't seen this kind of pet before."} ⚠️
- elsif @preview_error == :no_item_data
%span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️
= select_tag "preview[color_id]",
options_from_collection_for_select(Color.funny.alphabetical,
"id", "human_name", @selected_preview_pet_type.color_id)
= select_tag "preview[species_id]",
options_from_collection_for_select(Species.alphabetical,
"id", "human_name", @selected_preview_pet_type.species_id)
= submit_tag "Go", name: nil
- unless @contributors_with_counts.empty?
#item-contributors

View file

@ -11,17 +11,20 @@
[nc]: https://secure.nc.neopets.com/get-neocash
[gc]: https://secure.nc.neopets.com/nickcash-cards
%table.item-list
%table.item-list{"data-complexity": complexity_for(@items[:nc_mall])}
%thead
%tr
%td
%th
%th.name-cell
Total: #{nc_total_for @items_needed[:nc_mall]} NC
(#{pluralize @items_needed[:nc_mall].size, "item"})
%td.actions-cell
- if @items_needed[:nc_mall].present?
%button{onclick: "alert('Todo!')"}
%button{
onclick: "alert('Todo!')",
data: {"action-kind": "bulk-nc-mall"},
}
= cart_icon alt: ""
Buy all in NC Mall
%tbody
@ -34,34 +37,41 @@
- if @items[:dyeworks].present?
%h2 Dyeworks items
:markdown
These are recolored "Dyeworks" variants of items. First get the "base"
item, then get a Dyeworks Hue Brew Potion, and combine them in the
[Dyeworks][dyeworks] section of the NC Mall! Potions can also be bought in
bundles of 5 or 10.
These are recolored "Dyeworks" variants of items. Dyeworks is a game of
chance: if you have the "base" item and a Dyeworks Hue Brew Potion, you can
[combine them][dyeworks] to receive a random color variant. You keep both
the new item *and* the base item.
If you don't get the color you want, you can use another potion to try
again. It's also common for users to exchange the variants they don't want
via "NC Trading". Potions can be bought individually, or in bundles of 5
or 10.
[dyeworks]: https://www.neopets.com/mall/dyeworks/
%table.item-list
%table.item-list{"data-complexity": complexity_for(@items[:dyeworks])}
%thead
%tr
%td.thumbnail-cell
= image_tag "https://images.neopets.com/items/mall_80x80_cleaning.gif",
alt: "Dyeworks Hue Brew Potion"
%th
%th.name-cell
Total: #{dyeworks_nc_total_for @items_needed[:dyeworks]} NC
= surround "(", ")" do
%span.price-breakdown{
title: "#{dyeworks_items_nc_total_for(@items_needed[:dyeworks])} NC"
}<
#{pluralize @items_needed[:dyeworks].size, "item"}
+
%span.price-breakdown{
title: dyeworks_potions_nc_summary(@items_needed[:dyeworks].size)
}<
#{pluralize @items_needed[:dyeworks].size, "potion"}
+
%span.price-breakdown{
title: "At least #{pluralize @items_needed[:dyeworks].size, 'potion'}, " +
"average " +
"#{dyeworks_average_num_potions_for @items_needed[:dyeworks]}, " +
"could be more. 125 NC per potion, but cheaper in bundles."
}
?? potions
(~#{dyeworks_estimated_potions_cost_for @items_needed[:dyeworks]} NC)
%td.actions-cell
- if @items_needed[:dyeworks].present?
%button{onclick: "alert('Todo!')"}
%button{
onclick: "alert('Todo!')",
data: {"action-kind": "bulk-nc-mall"},
}
= cart_icon alt: ""
Buy all in NC Mall
%tbody
@ -70,36 +80,40 @@
- base_item = item.dyeworks_base_item
- content_for :subtitle, flush: true do
= link_to base_item.name, base_item, target: "_blank"
+ 1 potion
- if item.dyeworks_permanent?
%span.dyeworks-timeframe{
title: "This recipe is NOT currently scheduled to be removed " +
"from Dyeworks. It might not stay forever, but it's also " +
"not part of a known limited-time event, like most " +
"Dyeworks items are. (Thanks Owls team!)"
}
(Permanent)
- elsif item.dyeworks_limited_final_date.present?
%span.dyeworks-timeframe{
title: "This recipe is part of a limited-time Dyeworks " +
"event. The last day you can dye this is " +
"#{item.dyeworks_limited_final_date.to_fs(:month_and_day)}. " +
"(Thanks Owls team!)"
}
(Thru #{item.dyeworks_limited_final_date.to_fs(:month_and_day)})
- elsif item.dyeworks_limited?
%span.dyeworks-timeframe{
title: "This recipe is part of a limited-time Dyeworks " +
"event, and is scheduled to be removed from the NC Mall " +
"soon. (Thanks Owls team!)"
}
(Limited-time)
= link_to base_item, target: "_blank" do
#{probability item.dyeworks_odds} chance
- if item.dyeworks_permanent?
%span.dyeworks-timeframe{
title: "This recipe is NOT currently scheduled to be removed " +
"from Dyeworks. It might not stay forever, but it's also " +
"not part of a known limited-time event, like most " +
"Dyeworks items are. (Thanks Owls team!)"
}
(Always available)
- elsif item.dyeworks_limited_final_date.present?
%span.dyeworks-timeframe{
title: "This recipe is part of a limited-time Dyeworks " +
"event. The last day you can dye this is " +
"#{item.dyeworks_limited_final_date.to_fs(:long)}. " +
"(Thanks Owls team!)"
}
(Limited-time: #{item.dyeworks_limited_final_date.to_fs(:month_and_day)})
- elsif item.dyeworks_limited?
%span.dyeworks-timeframe{
title: "This recipe is part of a limited-time Dyeworks " +
"event, and is scheduled to be removed from the NC Mall " +
"soon. (Thanks Owls team!)"
}
(Limited-time)
%button{onclick: "alert('Todo!')"}
= cart_icon alt: ""
Buy base (#{item.dyeworks_base_item.current_nc_price} NC)
= button_link_to "NC Trades",
item_trades_path(item, type: "offering"),
target: "_blank", icon: search_icon
- if @items[:np].present?
%h2 Neopoint items
:markdown
@ -112,11 +126,11 @@
[tp]: https://www.neopets.com/island/tradingpost.phtml?type=browse
[ag]: https://www.neopets.com/genie.phtml
%table.item-list
%table.item-list{"data-complexity": complexity_for(@items[:np])}
%thead
%tr
%td
%th{colspan: 2}
%th.name-cell{colspan: 2}
Total: #{pluralize @items_needed[:np].size, "item"}
%tbody
- @items[:np].each do |item|
@ -139,6 +153,7 @@
%table.item-list{
"data-group-type": "bundle",
"data-group-owned": items.all?(&:owned?),
"data-complexity": complexity_for(items),
}
%thead
%tr
@ -195,10 +210,10 @@
[owls]: https://www.neopets.com/~owls
%table.item-list
%table.item-list{"data-complexity": complexity_for(@items[:other_nc])}
%thead
%td
%th{colspan: 2}
%th.name-cell{colspan: 2}
Total: #{pluralize @items_needed[:other_nc].size, "item"}
%tbody
- @items[:other_nc].each do |item|