维护提醒

BWIKI 全站将于 9 月 3 日(全天)进行维护,期间无法编辑任何页面或发布新的评论。

全站通知:

讨论:钓鱼

来自星露谷物语维基
跳到导航 跳到搜索
本页面用于讨论 钓鱼
  • 将新留言放在已有留言的下方。
  • 在你的留言的末尾输入四个波浪符(~~~~)来签名并标注时间。
  • 保持礼貌。
  • 善意对待他人。
  • 不要删除已有讨论。

这个讨论主题极其硬核,建议配合着源码和解包后的json文件一一对照,目标是让所有有心了解的人都能以人类的语言理解完整的钓鱼判断逻辑(个人能力有限希望有人帮忙补充)

补充:我不介意后来者在我原文上进行修改,相反的,我更希望一个主题就能把逻辑讲述清楚;或者后来者为其进行分主题归类方便阅读

钓鱼流程步骤

以下是玩家进行一次抛竿钓鱼所经历的主要方法及其代表的动作:

  1. beginUsing():开始使用鱼竿
  2. setTimingCastAnimation():设置蓄力动画
  3. tickUpdate():游戏帧更新;持续更新鱼竿状态
    • 3.1 蓄力阶段:当玩家松开按键时,触发 startCasting()
  4. startCasting():触发抛竿
  5. doStartCasting():执行抛竿准备
  6. tickUpdate():续,浮标抛出;动画结束后调用 castingEndFunction()
  7. castingEndFunction():浮标落水;调用 DoFunction()
  8. DoFunction():处理浮标落水后的行为;
    • 检查钓鱼条件:通过 GameLocation.canFishHere()GameLocation.isTileFishable() 方法
    • 计算咬钩时间:通过 calculateTimeUntilFishingBite() 方法
  9. calculateTimeUntilFishingBite():计算咬钩时间
  10. tickUpdate():续,等待鱼咬钩;
    • 10.1 咬钩计时:鱼咬钩后,如果装备了自动钓鱼附魔,直接触发 DoFunction();否则,显示咬钩提示并设置 timeUntilFishingNibbleDone
    • 10.2 轻咬阶段:鱼脱钩后,重新计算下一次咬钩时间
  11. DoFunction():续,鱼咬钩后玩家收竿;
    • 获取鱼的类型:通过 GameLocation.getFish() 方法
    • 如果钓到的不是垃圾且玩家尚未进入小游戏,调用 startMinigameEndFunction(),进入钓鱼小游戏
    • 如果是垃圾物品或来自鱼塘的鱼,则直接调用 pullFishFromWater(),跳过小游戏
  12. startMinigameEndFunction():开始钓鱼小游戏;通过 StardewValley.Menus.BobberBar 的构造方法,创建并激活 BobberBar 菜单
  13. tickUpdate():续,钓鱼小游戏进行中
  14. pullFishFromWater():从水中拉起鱼
  15. doPullFishFromWater():处理拉起鱼的逻辑;动画结束后调用 playerCaughtFishEndFunction()
  16. playerCaughtFishEndFunction():鱼被拉到玩家手中;更新玩家钓鱼统计数据(涉及 Farmer.caughtFish() 方法)
  17. tickUpdate():续,玩家持有鱼;如果玩家点击屏幕,调用 doneHoldingFish()
  18. doneHoldingFish():处理钓鱼结果;
    • 18.1 正常钓鱼:如果钓到的不是钓鱼宝箱且没有鳟鱼大赛的黄金标签,调用 CreateFish() 创建鱼物品,尝试添加到背包,如果背包已满,则打开 StardewValley.Menus.ItemGrabMenu 让玩家选择如何处理物品,最后调用 doneFishing()
    • 18.2 钓鱼宝箱或黄金标签:添加钓鱼宝箱动画,动画结束后调用 openChestEndFunction()
  19. openChestEndFunction():打开钓鱼宝箱菜单;动画结束后调用 openTreasureMenuEndFunction()
  20. openTreasureMenuEndFunction():处理钓鱼宝箱物品;创建并激活 StardewValley.Menus.ItemGrabMenu,让玩家拾取钓鱼宝箱,最后调用 doneFishing()
  21. doneFishing():结束钓鱼
  22. doDoneFishing():重置钓鱼状态

