Part of #2687, after #2709. One commit per section, in the same order. ### Failures, maps, bottles and colored nets A GM's cast fails about a quarter of the time in any water, from the fishing table's last row, and deep water is no harder than a river. Maps and bottles are 0.5% per roll from T2A on, and 1 net in 100 is colored in every era. This removes the last row, so a cast only fails its skill check. From UO:R a cast in deep water checks skill from 65 to 115, through a `Fishing.MutateResource` override. Publish 5, which "was known as the Renaissance Publish", made deep water harder: "because the difficulty level is higher, the rate of success for pulling up SoS bottles will be lower." [O3] From LBR maps and bottles come up more often and 1 net in 10 is colored, both from Publish 16, which came out in parts during LBR [O14]. | Change | Era | Source | |---|---|---| | No "nothing" row | All | #2687 found no source for it; "A prepatch, you never failed catching something at GM" [S15]; a GM succeeds every time in lakes and rivers [C1] | | Deep water: nothing below 65 skill | UO:R on | "The minimum required skill level to catch *anything* in deep water is 65." [S5–S7] | | Deep water: 10% at 70, 70% at GM | UO:R on | "Even at 70, it is only about a 10% chance of success in deep water." [S14]; 29% of casts fail before Publish 16 and 30% after [F1, F2]; 70% success at 100 offshore [C1] | | Maps (skill − 80) / 2400, bottles (skill − 80) / 1500 | LBR on | A bottle "about once per hour" until October 2002 [S1–S6], then "a special item (a net, a message in a bottle or a treasure map) about five times per hour" [S7]; bottles went from 0.4% of catches before Publish 16 [F1] to 1.7% after [F2] | | Colored nets: none, then 10% | LBR on | "With Publish 16, we got a nice surprise--colored special fish nets!" [F4]; "Any net created has a 10% chance of being colored." [F3] | - From LBR a GM's deep-water catches are 0.8% maps and 1.3% bottles. Chrome counted 0.9% and 1.3% [C1], the FCB 0.7% and 1.7% after Publish 16 [F2]. Nets and big fish stay at 0.5%, close to both counts. - At Stratics' 280 casts an hour [S1–S6], that is about one bottle an hour from UO:R to UOTD and five nets, maps or bottles an hour from LBR, the two rates Stratics gives. - The gain chance rises as the success chance falls, so above 65 a cast in deep water gains faster than one near land, Publish 5's "best method for gaining at high levels" [O3]. Below 65 it neither catches nor gains. - Chrome's table also has three kinds of water near land, with difficulties of 15, 35 and 55. No other source has them, so near land stays at 0–100. ### Deep water is at least 14 tiles from land `SpecialFishingNet.FullValidation` decides what counts as deep water: where a special fishing net works, where maps, bottles, nets and big fish can be caught, and from UO:R where a cast is harder. It checks the target and 20 points on its four diagonals, 5 to 25 tiles out. Nothing off those points is read, so a spot 8 tiles from shore can count as deep, while one with land 15 to 25 tiles away on a diagonal does not. The sources give one rule in every era: land at least 14 tiles away. This checks every land tile within 13 of the target. ModernUO has no server lines inside a map, so the only boundary is the map's edge, which counts as land as before. Only land tiles are read, so statics such as rocks are ignored. `SOS.FindLocation` had its own copy of the old check and now calls `FullValidation`, so wrecks follow the same rule. - 1999 to 2000: "which means you must be at least 14 tiles away from all land" [S1–S4] - March 2001: "14 tiles away from any land mass" [F5] - 2001 to 2002: "which means you must be at least 14 tiles away from all land and server lines" [S5–S7] - Today: "You need to be a minimum of 14 tiles from land or a server boundary to be in ‘deep water’." [O9] One guide from January 2000 says "16 tiles away from land" [S9]; every other source says 14. - In the area an SOS picks from on Felucca, 59% of spots pass the old check and 63% pass the new one; in Ilshenar's two sea regions, 16% and 30%. - The check reads up to 729 land tiles instead of 21: about 6 µs per check in a Release build, against under 0.1 µs before; a cast makes at most two. ### Sea serpents In ModernUO a sea serpent comes up from fishing only with a treasure map, a bottle or a net, which it carries. A quarter are deep sea serpents. It has no timer, and nothing makes it attack the fisherman first. This follows the notes of March 28, 1999 and Publish 4. A fished serpent attacks the fisherman at once. Five minutes after it surfaced it goes back to deep water, taking what it carries, unless someone is still fighting it: it took damage in the last two minutes and has not healed to full since, which is what ModernUO's damage entries record. From T2A, at 80 skill or more in deep water, 1% of casts also bring up a serpent that carries nothing, whether the cast catches something or not. 1 serpent in 10 is a deep sea serpent. | Change | Era | Source | |---|---|---| | Attacks the fisherman at once | All | "Sea Serpents will now immediately attack the fisherman that brought them from the deep." [O1] | | Goes back after 5 minutes unless fought | All | "Sea Serpents will return to deep water after 5 minutes if not attacked." [O1]; "will return to the depths after a brief period if they are not engaged" [S10] | | 1% of casts bring up an empty serpent | T2A on | "Fishermen at over 80 skill will have a chance to pull up a sea serpent regardless of whether they succeed or fail at fishing when in deep sea." [O2]; 1% at 80 or more, before the skill check [B1] | | 1 in 10 is a deep sea serpent | All | "A small percentage of Sea Serpents will be Deep-Sea Serpents." [O1]; about 10% [B1]; 9.8% and 10.8% of the serpents counted [F1, F2] | - Publish 4 gives no rate. The 1% comes from a post by Hance, a UO developer, in December 2000 [B1]; T2A uses it too, since it is the rule Publish 4 brought in (our inference). The FCB's counts agree: 1.76% of casts brought up a serpent and 55% of serpents carried nothing, so about 1% of casts brought up an empty one [F1] (our calculation). - Before Publish 4 serpents came up "on failure" [O1], and Stratics kept "if you are fishing with 80+ skill and you fail" from July 1999 to 2002 [S1–S7]. Publish 4 is the last rule in T2A, so it is the one used from T2A. - Hance also relays a side comment that the empty serpent cancels the rest of the cast [B1]. If it did, no cast could bring up two serpents, but the FCB saw two from one cast on 0.96% of serpents [F1], and Chrome caught empty serpents together with fish and footwear [C1]. So the cast goes on. - Hance's post also needs room in the backpack for a fish and fish left in the spot [B1]. A spot with no fish left already ends the cast before the roll, unless an SOS keeps the cast going (next section). A full backpack doesn't: ModernUO doesn't check for room before a cast, so neither does the serpent. - Serpents from a special fishing net are unchanged; Hance's post says nets don't follow its formula [B1]. ### The SOS chest In ModernUO, a fisherman carrying an SOS within 60 tiles of its spot brings up the chest on 1 catch in 8 and a shipwrecked item on the other 7. The SOS also lets a cast go on where the fish have run out. This makes the chest 1 catch in 4, in every era. From UO:R an SOS needs 65 skill, and below that the cast is an ordinary one. | Change | Era | Source | |---|---|---| | The chest on 1 catch in 4 | All | "You have a 25% chance of getting the chest and a 75% chance of getting something else with each cast." [F3]; "a 25% chance to fish up the actual treasure chest" [S7] | | An SOS needs 65 skill | UO:R on | 65 in the chest's row of Stratics' table: "The skills mentioned above are minimum skills needed to fish up the corresponding item." [S5–S7] | - No source gives a number before October 2002, or any for ModernUO's 1 in 8, so the 25% applies in every era. The other catches keep ModernUO's seven kinds of shipwrecked items, equally likely. Hanse's die splits them in three: "One roll is body parts; another roll is shells, barrel staves, globes, etc.; another roll is paintings, broken clocks, etc.; and another is the chest itself." [F3] - Stratics' guide of November 2000 has neither the chest's row nor the 65 for deep water [S4]. The May 2001 version, during Third Dawn, has both: "The minimum required skill level to catch *anything* in deep water is 65." [S5] The first section applies that from UO:R, after Publish 5 made deep water harder [O3], so the chest follows it (our inference). An SOS's spot is picked in deep water, but a cast within 60 tiles of it can be nearer land, so the 65 is checked on its own. - Below 65 there are no shipwrecked items either, because Hanse's die is rolled once you "can even attempt to get a treasure chest" [F3] (our reading). Before UO:R an SOS still works at any skill; the only period line on it is "You do not have to be GM Fisherman to actually fish up the treasure from a bottle, only to fish up A bottle." [S9] ### Fishing reveals from T2A ModernUO reveals a hidden fisherman from ML. The notes of March 28, 1999 already say "If you are hidden, fishing will now unhide you." [O1], so this reveals from T2A, at the same two points: the splash and the end of the cast. ### Big fish and the leviathan's artifact In ModernUO every big fish weighs 3 to 200 stones and gives max(16, weight) / 4 steaks, and the net thrower has a 1 in 4 chance at an artifact at any skill. | Change | Era | Source | |---|---|---| | Big fish weigh 100 stones | Before SE | "They weigh 100 stones each" [S5–S7]; Publish 28 "Added variable weight and fisherman's mark on big fish" [O6] | | A steak per 5 stones, at least 4 | SE on | "now its weight / 5 will determine number of fish steaks (minimum of 4)" [O7] | | The net thrower's own chance at an artifact: 1 in 4, only at GM | SE on | "Give the fabled net thrower (if a GM Fisherman) a 1 in 4 chance of getting a minor artifact (not modified by luck or damage)." [O6] | - Before SE a big fish keeps ModernUO's weight / 4 steaks, 25 for 100 stones: no source gives a number before Publish 29. - Publish 27 names only the attackers' chance: "Minor Artifacts will have a chance of spawning in each attacker's backpack after a paragon creature or a leviathan is slain." [O5] Publish 28 adds the thrower's roll [O6], so before SE there is none (our inference). The attackers' chance is unchanged. ### Magic fish From SE the prized, wondrous and truly rare fish give +1 for 12 seconds instead of +5 for a minute [C1]. A Stratics FAQ of January 5, 2005 already says "The stat boost fish used to provide the person who ate it a +5 boost, but now it's only one or two points, and lasts only a short while." [S16] - Stratics still gives +5 in December 2002 [S7] and no note dates the change. [S16], during SE, is the earliest source with the new values, so the eras before SE keep +5 and ModernUO's minute (our inference). - Today's guide gives "Adds 8 – 12% to your Dexterity for 150 seconds when eaten" [O9]. No note dates that change either, so it is left out. ### The white pearl at an SOS's spot From ML the white pearl is a bonus on top of the catch, but it went through `Fishing.Construct`'s SOS branch: at an SOS's spot the fisherman read "You have found a white pearl!" and got a shipwrecked item or the chest, a second SOS roll in the same cast. The pearl now skips that branch. The keys are #2687's, plus: - [O14] uo.com, the 2002 publishes: Publish 16 in parts on April 23, July 23, September 20 and December 2, 2002. https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/ - [S14] Stratics fishing board, FAQ 1.2 "How do I train fishing?", January 5, 2005. https://web.archive.org/web/20050410171430/http://boards.stratics.com/php-bin/uo/showthreaded.php?Cat=&Board=uofisherman&Number=5622892 - [S15] Stratics fishing board, a post on Publish 5's SOS change, April 25, 2000. https://web.archive.org/web/20000511000424/http://boards.stratics.com:80/cgi-bin/forums/showflat.pl?Cat=21&Board=uofisherman&Number=144300&page=0&view=collapsed&sb=5 - [S16] The FAQ of [S14], 1.4 "What's with the horned leather and scales? (Resources)", January 5, 2005, in the FAQ's flat view. https://web.archive.org/web/20050410171356/http://boards.stratics.com/php-bin/uo/showflat.php?Cat=&Number=5622877&page=0&view=collapsed&sb=5&o=&fpart=1 - [F1] The Fishin' Statistician, 11,101 casts from August 2001 to April 2002, before Publish 16. Skill and water not stated; most likely GM and mostly deep water, since the counts include big fish and serpents. https://web.archive.org/web/20020609120859/http://www.thefcb.com:80/novellettes/stats.html - [F2] The same page, 7,371 casts from July 2002 to February 2003, after Publish 16, with nets, maps and bottles counted too. https://web.archive.org/web/20030216074120/http://www.thefcb.com:80/novellettes/stats.html - [F3] The FCB's interview with Hanse, an OSI designer, October 21, 2002. https://web.archive.org/web/20021104194424/http://www.thefcb.com:80/Hanse.htm - [F4] The FCB's encyclopaedia, colored nets, last modified July 13, 2002. https://web.archive.org/web/20020715111230/http://www.thefcb.com:80/encyclopaedia/colornets.html - [F5] The FCB's "Illustrated Treatise on Deep Water Defined", by Captain Pickles and Mason, last modified March 20, 2001. https://web.archive.org/web/20010419135823/http://www.thefcb.com:80/novellettes/deep-water.html - [B1] Brikan's skill guide, which quotes a post on Origin's boards by Hance, a UO developer, on December 20, 2000; most likely Hanse, the OSI designer the FCB interviewed in 2002. Page last modified February 3, 2007. https://web.archive.org/web/20070715201811/http://www.jgnn.com:80/uo/bk/2skill/skill.htm - [C1] Chrome's fishing page on OSI's Yamato shard, captured November 25, 2007: 5,000 offshore catches at GM, a table of success by water and what each magic fish does. https://web.archive.org/web/20071125144104/http://www.geocities.co.jp:80/Playtown-Yoyo/4812/fishing02.html
578 lines
21 KiB
C#
578 lines
21 KiB
C#
using System;
|
|
using Server.Engines.Quests.Collector;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
using Server.Spells;
|
|
|
|
namespace Server.Engines.Harvest
|
|
{
|
|
public class Fishing : HarvestSystem
|
|
{
|
|
private static Fishing _system;
|
|
|
|
private static readonly Type[] _magicFish =
|
|
[
|
|
typeof(PrizedFish), typeof(WondrousFish), typeof(TrulyRareFish), typeof(PeculiarFish)
|
|
];
|
|
|
|
private static readonly Type[] _footwear = [typeof(Boots), typeof(Shoes), typeof(Sandals), typeof(ThighBoots)];
|
|
|
|
private static readonly MutateEntry[] _mutateTable =
|
|
[
|
|
new(80.0, 80.0, 4080.0, true, Expansion.UOR, typeof(SpecialFishingNet)),
|
|
new(80.0, 80.0, 4080.0, true, Expansion.UOR, typeof(BigFish)),
|
|
// T2A-UOTD: needs 90; 0.25% at 90, 0.5% at 100.
|
|
// LBR onward: needs 90; 0.42% at 90, 0.83% at 100.
|
|
new(90.0, 80.0, 4080.0, true, Expansion.T2A, Expansion.UOTD, typeof(TreasureMap)),
|
|
new(90.0, 80.0, 2480.0, true, Expansion.LBR, typeof(TreasureMap)),
|
|
// T2A-UOTD: needs 100; 0.5% at 100.
|
|
// LBR onward: needs 100; 1.33% at 100.
|
|
new(100.0, 80.0, 4080.0, true, Expansion.T2A, Expansion.UOTD, typeof(MessageInABottle)),
|
|
new(100.0, 80.0, 1580.0, true, Expansion.LBR, typeof(MessageInABottle)),
|
|
// T2A-UOTD: needs 80; 2.0% at 80, 2.5% at 100.
|
|
// LBR-SA: no minimum; 0% at 0, 5% at 100.
|
|
// HS onward: needs 80; 4.0% at 80, 5.0% at 100.
|
|
new(80.0, 0.0, 4000.0, false, Expansion.T2A, Expansion.UOTD, _magicFish),
|
|
new(0.0, 0.0, 2000.0, false, Expansion.LBR, Expansion.SA, _magicFish),
|
|
new(80.0, 0.0, 2000.0, false, Expansion.HS, _magicFish),
|
|
new(0.0, 103.5, -34.5, false, Expansion.T2A, Expansion.UOTD, _footwear),
|
|
// From LBR the two footwear rows make one curve: 75% at 0 skill, 10% at 100 and 5% at 120.
|
|
new(0.0, 100.0, -62.5, false, Expansion.LBR, _footwear),
|
|
new(0.0, 140.0, -260.0, false, Expansion.LBR, _footwear)
|
|
];
|
|
|
|
// From UO:R deep water needs 65 skill to catch anything, and a GM succeeds 70% of the time.
|
|
private static readonly HarvestResource _deepWaterFish = new(0.0, 65.0, 115.0, 1043297, typeof(Fish));
|
|
|
|
private static readonly int[] _waterLandTiles =
|
|
[
|
|
0x00A8, 0x00AB,
|
|
0x0136, 0x0137
|
|
];
|
|
|
|
private static readonly int[] waterStaticTiles =
|
|
[
|
|
0x1797, 0x179C,
|
|
0x346E, 0x3485,
|
|
0x3490, 0x34AB,
|
|
0x34B5, 0x35D5
|
|
];
|
|
|
|
private static readonly int[] bodyParts =
|
|
[
|
|
0x1CDD, 0x1CE5, // arm
|
|
0x1CE0, 0x1CE8, // torso
|
|
0x1CE1, 0x1CE9, // head
|
|
0x1CE2, 0x1CEC // leg
|
|
];
|
|
|
|
private static readonly int[] boneParts =
|
|
[
|
|
0x1AE0, 0x1AE1, 0x1AE2, 0x1AE3, 0x1AE4, // skulls
|
|
0x1B09, 0x1B0A, 0x1B0B, 0x1B0C, 0x1B0D, 0x1B0E, 0x1B0F, 0x1B10, // bone piles
|
|
0x1B15, 0x1B16 // pelvis bones
|
|
];
|
|
|
|
private static readonly int[] miscItems =
|
|
[
|
|
0x1EB5, // unfinished barrel
|
|
0xA2A, // stool
|
|
0xC1F, // broken clock
|
|
0x1047, 0x1048, // globe
|
|
0x1EB1, 0x1EB2, 0x1EB3, 0x1EB4 // barrel staves
|
|
];
|
|
|
|
private Fishing()
|
|
{
|
|
var fish = new HarvestDefinition
|
|
{
|
|
BankWidth = 8,
|
|
BankHeight = 8,
|
|
MinTotal = 5,
|
|
MaxTotal = 15,
|
|
MinRespawn = TimeSpan.FromMinutes(10.0),
|
|
MaxRespawn = TimeSpan.FromMinutes(20.0),
|
|
Skill = SkillName.Fishing,
|
|
LandTiles = _waterLandTiles,
|
|
StaticTiles = waterStaticTiles,
|
|
RangedTiles = true,
|
|
MaxRange = 4,
|
|
ConsumedPerHarvest = 1,
|
|
ConsumedPerFeluccaHarvest = 1,
|
|
EffectActions = [12],
|
|
EffectSounds = [],
|
|
EffectCounts = [1],
|
|
EffectDelay = TimeSpan.Zero,
|
|
EffectSoundDelay = TimeSpan.FromSeconds(8.0),
|
|
NoResourcesMessage = 503172, // The fish don't seem to be biting here.
|
|
FailMessage = 503171, // You fish a while, but fail to catch anything.
|
|
TimedOutOfRangeMessage = 500976, // You need to be closer to the water to fish!
|
|
OutOfRangeMessage = 500976, // You need to be closer to the water to fish!
|
|
PackFullMessage = 503176, // You do not have room in your backpack for a fish.
|
|
ToolBrokeMessage = 503174 // You broke your fishing pole.
|
|
};
|
|
|
|
HarvestResource[] res = [new(00.0, 00.0, 100.0, 1043297, typeof(Fish))];
|
|
|
|
HarvestVein[] veins = [new(1000, 0.0, res[0], null)];
|
|
|
|
fish.Resources = res;
|
|
fish.Veins = veins;
|
|
|
|
if (Core.ML)
|
|
{
|
|
fish.BonusResources =
|
|
[
|
|
new BonusHarvestResource(0, 99.4, null, null), // set to same chance as mining ml gems
|
|
new BonusHarvestResource(80.0, .6, 1072597, typeof(WhitePearl))
|
|
];
|
|
}
|
|
|
|
Definitions = [fish];
|
|
}
|
|
|
|
public static Fishing System => _system ??= new Fishing();
|
|
|
|
public override void OnConcurrentHarvest(Mobile from, Item tool, HarvestDefinition def, object toHarvest)
|
|
{
|
|
from.SendLocalizedMessage(500972); // You are already fishing.
|
|
}
|
|
|
|
public override bool SpecialHarvest(Mobile from, Item tool, HarvestDefinition def, Map map, Point3D loc)
|
|
{
|
|
if (from is PlayerMobile player)
|
|
{
|
|
var qs = player.Quest;
|
|
|
|
if (qs is CollectorQuest)
|
|
{
|
|
var obj = qs.FindObjective<FishPearlsObjective>();
|
|
|
|
if (obj?.Completed == false)
|
|
{
|
|
if (Utility.RandomBool())
|
|
{
|
|
// You pull a shellfish out of the water, and find a rainbow pearl inside of it.
|
|
player.SendLocalizedMessage(1055086, "", 0x59);
|
|
obj.CurProgress++;
|
|
}
|
|
else
|
|
{
|
|
// You pull a shellfish out of the water, but it doesn't have a rainbow pearl.
|
|
player.SendLocalizedMessage(1055087, "", 0x2C);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public override HarvestResource MutateResource(
|
|
Mobile from, Item tool, HarvestDefinition def, Map map, Point3D loc,
|
|
HarvestVein vein, HarvestResource primary, HarvestResource fallback
|
|
) => Core.UOR && SpecialFishingNet.FullValidation(map, loc.X, loc.Y)
|
|
? _deepWaterFish
|
|
: base.MutateResource(from, tool, def, map, loc, vein, primary, fallback);
|
|
|
|
public override Type MutateType(
|
|
Type type, Mobile from, Item tool, HarvestDefinition def, Map map, Point3D loc,
|
|
HarvestResource resource
|
|
)
|
|
{
|
|
var deepWater = SpecialFishingNet.FullValidation(map, loc.X, loc.Y);
|
|
|
|
var skillBase = from.Skills.Fishing.Base;
|
|
var skillValue = from.Skills.Fishing.Value;
|
|
|
|
for (var i = 0; i < _mutateTable.Length; ++i)
|
|
{
|
|
var entry = _mutateTable[i];
|
|
|
|
if (Core.Expansion < entry.m_Expansion || Core.Expansion > entry.m_MaxExpansion)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!deepWater && entry.m_DeepWater)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (skillBase >= entry.m_ReqSkill)
|
|
{
|
|
var chance = (skillValue - entry.m_MinSkill) / (entry.m_MaxSkill - entry.m_MinSkill);
|
|
|
|
if (chance > Utility.RandomDouble())
|
|
{
|
|
return entry.m_Types.RandomElement();
|
|
}
|
|
}
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
// From UO:R the chest needs 65 skill, and below that an SOS changes nothing.
|
|
private static bool CanFishUpShipwreck(Mobile from) => !Core.UOR || from.Skills.Fishing.Base >= 65.0;
|
|
|
|
public override bool CheckResources(Mobile from, Item tool, HarvestDefinition def, Map map, Point3D loc, bool timed)
|
|
{
|
|
var pack = from.Backpack;
|
|
|
|
if (pack != null && CanFishUpShipwreck(from))
|
|
{
|
|
foreach (var sos in pack.FindItemsByType<SOS>())
|
|
{
|
|
if (from.Map == sos.TargetMap && from.InRange(sos.TargetLocation, 60))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return base.CheckResources(from, tool, def, map, loc, timed);
|
|
}
|
|
|
|
public override Item Construct(Type type, Mobile from)
|
|
{
|
|
if (type == typeof(TreasureMap))
|
|
{
|
|
int level;
|
|
if (from is PlayerMobile mobile && mobile.Young && mobile.Map == Map.Trammel &&
|
|
TreasureMap.IsInHavenIsland(from))
|
|
{
|
|
level = 0;
|
|
}
|
|
else
|
|
{
|
|
level = 1;
|
|
}
|
|
|
|
return new TreasureMap(level, from.Map == Map.Felucca ? Map.Felucca : Map.Trammel);
|
|
}
|
|
|
|
if (type == typeof(MessageInABottle))
|
|
{
|
|
return new MessageInABottle(from.Map == Map.Felucca ? Map.Felucca : Map.Trammel);
|
|
}
|
|
|
|
// The pearl is a bonus on top of the catch, so an SOS never takes its place.
|
|
if (type == typeof(WhitePearl))
|
|
{
|
|
return base.Construct(type, from);
|
|
}
|
|
|
|
var pack = from.Backpack;
|
|
|
|
if (pack != null && CanFishUpShipwreck(from))
|
|
{
|
|
// We don't have to queue since we are returning on the first SOS.
|
|
foreach (var sos in pack.FindItemsByType<SOS>())
|
|
{
|
|
if (from.Map != sos.TargetMap || !from.InRange(sos.TargetLocation, 60))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (Utility.Random(4) != 0)
|
|
{
|
|
return Utility.Random(7) switch
|
|
{
|
|
0 => new ShipwreckedItem(bodyParts.RandomElement()), // Body Parts
|
|
1 => new ShipwreckedItem(boneParts.RandomElement()), // Bone Parts
|
|
2 => new ShipwreckedItem(Utility.Random(0xE9F, 10)), // Paintings and portraits
|
|
3 => new ShipwreckedItem(Utility.Random(0x13A4, 11)), // Pillows
|
|
4 => new ShipwreckedItem(Utility.Random(0xFC4, 9)), // Shells
|
|
5 => Utility.RandomBool() // Hats
|
|
? new SkullCap { IsShipwreckedItem = true }
|
|
: new TricorneHat { IsShipwreckedItem = true },
|
|
6 => Utility.Random(miscItems.Length + 1) switch // Misc
|
|
{
|
|
var r when r < miscItems.Length => new ShipwreckedItem(miscItems[r]),
|
|
_ => new Candelabra { IsShipwreckedItem = true },
|
|
}
|
|
};
|
|
}
|
|
|
|
LockableContainer chest = Utility.RandomBool() ? new MetalGoldenChest() : new WoodenChest();
|
|
|
|
if (sos.IsAncient && Core.SE)
|
|
{
|
|
chest.Hue = 0x481;
|
|
}
|
|
|
|
TreasureMapChest.Fill(chest, Math.Max(1, Math.Min(4, sos.Level)));
|
|
|
|
if (Core.UOR)
|
|
{
|
|
chest.DropItem(sos.IsAncient ? new FabledFishingNet() : new SpecialFishingNet());
|
|
}
|
|
|
|
chest.Movable = true;
|
|
chest.Locked = false;
|
|
chest.TrapType = TrapType.None;
|
|
chest.TrapPower = 0;
|
|
chest.TrapLevel = 0;
|
|
|
|
sos.Delete();
|
|
|
|
return chest;
|
|
}
|
|
}
|
|
|
|
return base.Construct(type, from);
|
|
}
|
|
|
|
public override bool Give(Mobile m, Item item, bool placeAtFeet)
|
|
{
|
|
if (item is TreasureMap or MessageInABottle or SpecialFishingNet)
|
|
{
|
|
SurfaceSerpent(m).PackItem(item);
|
|
|
|
return true; // we don't want to give the item to the player, it's on the serpent
|
|
}
|
|
|
|
return base.Give(m, item, placeAtFeet || item is BigFish or WoodenChest or MetalGoldenChest);
|
|
}
|
|
|
|
private static BaseCreature SurfaceSerpent(Mobile m)
|
|
{
|
|
BaseCreature serp = Utility.RandomDouble() < 0.1 ? new DeepSeaSerpent() : new SeaSerpent();
|
|
|
|
int x = m.X, y = m.Y;
|
|
|
|
var map = m.Map;
|
|
|
|
for (var i = 0; map != null && i < 20; ++i)
|
|
{
|
|
var tx = m.X - 10 + Utility.Random(21);
|
|
var ty = m.Y - 10 + Utility.Random(21);
|
|
|
|
var t = map.Tiles.GetLandTile(tx, ty);
|
|
|
|
if (t.Z == -5 && (t.ID >= 0xA8 && t.ID <= 0xAB || t.ID >= 0x136 && t.ID <= 0x137) &&
|
|
!SpellHelper.CheckMulti(new Point3D(tx, ty, -5), map))
|
|
{
|
|
x = tx;
|
|
y = ty;
|
|
break;
|
|
}
|
|
}
|
|
|
|
serp.MoveToWorld(new Point3D(x, y, -5), map);
|
|
|
|
serp.Home = serp.Location;
|
|
serp.RangeHome = 10;
|
|
serp.Combatant = m;
|
|
|
|
Timer.StartTimer(TimeSpan.FromMinutes(5.0), () => ReturnToDeepWater(serp));
|
|
|
|
m.SendLocalizedMessage(503170); // Uh oh! That doesn't look like a fish!
|
|
|
|
return serp;
|
|
}
|
|
|
|
// Damage entries last 2 minutes and clear at full health, so a serpent nobody is fighting goes back to deep water.
|
|
private static void ReturnToDeepWater(BaseCreature serpent)
|
|
{
|
|
if (!serpent.Deleted && serpent.FindMostRecentDamager(false) == null)
|
|
{
|
|
serpent.Delete();
|
|
}
|
|
}
|
|
|
|
public override void SendSuccessTo(Mobile from, Item item, HarvestResource resource)
|
|
{
|
|
if (item is BigFish fish)
|
|
{
|
|
from.SendLocalizedMessage(1042635); // Your fishing pole bends as you pull a big fish from the depths!
|
|
fish.Fisher = from;
|
|
}
|
|
else if (item is WoodenChest or MetalGoldenChest)
|
|
{
|
|
from.SendLocalizedMessage(503175); // You pull up a heavy chest from the depths of the ocean!
|
|
}
|
|
else
|
|
{
|
|
int number;
|
|
string name;
|
|
|
|
if (item is BaseMagicFish)
|
|
{
|
|
number = 1008124;
|
|
name = "a mess of small fish";
|
|
}
|
|
else if (item is Fish or BaseShoes)
|
|
{
|
|
number = 1008124;
|
|
name = item.ItemData.Name;
|
|
}
|
|
else if (item is TreasureMap)
|
|
{
|
|
number = 1008125;
|
|
name = "a sodden piece of parchment";
|
|
}
|
|
else if (item is MessageInABottle)
|
|
{
|
|
number = 1008125;
|
|
name = "a bottle, with a message in it";
|
|
}
|
|
else if (item is SpecialFishingNet)
|
|
{
|
|
number = 1008125;
|
|
name = "a special fishing net"; // TODO: this is just a guess--what should it really be named?
|
|
}
|
|
else
|
|
{
|
|
number = 1043297;
|
|
|
|
if ((item.ItemData.Flags & TileFlag.ArticleA) != 0)
|
|
{
|
|
name = $"a {item.ItemData.Name}";
|
|
}
|
|
else if ((item.ItemData.Flags & TileFlag.ArticleAn) != 0)
|
|
{
|
|
name = $"an {item.ItemData.Name}";
|
|
}
|
|
else
|
|
{
|
|
name = item.ItemData.Name;
|
|
}
|
|
}
|
|
|
|
var ns = from.NetState;
|
|
|
|
if (number == 1043297 || ns?.HighSeas == true)
|
|
{
|
|
from.SendLocalizedMessage(number, name);
|
|
}
|
|
else
|
|
{
|
|
from.SendLocalizedMessage(number, true, name);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnHarvestStarted(Mobile from, Item tool, HarvestDefinition def, object toHarvest)
|
|
{
|
|
base.OnHarvestStarted(from, tool, def, toHarvest);
|
|
|
|
if (GetHarvestDetails(from, tool, toHarvest, out _, out var map, out var loc, out _))
|
|
{
|
|
Timer.StartTimer(
|
|
TimeSpan.FromSeconds(1.5),
|
|
() =>
|
|
{
|
|
if (Core.T2A)
|
|
{
|
|
from.RevealingAction();
|
|
}
|
|
|
|
Effects.SendLocationEffect(loc, map, 0x352D, 16, 4);
|
|
Effects.PlaySound(loc, map, 0x364);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
public override void OnHarvestFinished(
|
|
Mobile from, Item tool, HarvestDefinition def, HarvestVein vein,
|
|
HarvestBank bank, HarvestResource resource, object harvested
|
|
)
|
|
{
|
|
base.OnHarvestFinished(from, tool, def, vein, bank, resource, harvested);
|
|
|
|
if (Core.T2A)
|
|
{
|
|
from.RevealingAction();
|
|
}
|
|
|
|
// From T2A, 1% of casts in deep water at 80 skill or more bring up an empty sea serpent, whatever the catch.
|
|
if (Core.T2A && from.Skills.Fishing.Base >= 80.0 && Utility.RandomDouble() < 0.01 &&
|
|
GetHarvestDetails(from, tool, harvested, out _, out var map, out var loc, out _) &&
|
|
SpecialFishingNet.FullValidation(map, loc.X, loc.Y))
|
|
{
|
|
SurfaceSerpent(from);
|
|
}
|
|
}
|
|
|
|
public override object GetLock(Mobile from, Item tool, HarvestDefinition def, object toHarvest) => this;
|
|
|
|
public override bool BeginHarvesting(Mobile from, Item tool)
|
|
{
|
|
if (!base.BeginHarvesting(from, tool))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
from.SendLocalizedMessage(500974); // What water do you want to fish in?
|
|
return true;
|
|
}
|
|
|
|
public override bool CheckHarvest(Mobile from, Item tool)
|
|
{
|
|
if (!base.CheckHarvest(from, tool))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (from.Mounted)
|
|
{
|
|
from.SendLocalizedMessage(500971); // You can't fish while riding!
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public override bool CheckHarvest(Mobile from, Item tool, HarvestDefinition def, object toHarvest)
|
|
{
|
|
if (!base.CheckHarvest(from, tool, def, toHarvest))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (from.Mounted)
|
|
{
|
|
from.SendLocalizedMessage(500971); // You can't fish while riding!
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private class MutateEntry
|
|
{
|
|
public readonly bool m_DeepWater;
|
|
public readonly Expansion m_Expansion;
|
|
public readonly Expansion? m_MaxExpansion;
|
|
public readonly double m_MaxSkill;
|
|
public readonly double m_MinSkill;
|
|
public readonly double m_ReqSkill;
|
|
public readonly Type[] m_Types;
|
|
|
|
public MutateEntry(
|
|
double reqSkill, double minSkill, double maxSkill, bool deepWater, Expansion expansion, params Type[] types
|
|
) : this(reqSkill, minSkill, maxSkill, deepWater, expansion, null, types)
|
|
{
|
|
}
|
|
|
|
public MutateEntry(
|
|
double reqSkill, double minSkill, double maxSkill, bool deepWater, Expansion expansion,
|
|
Expansion? maxExpansion, params Type[] types
|
|
)
|
|
{
|
|
m_ReqSkill = reqSkill;
|
|
m_MinSkill = minSkill;
|
|
m_MaxSkill = maxSkill;
|
|
m_DeepWater = deepWater;
|
|
m_Expansion = expansion;
|
|
m_MaxExpansion = maxExpansion;
|
|
m_Types = types;
|
|
}
|
|
}
|
|
}
|
|
}
|