Skip to content

Commit 36186ff

Browse files
committed
Add fishing area to Fish ranking dimensions
1 parent 40b0bba commit 36186ff

16 files changed

Lines changed: 357 additions & 160 deletions

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Source: github:metyatech/agent-rules@HEAD/rules/global/command-execution.md
8989
- Do not assume agent platform capabilities beyond what is available; fail explicitly when unavailable.
9090
- When building a CLI, follow standard conventions: --help/-h, --version/-V, stdin/stdout piping, --json output, --dry-run for mutations, deterministic exit codes, and JSON Schema config validation.
9191
- Treat `agent-browser` sessions as temporary resources: close them immediately when they are no longer needed, and before concluding a task verify that no sessions spawned for that task remain open.
92+
- When launching `agent-browser` on Windows, do not rely on the implicit default session; always provide an explicit `--session <name>` or set `AGENT_BROWSER_SESSION` to a chosen session name before use, and if a localhost bind/access error occurs (for example `os error 10013`), retry with a different explicit session name instead of reusing the blocked default session, then report the working session name or the remaining failure.
9293
- For federated identity flows (Google, Apple, Microsoft, GitHub, etc.), when automation-launched browser contexts are blocked, degraded, or risky, hand off only the IdP step to a real browser/session via CDP or explicit user interaction, then resume automation after the redirect; do not try to bypass provider anti-automation or embedded-browser restrictions.
9394
## Codex-only PowerShell safety
9495
- `Remove-Item` (aliases: `rm`, `ri`, `del`, `erase`) ↁEUse: `if ([IO.File]::Exists($p)) { [IO.File]::SetAttributes($p,[IO.FileAttributes]::Normal); [IO.File]::Delete($p) }`

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project are documented here.
44

55
## [Unreleased]
66

7+
## [0.12.71] - 2026-03-16
8+
9+
### Changed
10+
11+
- **Fishing Area is now a first-class ranking dimension**: ranking mode can treat `釣り場` like `Rod / Line / Bobber / Enchant`, so you can rank areas directly or include area alongside gear in combination searches instead of hiding area choice inside setup only.
12+
- **The ranking and optimizer internals now carry area identity all the way through the result rows**: subset searches, multi-slot rankings, and result tables keep `areaId` / area name attached to each candidate, which lets the UI show area-aware rankings without special-case postprocessing.
13+
714
## [0.12.70] - 2026-03-16
815

916
### Changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ npm run dev
8888
- **今の装備を見たまま候補を比べられる**: 候補パネルを開いても左の現在装備表は隠れないので、ほかの装備や合計ステータスを見失わずに選び直せます。
8989
- **入力が先、結果は後**: 先に場所や装備を決めてから候補や結果を見る流れにしているため、初見でも手順を追いやすくしています。
9090
- **いまの目的を常に表示**: `基準装備 / 釣り場 / 時間帯 / 天気 / この条件の目安` をコンテキスト欄に固定し、何を見ているかを見失いにくくしています。
91-
- **ランキングはスロット順位として読める**: 選んだ欄だけを入れ替え、ほかの欄は今の装備のまま固定して順位を出します。Fishing Area を自動のまま使うと、同じ装備でも釣り場ごとに別行で順位が出ます
91+
- **ランキングは欄ごとの順位として読める**: `Rod / Line / Bobber / Enchant / 釣り場` のうち選んだ欄だけを入れ替え、ほかの欄は今の装備のまま固定して順位を出します。釣り場を選べば「同じ装備でどの場所が強いか」、Rod と釣り場を同時に選べば「Rod と釣り場をセットで替えたときの順位」をそのまま見られます
9292
- **次に試す候補をすぐ出す**: `今の装備から次を探す` では、その目的に合わせた最初の候補と期待値/時間の差をすぐ提示します。
9393
- **細かい設定は後ろへ**: `詳細調整` に推定前提やレア度上書きをまとめ、必要になるまで初期表示から退かせています。
9494