蓄力阶段

玩家进入蓄力状态后,游戏会进入 tickUpdate() 控制蓄力条的增长并进行抛竿距离的计算。

蓄力

  • 当玩家开始蓄力时,会将 castingPower 从 0 开始,以每帧 0.016 的精确速度逐渐递增
  • 经过 63 帧达到 1.0(即满蓄力)时,会以相同速度开始递减,直到回到 0,然后再次反转递增,如此循环。
  • 玩家需要在蓄力条达到理想位置时松开按键,以决定最终的抛竿力度。在 0.99 以上(第 62 和 63 两帧)会显示满力抛竿

抛竿距离

  • 具体计算抛竿距离的逻辑在 castingChosenCountdown 结束后触发。
  • 对于水平抛竿(玩家面向左右)距离计算公式为: castingPower × (getAddedDistance(who) + 4) × 64 - 8(px)。
  • 对于垂直抛竿(玩家面向上下)距离计算公式为: castingPower × (getAddedDistance(who) + 3) × 64(px)。
  • 两种情况计算结果最小取 128 像素(即 2 格)
  • castingPower 介于 0.0 到 1.0 之间,会将实际抛竿距离控制为满力抛竿距离的对应比例。
  • getAddedDistance方法根据玩家的钓鱼等级提供额外的距离加成:
    • 1-3 级:1 格(64 px)
    • 4-7 级:2 格(128 px)
    • 8-14 级:3 格(192 px)
    • 15+ 级:4 格(256 px)
  • 这意味着高钓鱼等级的玩家即使没有完美蓄力,也能抛出更远的距离,或者在完美蓄力时达到更远的极限距离。

离岸距离

浮标落水后,会经过distanceToLand()方法计算离岸距离

离岸距离计算

系统会以浮标中心点所处格为中心,设置一个初始 3×3 的检测框并进行以下操作:

  1. 检查检测框内有没有非水地块,若有则设置distance = ⌊检测框宽度/2⌋
  2. 检测框扩大一圈*(3×3 -> 5×5 -> 7×7 -> 9×9 -> 11×11 -> 13×13)
  3. 若没发现非水地块或者检测框不超过 11×11,回到第一步
  4. 如果检测框超过 11×11,则设置distance = 6
  5. 返回 distance - 1作为离岸距离

理想情况下这个方法会完整输出 0~5 作为离岸距离

但当检测框为 11×11 发现非水地块时,也就是理论上应输出离岸距离 4 时

  1. 设置distance = ⌊11/2⌋ = 5
  2. 检测框扩大一圈* 11×11 -> 13×13
  3. 发现陆地且检测框超过 11×11,不回到第一步
  4. 检测框(13×13)超过 11×11,设置distance = 6
  5. 返回 6 - 1 = 5作为离岸距离

这就是星露谷没有离岸距离 4 的原因

等待阶段

浮标落水后,鱼的咬钩时机由 calculateTimeUntilFishingBite() 计算得出。

等待时间

初始等待时间

  • 如果是在鱼塘垂钓则固定等待 1000 ms,无需进行下面的判断。
  • 系统会在 [600 ms, 最大等待时间] 范围内随机生成一个等待时间。
  • 最大等待时间基础值为 30,000 ms,会受到以下因素影响:
  • 玩家钓鱼等级:每级减少 250 ms。
  • 渔具:每装备一个 精装旋式鱼饵 减少 10,000 ms;旋式鱼饵 减少 5,000 ms。

等待时间调整

  • 首次咬钩:如果是本次钓鱼的第一次咬钩,等待时间 × 0.75;未及时收杆进行二次等待时不会应用。
  • 鱼饵:只要装备了鱼饵,等待时间 × 0.5
    • 万能鱼饵 或 挑战鱼饵:在装备鱼饵的基础上,等待时间 × 0.75
    • 高级鱼饵:在装备鱼饵的基础上,等待时间 × 0.66
  • 气泡点:如果浮标落在气泡点范围内。
    • 普通气泡点:时间 ÷ 4
    • 鱼的狂热气泡:时间 ÷ 2

