Compare commits

...

12 commits

Author SHA1 Message Date
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
5 changed files with 123 additions and 94 deletions

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

@ -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

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

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

@ -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|