@@ -109,12 +109,12 @@ npm run dev
109109
- `入手場所` は 1 つだけでなく、`さらに絞る` から複数の場所をまとめて残せます。
110110
- まず 1 か所だけ変えて比べたいときに使います。
111111

112-
### 組み合わせ最適化(複数スロット
112+
### 組み合わせ最適化(複数欄
113113

114-
- `Rod / Line / Bobber / Enchant` の4つを複数選択すると、変えるスロットを自由に組み合わせて探せます(最低 1 つ)。
114+
- ランキングでは `Rod / Line / Bobber / Enchant / 釣り場` を複数選択すると、変える欄を自由に組み合わせて探せます(最低 1 つ)。
115115
- **全スロット選択**(デフォルト)は全装備の組み合わせ候補(理論上 15 × 8 × 8 × 43 = **41,280 通り**)から、能力的に完全に劣る Rod / Line / Bobber を先に外した exact search を行い、期待値/時間の上位候補を一覧表示します。
116-
- **部分サブセット**(例: Rod + Enchant)を選ぶと、固定スロットはいまの装備のまま、変えるスロットだけを組み合わせて探します
117-
- 変えるスロットは画面の `Rod / Line / Bobber / Enchant` ボタンを複数選択して切り替えます。現在の選択は説明カードで常に確認できます。
116+
- **部分サブセット**(例: Rod + Enchant、Rod + 釣り場)を選ぶと、固定欄はいまの装備 / 条件のまま、選んだ欄だけを組み合わせて探します
117+
- 変える欄はランキングの `Rod / Line / Bobber / Enchant / 釣り場` ボタンを複数選択して切り替えます。現在の選択は説明カードで常に確認できます。
118118
- 上位 K 候補への枝刈りなし。除外された装備はありません。
119119
- Luck スケーリング・時間モデル・Big Catch weight percentile などの experimental 近似モデルを前提とした推定値です。
120120
- 検索中にスピナーとプログレスバーで進捗を表示し、途中経過の上位候補をリアルタイムで確認できます(「速報」として表示)。
@@ -144,7 +144,7 @@ npm run dev
144144

145145
5. **次に、比べる欄を選ぶ**: `Rod / Line / Bobber / Enchant` の4つを押して、1欄だけ見るか、複数スロットを同時に探すかを切り替えます。
146146

147-
6. **ランキングは、見たい欄を先に選ぶ**: `2. 順位を見る欄を選ぶ``Rod / Line / Bobber / Enchant` を1つ選ぶと個別順位、複数選ぶと組み合わせ最適化になります。Fishing Area が自動なら、同じ装備でも `Coconut Bay` / `Tangleyew Marshes` のように釣り場ごとに別行で並びます
147+
6. **ランキングは、見たい欄を先に選ぶ**: `2. 順位を見る欄を選ぶ``Rod / Line / Bobber / Enchant / 釣り場` を1つ選ぶと個別順位、複数選ぶとその欄だけを同時に入れ替えた順位になります。釣り場だけを選べば「この装備でどの場所が強いか」、Rod と釣り場を選べば「Rod と場所を同時に替えたときの順位」をそのまま見られます
148148

149149
7. **`3. 順位を見る` でそのまま比較する**: 先に順位を見て、必要ならあとで `4. 必要なら条件を変える` を開きます。Fishing Area を固定したときはその釣り場だけ、Fishing Area を自動にしたときは釣り場ごとの順位をそのまま確認できます。
150150