经过所有计算后,最终的等待时间不会低于 500 ms

值得注意的是这些数据由于游戏时间精度问题还需经过公式转化为现实时间:等待时间/960

收杆

  • 鱼咬钩后玩家有固定的 800 ms (0.833 s) 时间来点击收杆。
  • 若鱼竿拥有自动上钩附魔,系统会在鱼咬钩时自动触发收杆,无需玩家手动操作。

获取物品种类

当玩家成功收杆后,游戏会通过一系列方法调用来最终确定玩家钓到的物品。这个过程从高层级的 getFish() 方法开始,深入到 GetFishFromLocationData() 进行数据筛选,最后由 CheckGenericFishRequirements() 完成最终的咬钩判定。本章将逐一深入解读这些方法的运作机制。

钓鱼入口方法-getFish()

这是玩家收杆后游戏调用的第一个核心方法,它负责调度和处理各种钓鱼的特殊情况,并最终返回一个物品。

值得注意的是传入该方法的“离岸距离”参数会因为浮标处于气泡点范围内额外+1。也就是说在获取鱼的种类时可以存在离岸4,离岸6的鱼。

执行顺序

getFish() 的执行逻辑遵循一个严格的优先级顺序,一旦在某一步骤成功获取到物品,流程就会立即终止并返回该物品。

  1. 地点重写检查:首先,检查当前地点(GameLocation)是否对 getFish() 方法有自己的特殊实现(Override)。如果存在,则优先执行该地点的专属逻辑。
  2. 特殊水域判定
    • 鱼塘 (FishPond):检查浮标 (bobberTile) 是否落在了玩家农场的鱼塘建筑范围内。如果是,则直接调用鱼塘的 CatchFish() 方法来获取物品。
    • 鱼群狂热 (fishFrenzy):检查当前地点是否存在鱼群狂热活动,并且浮标是否落在了指定的气泡点附近(距离小于等于 2 格)。如果是,则直接生成该活动对应的鱼 (fishFrenzyFish)。
  3. 调用核心数据处理器:如果以上特殊情况都不满足,方法将调用 GameLocation.GetFishFromLocationData()。这是绝大多数情况下生成鱼类和普通物品的核心处理方法。
  4. 默认回退:如果在 GetFishFromLocationData() 中没有找到任何符合条件的物品(返回 null),此方法会返回一个默认物品:太阳鱼 ((O)168)

地点重写详情

以下是原版游戏中重写了 getFish 方法的几个特殊地点及其逻辑:

  • 农场 (Farm):原版农场地图不会重写。但 Mod 作者可以通过为地图添加 FarmFishLocationOverride 属性,让其农场地图模拟在其他任何地点钓鱼。
  • 姜岛 (Ginger Island):在姜岛任意区域垂钓,都有 15%的概率钓到一个金核桃,直到通过此方式获得满 5 个为止。
  • 姜岛东南岸 (Island Southeast):在该区域的星形潮汐池中第一次垂钓,必定会获得一个金核桃。
  • 矿井 (MineShaft): 在矿井的特定楼层钓鱼,其特有鱼的出现概率由一个“概率乘数”决定。
    • 概率乘数公式概率乘数 = 1 + 0.4 × 玩家钓鱼等级 + 离岸距离 × 0.1
    • 如果装备了珍稀诱钩 (Treasure Hunter),乘数额外 +5
    • 如果使用了对应鱼的针对性鱼饵,乘数额外 +10。 不同楼层的计算如下:
    • 20 层石鱼概率 = 0.02 + 0.01 × 概率乘数
    • 60 层冰柱鱼概率 = 0.015 + 0.009 × 概率乘数
    • 100 层: 此层仅能钓到以下三类物品,不会再进入 GetFishFromLocationData()
      • 岩浆鳗鱼概率 = 0.01 + 0.08 × 概率乘数
      • 洞穴凝胶概率 = (1 - 岩浆鳗鱼概率) × 0.05 + 玩家幸运等级 × 0.05
      • 垃圾概率 = 1 - 岩浆鳗鱼概率 - 洞穴凝胶概率 (垃圾包含概率均分的 6 种物品)
    • 矿洞鱼品质概率:这三种特有鱼的品质判定也使用特殊公式:
      • 金星概率 = 玩家钓鱼等级 / 50 + 玩家幸运等级 / 100
      • 银星概率 = (1 - 金星概率) × (玩家钓鱼等级 / 10)
      • 普通概率 = 1 - 金星概率 - 银星概率
  • 火车站 (Railroad):阅读过秘密纸条 #25 后(即阿比盖尔丢失了妈妈的项链),在温泉门口钓鱼会获取“华丽的项链”。

核心数据筛选方法-GetFishFromLocationData()

此方法是决定能钓到何种物品的核心。它负责从游戏数据中读取潜在物品列表,并根据一系列复杂的规则进行筛选和随机判定。

1. 数据准备与排序

  • 加载数据:首先,方法会加载并合并 Data/Locations.xnb 文件中 Default(全局默认物品)和当前地点的 Fish 列表,形成一个完整的潜在物品列表 possibleFish
  • 排序规则:列表会根据每个物品的 Precedence 属性值进行升序排列。如果多个物品的 Precedence 值相同,它们之间将随机排序。这个随机性是导致精确计算特定鱼类钓获率极为困难的核心原因。

2. 存活筛选流程

这部分逻辑由于历史遗留问题,我不会按照源码顺序解读而是在保证最终结果不变的前提下整理出更好理解的解读顺序。

程序会严格按照排好的顺序遍历 possibleFish 列表,对每个物品进行一连串条件检查。

  • 基础条件筛选
    • CanBeInherited:当通过占位符(见下文)继承时,检查该物品是否允许被继承。原版中有 25 种物品(主要是鱼王和特殊物品)不可继承。
    • FishAreaId:检查玩家所在的钓鱼分区是否与物品要求的分区匹配。
    • Season:如果未使用魔法鱼饵,检查当前季节是否与物品要求的季节匹配。
    • PlayerPosition:检查玩家的站立位置是否在物品要求的矩形区域内。
    • BobberPosition:检查浮标的落点位置是否在物品要求的矩形区域内。
    • MinFishingLevel:检查玩家的钓鱼等级是否达到物品的最低要求。
    • MinDistanceFromShore, MaxDistanceFromShore:检查浮标与最近陆地的距离是否在要求的范围内。
    • RequireMagicBait:检查是否满足必须使用魔法鱼饵的要求。
    • Condition:检查是否满足更为复杂的、通过游戏状态查询(GameStateQuery)定义的特殊条件。
    • CatchLimit:检查玩家已捕获该物品的数量是否超过了上限。
  • 存活概率判定 (SpawnFishData.GetChance()): 通过筛选后,物品需经过一次“存活”判定。系统根据以下流程计算出一个存活概率,并进行随机投骰。
    1. 读取 Locations.xnb 中该物品的 Chance 作为基础存活概率
    2. 如果装备了珍稀诱钩 (CuriosityLure) 且该物品的 CuriosityLureBuff > -1,则:存活概率 += CuriosityLureBuff
    3. 如果该物品的 ApplyDailyLucktrue,则:存活概率 += 当日每日运气值
    4. 如果该物品的 chanceModifiers 不为 null,则根据其内容对概率进行加法或乘法修正。
    5. 如果装备了该物品对应的针对性鱼饵,则:存活概率 = 存活概率 × SpecificBaitMultiplier + SpecificBaitBuff
    6. 最终返回 存活概率 + ChanceBoostPerLuckLevel × 当前玩家的幸运等级