e2e/smoke.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,13 @@ test('calculator updates summary cards and fish list when loadout and filters ch
135135
})
136136
.click();
137137
expect(await page.getByTestId('slot-picker-panel').count()).toBe(0);
138-
await page.getByLabel('釣り場').selectOption('open-sea');
139-
await page.getByLabel('時間帯').selectOption('night');
140-
await page.getByLabel('天気').selectOption('rainy');
141-
await page.getByLabel('投げてから着水まで (sec)').fill('1.4');
142-
await page.getByLabel('`!` が出てから反応するまで (sec)').fill('0.25');
143-
await page.getByLabel('ミス率').fill('0.12');
138+
const setupSection = page.getByTestId('setup-section');
139+
await setupSection.getByLabel('釣り場').selectOption('open-sea');
140+
await setupSection.getByLabel('時間帯').selectOption('night');
141+
await setupSection.getByLabel('天気').selectOption('rainy');
142+
await setupSection.getByLabel('投げてから着水まで (sec)').fill('1.4');
143+
await setupSection.getByLabel('`!` が出てから反応するまで (sec)').fill('0.25');
144+
await setupSection.getByLabel('ミス率').fill('0.12');
144145

145146
await page.getByRole('button', { name: // }).click();
146147
await page.getByTestId('compare-slot-button-enchant').click();

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vrchat-fish-guide",
3-
"version": "0.12.70",
3+
"version": "0.12.71",
44
"private": true,
55
"description": "Unofficial community guide and probability calculator for the VRChat world Fish!",
66
"homepage": "https://metyatech.github.io/vrchat-fish-guide/",

src/app/calculator/CalculatorPageClient.tsx

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
formatCurrency,
2424
getDefaultParams,
2525
} from '@/lib/calculator';
26-
import { rankAllSlots, RankSlot } from '@/lib/ranking';
26+
import { rankAllSlots, RankDimension, RankSlot } from '@/lib/ranking';
2727
import {
2828
createBuildFrom,
2929
createDefaultBuild,
@@ -36,14 +36,16 @@ import {
3636
} from '@/lib/url-state';
3737
import { BuildConfig, CalculatorParams, DistributionResult, Rarity } from '@/types';
3838

39-
const SLOT_LABELS: Record<RankSlot, string> = {
39+
const SLOT_LABELS: Record<RankDimension, string> = {
4040
rod: 'Rod',
4141
line: 'Line',
4242
bobber: 'Bobber',
4343
enchant: 'Enchant',
44+
area: '釣り場',
4445
};
4546

46-
const ALL_SLOTS: RankSlot[] = ['rod', 'line', 'bobber', 'enchant'];
47+
const EQUIPMENT_SLOTS: RankSlot[] = ['rod', 'line', 'bobber', 'enchant'];
48+
const RANKING_SLOTS: RankDimension[] = ['rod', 'line', 'bobber', 'enchant', 'area'];
4749

4850
const GOAL_HELPER_COPY: Record<CalculatorGoal, string> = {
4951
ranking: '見たい欄を選ぶと、その候補が強い順に並びます。',
@@ -132,7 +134,7 @@ export function CalculatorPageClient() {
132134
const [activeId, setActiveId] = useState(initialActiveId);
133135
const [chartMode, setChartMode] = useState<'per-catch' | 'per-hour'>('per-hour');
134136
const [urlRestoreError, setUrlRestoreError] = useState<string | undefined>(initialUrlError);
135-
const [selectedSlots, setSelectedSlots] = useState<RankSlot[]>(['rod']);
137+
const [selectedSlots, setSelectedSlots] = useState<RankDimension[]>(['rod']);
136138
const [goalView, setGoalView] = useState<CalculatorGoal>(() =>
137139
initialBuilds.length > 1 ? 'compare' : 'ranking',
138140
);
@@ -156,6 +158,12 @@ export function CalculatorPageClient() {
156158
// Ref to the setup section (step 2); used to scroll into view after goal selection.
157159
const nextSectionRef = useRef<HTMLElement | null>(null);
158160
const handleGoalChange = useCallback((goal: CalculatorGoal) => {
161+
if (goal !== 'ranking') {
162+
setSelectedSlots((prev) => {
163+
const next = EQUIPMENT_SLOTS.filter((slot) => prev.includes(slot));
164+
return next.length > 0 ? next : ['rod'];
165+
});
166+
}
159167
setGoalView(goal);
160168
requestAnimationFrame(() => {
161169
nextSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
@@ -195,7 +203,9 @@ export function CalculatorPageClient() {
195203
// ── Build operations ───────────────────────────────────────────────────────
196204

197205
const activeBuild = builds.find((b) => b.id === activeId) ?? builds[0];
198-
const orderedSelectedSlots = ALL_SLOTS.filter((slot) => selectedSlots.includes(slot));
206+
const isRankingGoal = goalView === 'ranking';
207+
const availableRankSlots = isRankingGoal ? RANKING_SLOTS : EQUIPMENT_SLOTS;
208+
const orderedSelectedSlots = availableRankSlots.filter((slot) => selectedSlots.includes(slot));
199209
const isSingleSlotSelection = orderedSelectedSlots.length === 1;
200210
const primarySlot = orderedSelectedSlots[0] ?? 'rod';
201211

@@ -245,7 +255,7 @@ export function CalculatorPageClient() {
245255
}, []);
246256

247257
const handleCreateSlotComparison = useCallback(
248-
(slot: RankSlot, itemId: string, itemName: string) => {
258+
(slot: RankDimension, itemId: string, itemName: string) => {
249259
if (!activeBuild) return;
250260

251261
const currentItemId =
@@ -255,18 +265,24 @@ export function CalculatorPageClient() {
255265
? activeBuild.params.loadout.lineId
256266
: slot === 'bobber'
257267
? activeBuild.params.loadout.bobberId
258-
: activeBuild.params.loadout.enchantId;
268+
: slot === 'enchant'
269+
? activeBuild.params.loadout.enchantId
270+
: activeBuild.params.areaId;
259271

260272
if (itemId === currentItemId) return;
261273

262274
const nextBuild = createBuildFrom(activeBuild, builds.length);
263275
nextBuild.name = `${itemName} を試す`;
264276
nextBuild.params = {
265277
...nextBuild.params,
266-
loadout: {
267-
...nextBuild.params.loadout,
268-
[`${slot}Id`]: itemId,
269-
},
278+
...(slot === 'area'
279+
? { areaId: itemId }
280+
: {
281+
loadout: {
282+
...nextBuild.params.loadout,
283+
[`${slot}Id`]: itemId,
284+
},
285+
}),
270286
};
271287

272288
setBuilds((prev) => [...prev, nextBuild]);
@@ -292,7 +308,9 @@ export function CalculatorPageClient() {
292308
? activeBuild.params.loadout.lineId
293309
: primarySlot === 'bobber'
294310
? activeBuild.params.loadout.bobberId
295-
: activeBuild.params.loadout.enchantId;
311+
: primarySlot === 'enchant'
312+
? activeBuild.params.loadout.enchantId
313+
: activeBuild.params.areaId;
296314

297315
if (bestEntry.item.id === currentItemId) return;
298316

@@ -309,7 +327,7 @@ export function CalculatorPageClient() {
309327
if (!activeBuild) return;
310328
const nextBuild = createBuildFrom(activeBuild, builds.length);
311329
const varyingLabel =
312-
orderedSelectedSlots.length === ALL_SLOTS.length
330+
orderedSelectedSlots.length === EQUIPMENT_SLOTS.length
313331
? '4スロット比較'
314332
: orderedSelectedSlots.map((slot) => SLOT_LABELS[slot]).join('+');
315333
nextBuild.name = rank === 0 ? `${varyingLabel}のおすすめ` : `${varyingLabel} ${rank + 1}`;
@@ -384,10 +402,9 @@ export function CalculatorPageClient() {
384402
const selectionThemeKey: CompareTarget = isSingleSlotSelection ? primarySlot : 'full-build';
385403
const compareTargetTheme = SLOT_THEME[selectionThemeKey];
386404
const selectedSlotsLabel = orderedSelectedSlots.map((slot) => SLOT_LABELS[slot]).join(' + ');
387-
const fixedSlots = ALL_SLOTS.filter((slot) => !orderedSelectedSlots.includes(slot));
405+
const fixedSlots = availableRankSlots.filter((slot) => !orderedSelectedSlots.includes(slot));
388406
const fixedSlotsLabel =
389407
fixedSlots.length > 0 ? fixedSlots.map((slot) => SLOT_LABELS[slot]).join(' / ') : 'なし';
390-
const isRankingGoal = goalView === 'ranking';
391408
const isUpgradeGoal = goalView === 'upgrade';
392409
const showSelectionTools = isRankingGoal || isUpgradeGoal;
393410
const selectionHelperText = isSingleSlotSelection
@@ -687,7 +704,7 @@ export function CalculatorPageClient() {
687704
</div>
688705

689706
<div data-testid="compare-slot-selector" className="mb-4 flex flex-wrap gap-2">
690-
{ALL_SLOTS.map((slot) => {
707+
{availableRankSlots.map((slot) => {
691708
const isSelected = orderedSelectedSlots.includes(slot);
692709
const theme = SLOT_THEME[slot];
693710
return (
@@ -701,11 +718,11 @@ export function CalculatorPageClient() {
701718
? prev.filter((candidate) => candidate !== slot)
702719
: prev
703720
: [...prev, slot];
704-
return ALL_SLOTS.filter((candidate) => next.includes(candidate));
721+
return availableRankSlots.filter((candidate) => next.includes(candidate));
705722
})
706723
}
707724
aria-pressed={isSelected}
708-
aria-label={`${SLOT_LABELS[slot]} ${isSelected ? '選択中' : '未選択'}`}
725+
aria-label={`順位に含める ${SLOT_LABELS[slot]} ${isSelected ? '選択中' : '未選択'}`}
709726
data-state={isSelected ? 'selected' : 'idle'}
710727
data-testid={`compare-slot-button-${slot}`}
711728
className={`flex min-w-[9.5rem] items-center justify-between gap-3 rounded-2xl border px-4 py-2.5 text-left text-sm font-semibold transition-all duration-150 hover:-translate-y-0.5 active:translate-y-0 ${
@@ -860,22 +877,16 @@ export function CalculatorPageClient() {
860877
alwaysOpen={true}
861878
onPickItem={isRankingGoal ? undefined : handleCreateSlotComparison}
862879
showPickActions={!isRankingGoal}
863-
includeAreaBreakdown={
864-
isRankingGoal && activeBuild.params.areaId === BEST_AREA_ID
865-
}
880+
includeAreaBreakdown={false}
866881
title={isRankingGoal ? undefined : `${SLOT_LABELS[primarySlot]} の候補一覧`}
867882
description={
868883
isRankingGoal
869-
? activeBuild.params.areaId === BEST_AREA_ID
870-
? `${SLOT_LABELS[primarySlot]} だけを入れ替えた順位を、釣り場ごとに並べています。${fixedSlotsLabel} は今の装備のまま固定です。`
871-
: `${SLOT_LABELS[primarySlot]} だけを入れ替えた順位です。${fixedSlotsLabel} は今の装備のまま固定です。`
884+
? `${SLOT_LABELS[primarySlot]} だけを入れ替えた順位です。${fixedSlotsLabel} は今の装備のまま固定です。`
872885
: undefined
873886
}
874887
helperText={
875888
isRankingGoal
876-
? activeBuild.params.areaId === BEST_AREA_ID
877-
? `${SLOT_LABELS[primarySlot]} だけを変えた結果を、釣り場ごとに見ます。`
878-
: `${SLOT_LABELS[primarySlot]} だけを変えた結果です。`
889+
? `${SLOT_LABELS[primarySlot]} だけを変えた結果です。`
879890
: undefined
880891
}
881892
/>

0 commit comments

Comments
 (0)