非默认属性值的物品列表:

  • CuriosityLureBuff
    • 0.1:春鱼王(两代)、鲤鱼王(两代)、水滴鱼(潜艇)、大海参(潜艇)
    • 0.07:夏鱼王(两代)、绯红鱼之子(夜市)
    • 0.05:秋鱼王(两代)、幽灵鱼(潜艇)
    • 0.205:水滴鱼、幽灵鱼、午夜鱿鱼(魔法鱼饵、沙滩)
    • 0.15:虾虎鱼(瀑布)
    • 0.02:珍珠(潜艇)
    • 0:冬鱼王(两代、无影响)
  • ApplyDailyLuck (设为 true):仅有木跃鱼。
  • SpecificBaitMultiplier / SpecificBaitBuff:仅有虾虎鱼,其乘数为 1.0,增益为 0.2
  • ChanceBoostPerLuckLevel:仅有三种凝胶 (Slime),其值为 0.05
  • 占位符解析 (LOCATION_FISH)possibleFish 列表中可能包含形如 LOCATION_FISH <地点> ... 的特殊占位符物品。当它通过存活判定后,系统会通过 ItemQueryResolver 再次递归调用 GetFishFromLocationData() 方法,从占位符指定的 <地点> 中随机选取一个符合条件的物品来替换它。这意味着一次钓鱼判定可能会嵌套另一次完整的钓鱼判定,占位符本身也会将 Default 中的垃圾物品纳入其随机池,并且被替换后的最终物品需要经过两次 CheckGenericFishRequirements() 判定。

咬钩概率判定方法-CheckGenericFishRequirements()

当一个潜在的鱼类通过了存活判定后,此方法将执行最后一步检查,决定它是否会最终咬钩。

数据解读Data/Fish.xnb中每一行都遵循"物品代码": "英文名称/难度/运动类型/最小英寸/最大英寸/最早出没时间 最晚出没时间/出没季节/出没天气/已弃用的地点概率数据/舒适距离/基础概率/距离修正系数/最小捕获等级/能否出现在教程",

1. 前置检查

  • 数据有效性:检查物品是否存在于 Data/Fish.xnb 数据中。
  • 鱼类类型:检查该鱼在数据中的“运动类型”是否为 trap(蟹笼鱼)。
  • 如果物品数据不存在或是蟹笼鱼,此方法会直接返回 true(即判定通过),除非这是玩家教程中的第一次钓鱼(此时返回 false)。

2. 特殊情况处理

  • 训练用钓竿 ((T)TrainingRod)
    • 硬性规则:检查鱼的 SpawnFishDataCanUseTrainingRod 属性。如果明确规定为 false,则判定失败。
    • 难度门槛:如果没有硬性规定,则检查鱼的垂钓难度 (difficulty)。难度值大于等于 50 的鱼无法被训练用钓竿钓起,判定失败。
  • 教程中的首次捕获 (isTutorialCatch)
    • 检查鱼的数据中能否出现在教程 (isTutorialFish) 标志。只有被标记为教程鱼的鱼才能在此次咬钩,否则判定失败。

3. 常规上钩条件

这些条件会被玩家使用魔法鱼饵 (usingMagicBait)完全忽略。

  • 时间:检查当前游戏时间 (Game1.timeOfDay) 是否在鱼类数据定义的最早出没时间最晚出没时间构成的有效时间段内。
  • 天气:检查当前地点的天气是否符合鱼的出没天气要求。
  • 最低钓鱼等级:检查玩家的钓鱼等级是否达到了鱼类数据中定义的最小捕获等级 (minFishingLevel)。

4. 核心咬钩率计算

  • 读取基础数据:从 Data/Fish.xnb 获取 基础概率 (chance)、舒适距离 (maxDepth)和 距离修正系数 (depthMultiplier)。
  • 计算舒适距离影响: 此步骤模拟了部分鱼类喜欢栖息在岸边的行为。仅当浮标的 离岸距离 小于鱼的 舒适距离 阈值时,咬钩率才会因此下降。这个惩罚值与两者之间的距离差成正比,距离岸边越近,咬钩率下降得越多。其计算方式如下: 咬钩概率 = 基础概率 - (舒适距离 − 离岸距离) × 距离修正系数 × 基础概率(舒适距离 − 离岸距离) 最大取 0)
  • 计算钓鱼等级加成: 玩家的钓鱼技能会直接提升咬钩率。其加成非常直观,每级钓鱼等级会提供固定的 0.02(即 2%)的加成,并直接累加到当前的咬钩率上。 咬钩概率 = 咬钩概率 + (玩家钓鱼等级 / 50)

5. 咬钩率修正

  • 训练用钓竿加成:若使用,咬钩概率 × 1.1
  • 概率上限:此时的 咬钩概率 不能超过 0.9 (90%)。
  • 珍稀诱钩 (CuriosityLure)
    • 仅当 咬钩概率 低于 0.25 时触发。
    • 如果鱼的CuriosityLureBuff > -1,则 咬钩概率 + CuriosityLureBuff
    • 否则,根据公式重新计算:咬钩概率 = (0.25 - 0.08) / 0.25 * 咬钩概率 + (0.25 - 0.08) / 2;
  • 针对性鱼饵 (TargetBait):若使用,咬钩概率 × 1.66
  • 每日运气:如果鱼的ApplyDailyLucktrue,则 咬钩概率 + 当日每日运气
  • 通用修正器:应用该鱼的 ChanceModifiers 进行最终调整。(原版均无调整)

最终返回物品

咬钩概率判定方法有 咬钩概率 的概率通过判定,通过两层概率检测后会将其作为最终结果让玩家捕获。

若使用了针对性鱼饵,在GetFishFromLocationData()中还会有脸黑重试机制。

  • 系统会完整的跑两遍GetFishFromLocationData(),并限制即使有物品通过两层概率检测也不立即捕获。
  • 若两次中前三条通过两层概率检测的鱼中有一次包含被针对的鱼则立即捕获该鱼,否则就正常捕获第一次第一个通过的物品。

值得注意的是 Default 中有一个优先级极低的 垃圾类别物品,正常情况下必定通过两层概率检测,由此作为 GetFishFromLocationData() 的保底物品。

但当玩家一次钓鱼小游戏都没完成过时,该垃圾物品由于不在 Data/Fish.xnb 中无法通过咬钩概率检测

因此第一杆若没被 GetFishFromLocationData() 中的其他鱼拦截到,可以在任何地区、季节、天气钓上 getFish() 方法的默认物品 太阳鱼

钓鱼小游戏

当玩家成功收杆且钓获的非垃圾物品后,游戏将调用 startMinigameEndFunction 方法。此方法是进入钓鱼小游戏的“前置处理器”,它负责计算并设定小游戏所需的一切初始参数,随后通过 StardewValley.Menus.BobberBar 的构造方法实例化并激活小游戏界面。

小游戏准备阶段-startMinigameEndFunction

此方法在小游戏 UI 渲染前执行,其核心任务是精确计算并打包好所有即将影响小游戏难度、奖励和体验的动态变量。

传说鱼判定:检查上钩的鱼是否为“传说鱼”或“二代传说鱼”。该判定会改变小游戏中鱼图标的样式(变为戴着帽子的特殊样式)和后续的难度计算。

鱼的尺寸计算:这是一个决定最终鱼品质和图标大小的浮点数(范围 0.0-1.0),其计算过程融合了多种因素:

  • 基础尺寸离岸距离 / 5
  • 等级修正* 随机整数 / 5。该随机整数的范围下限为 1 + 玩家钓鱼等级 / 2,上限为 下限6 中的较大值。这意味着钓鱼等级越高,该随机乘数的上下限也越高,从而更容易获得大尺寸的鱼。
  • 鱼饵加成: 如果使用了魔法鱼饵尺寸 * 1.2
  • 随机浮动: 最后,尺寸 * [0.9 ~ 1.1] 之间的一个随机系数,引入 ±10% 的最终随机性。
  • 最终结果:将计算出的结果严格限制在 0.01.0 之间,然后传递给小游戏界面。

宝箱出现概率计算:宝箱的出现与否由一个复杂的概率公式决定,多个因素会累加提升基础概率:

  • 基础概率15%
  • 运气加成+ 玩家幸运等级 * 0.005 + 每日运气 / 2.0
  • 装备加成
    • 磁铁鱼饵 ((O)703)+ 15%
    • 寻宝者浮漂 ((O)693): 每个 + 5%
  • 职业加成
    • 海盗+ 15%
  • 黄金宝箱:当以上概率判定成功,确定有宝箱出现后,若玩家已解锁钓鱼精通,则有 25% + 平均每日运气 的概率将普通宝箱升级为黄金宝箱。

未完成,待补充