#W# Merge: Merged with upstream. Conflicts resolved.
This commit is contained in:
commit
f6f50470a9
276 changed files with 6742 additions and 8845 deletions
|
|
@ -28,7 +28,7 @@ using Server.Logging;
|
|||
|
||||
namespace Server.Engines.CannedEvil;
|
||||
|
||||
[SerializationGenerator(11, false)]
|
||||
[SerializationGenerator(12, false)]
|
||||
public partial class ChampionSpawn : Item
|
||||
{
|
||||
private void MigrateFrom(V10Content content)
|
||||
|
|
@ -59,6 +59,35 @@ public partial class ChampionSpawn : Item
|
|||
_restartTime = content.RestartTime;
|
||||
}
|
||||
|
||||
private void MigrateFrom(V11Content content)
|
||||
{
|
||||
_level = content.Level;
|
||||
_activatedByProximity = content.ActivatedByProximity;
|
||||
_nextProximityTime = content.NextProximityTime;
|
||||
_maxLevel = content.MaxLevel;
|
||||
_activatedByValor = content.ActivatedByValor;
|
||||
_damageEntries = content.DamageEntries;
|
||||
_confinedRoaming = content.ConfinedRoaming;
|
||||
_idol = content.Idol;
|
||||
_hasBeenAdvanced = content.HasBeenAdvanced;
|
||||
_spawnArea = content.SpawnArea;
|
||||
_randomizeType = content.RandomizeType;
|
||||
_kills = content.Kills;
|
||||
_active = content.Active;
|
||||
_type = content.Type;
|
||||
_creatures = content.Creatures;
|
||||
_redSkulls = content.RedSkulls;
|
||||
_whiteSkulls = content.WhiteSkulls;
|
||||
_platform = content.Platform;
|
||||
_altar = content.Altar;
|
||||
_expireDelay = content.ExpireDelay;
|
||||
_expireTime = content.ExpireTime;
|
||||
_champion = content.Champion;
|
||||
_restartDelay = content.RestartDelay;
|
||||
_restartTime = content.RestartTime;
|
||||
_minions = [];
|
||||
}
|
||||
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionSpawn));
|
||||
|
||||
[SerializableField(1)]
|
||||
|
|
@ -143,6 +172,12 @@ public partial class ChampionSpawn : Item
|
|||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private DateTime _restartTime;
|
||||
|
||||
// Mobiles summoned by the champion or the spawn's creatures. Deleted with the creatures, never counted as kills.
|
||||
[Tidy]
|
||||
[SerializableField(24)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private List<Mobile> _minions;
|
||||
|
||||
private ChampionSpawnRegion m_Region;
|
||||
|
||||
//Goes back each level, below level 0 and it goes off!
|
||||
|
|
@ -173,6 +208,7 @@ public partial class ChampionSpawn : Item
|
|||
Visible = false;
|
||||
|
||||
_creatures = new List<Mobile>();
|
||||
_minions = [];
|
||||
_redSkulls = new List<Item>();
|
||||
_whiteSkulls = new List<Item>();
|
||||
|
||||
|
|
@ -294,6 +330,21 @@ public partial class ChampionSpawn : Item
|
|||
|
||||
public bool IsChampionSpawn(Mobile m) => _creatures.Contains(m);
|
||||
|
||||
/// <summary>
|
||||
/// The spawn <paramref name="creature" /> fights for: as its champion, one of its creatures, or a minion of either.
|
||||
/// </summary>
|
||||
public static ChampionSpawn FindSpawnOf(BaseCreature creature)
|
||||
{
|
||||
var spawn = Region.Find(creature.Location, creature.Map).GetRegion<ChampionSpawnRegion>()?.Spawn;
|
||||
|
||||
return spawn?.Deleted == false &&
|
||||
(spawn.Champion == creature || spawn._minions.Contains(creature) || spawn._creatures.Contains(creature))
|
||||
? spawn
|
||||
: null;
|
||||
}
|
||||
|
||||
public void AddMinion(Mobile minion) => AddToMinions(minion);
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public virtual int MaxKills
|
||||
{
|
||||
|
|
@ -1171,6 +1222,13 @@ public partial class ChampionSpawn : Item
|
|||
|
||||
ClearCreatures();
|
||||
}
|
||||
|
||||
for (var i = 0; i < _minions.Count; ++i)
|
||||
{
|
||||
_minions[i].Delete();
|
||||
}
|
||||
|
||||
ClearMinions();
|
||||
}
|
||||
|
||||
public virtual void RegisterDamageTo(Mobile m)
|
||||
|
|
@ -1321,6 +1379,7 @@ public partial class ChampionSpawn : Item
|
|||
_active = reader.ReadBool();
|
||||
_type = (ChampionSpawnType)reader.ReadInt();
|
||||
_creatures = reader.ReadEntityList<Mobile>();
|
||||
_minions = [];
|
||||
_redSkulls = reader.ReadEntityList<Item>();
|
||||
_whiteSkulls = reader.ReadEntityList<Item>();
|
||||
_platform = reader.ReadEntity<ChampionPlatform>();
|
||||
|
|
|
|||
|
|
@ -663,15 +663,7 @@ namespace Server.Engines.Craft
|
|||
}
|
||||
// ******************
|
||||
|
||||
for (var j = 0; types[i] == null && j < m_TypesTable.Length; ++j)
|
||||
{
|
||||
if (m_TypesTable[j][0] == baseType)
|
||||
{
|
||||
types[i] = m_TypesTable[j];
|
||||
}
|
||||
}
|
||||
|
||||
types[i] ??= [baseType];
|
||||
types[i] = GetEquivalentTypes(baseType) ?? [baseType];
|
||||
amounts[i] = craftRes.Amount;
|
||||
|
||||
// For stackable items that can be crafted more than one at a time
|
||||
|
|
@ -1271,13 +1263,54 @@ namespace Server.Engines.Craft
|
|||
return true;
|
||||
}
|
||||
|
||||
private static int GetHuedAmount(Container pack, Type type, int hue)
|
||||
/// <summary>
|
||||
/// Returns the interchangeable resource types for <paramref name="baseType"/> (e.g. logs and boards),
|
||||
/// or null when it has no equivalents.
|
||||
/// </summary>
|
||||
internal static Type[] GetEquivalentTypes(Type baseType)
|
||||
{
|
||||
for (var i = 0; i < m_TypesTable.Length; i++)
|
||||
{
|
||||
if (m_TypesTable[i][0] == baseType)
|
||||
{
|
||||
return m_TypesTable[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsHuedMatch(Item item, Type type, Type[] equivalents, int hue)
|
||||
{
|
||||
if (item.Hue != hue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (equivalents == null)
|
||||
{
|
||||
return type.IsInstanceOfType(item);
|
||||
}
|
||||
|
||||
for (var i = 0; i < equivalents.Length; i++)
|
||||
{
|
||||
if (equivalents[i].IsInstanceOfType(item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static int GetHuedAmount(Container pack, Type type, int hue)
|
||||
{
|
||||
var total = 0;
|
||||
var equivalents = GetEquivalentTypes(type);
|
||||
|
||||
foreach (var item in pack.FindItems(true))
|
||||
{
|
||||
if (item.Hue == hue && type.IsInstanceOfType(item))
|
||||
if (IsHuedMatch(item, type, equivalents, hue))
|
||||
{
|
||||
total += item.Amount;
|
||||
}
|
||||
|
|
@ -1289,6 +1322,7 @@ namespace Server.Engines.Craft
|
|||
private static bool ConsumeHuedAmount(Container pack, Type type, int hue, int amount)
|
||||
{
|
||||
var remaining = amount;
|
||||
var equivalents = GetEquivalentTypes(type);
|
||||
using var toDelete = PooledRefList<Item>.Create();
|
||||
|
||||
foreach (var item in pack.FindItems(true))
|
||||
|
|
@ -1298,7 +1332,7 @@ namespace Server.Engines.Craft
|
|||
break;
|
||||
}
|
||||
|
||||
if (item.Hue != hue || !type.IsInstanceOfType(item))
|
||||
if (!IsHuedMatch(item, type, equivalents, hue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
211
Projects/UOContent/Engines/Craft/T2A/T2ACooking.cs
Normal file
211
Projects/UOContent/Engines/Craft/T2A/T2ACooking.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
/// <summary>An item that can be double-clicked under T2A to combine with, or cook over, a target.</summary>
|
||||
public interface IT2ACombinable
|
||||
{
|
||||
bool TryCombine(Mobile from, object target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared helpers for T2A cooking, which is item-on-item and item-on-heat-source rather than a craft menu.
|
||||
/// Each ingredient item decides its own results in <see cref="IT2ACombinable.TryCombine"/>.
|
||||
/// </summary>
|
||||
public sealed class T2ACooking
|
||||
{
|
||||
private const int _heatSourceRange = 1;
|
||||
private const int _waterRange = 2;
|
||||
private const int _burnRange = 3;
|
||||
|
||||
private T2ACooking()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Asks the player for a target if the item is in their pack.</summary>
|
||||
public static void Prompt(Mobile from, Item source)
|
||||
{
|
||||
if (!ValidateInPack(from, source))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendAsciiMessage("What do you wish to combine this with, or cook it over?");
|
||||
from.Target = new CookTarget(source);
|
||||
}
|
||||
|
||||
public static bool ValidateInPack(Mobile from, Item item)
|
||||
{
|
||||
if (item is { Deleted: false } && item.IsChildOf(from.Backpack))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
from.SendLocalizedMessage(1042001); // That must be in your pack for you to use it.
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool CannotCombine(Mobile from)
|
||||
{
|
||||
from.SendAsciiMessage("You cannot combine those.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//TODO Support static water troughs and barrels (no static water ID list exists yet)
|
||||
public static IHasQuantity GetWaterSource(object o) =>
|
||||
o switch
|
||||
{
|
||||
BaseBeverage { Content: BeverageType.Water, Quantity: > 0 } bev => bev,
|
||||
BaseWaterContainer { IsEmpty: false } container => container,
|
||||
AddonComponent { Addon: IWaterSource { Quantity: > 0 } trough } => trough,
|
||||
IWaterSource { Quantity: > 0 } source => source,
|
||||
_ => null
|
||||
};
|
||||
|
||||
// Ground water only, so a nearby open container (someone else's pack) cannot be drawn from.
|
||||
public static bool CanReachWater(Mobile from, Item water)
|
||||
{
|
||||
if (water.IsChildOf(from.Backpack) || water.Parent == from ||
|
||||
water.RootParent == null && water.Map == from.Map &&
|
||||
from.InRange(water.GetWorldLocation(), _waterRange) && from.InLOS(water))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that.
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Spends one unit of each ingredient and gives the player the result.</summary>
|
||||
public static bool Combine(Mobile from, object source, object target, Item result, string message)
|
||||
{
|
||||
ConsumeOne(source);
|
||||
ConsumeOne(target);
|
||||
|
||||
if (!GiveResult(from, result))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
from.SendAsciiMessage(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
// AddToBackpack drops a result that does not fit at the mobile's feet (or logout location),
|
||||
// but with no valid map either it would be left on the internal map forever.
|
||||
private static bool GiveResult(Mobile from, Item result)
|
||||
{
|
||||
if (from.AddToBackpack(result) || result.Map != null && result.Map != Map.Internal)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
result.Delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
//TODO Confirm whether an empty bowl is returned when a BowlFlour is used up
|
||||
private static void ConsumeOne(object o)
|
||||
{
|
||||
if (o is IHasQuantity container)
|
||||
{
|
||||
container.Quantity--;
|
||||
}
|
||||
else if (o is Item item)
|
||||
{
|
||||
item.Consume();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cooks the source over a heat source if it is close enough and the player is not already cooking.</summary>
|
||||
public static bool BeginHeat(Mobile from, Item source, object target)
|
||||
{
|
||||
var point = target is IPoint3D p ? new Point3D(p) : from.Location;
|
||||
if (from.Map == null || !from.InRange(point, _heatSourceRange))
|
||||
{
|
||||
from.SendLocalizedMessage(500446); // That is too far away.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!from.BeginAction<T2ACooking>())
|
||||
{
|
||||
from.SendLocalizedMessage(500119); // You must wait to perform another action
|
||||
return false;
|
||||
}
|
||||
|
||||
from.PlaySound(0x225);
|
||||
source.Consume();
|
||||
|
||||
Timer.DelayCall(TimeSpan.FromSeconds(5.0), FinishHeat, from, source, from.Map, point);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The source is already consumed here and may be deleted, so only its type and CookingLevel are read.
|
||||
private static void FinishHeat(Mobile from, Item source, Map map, Point3D point)
|
||||
{
|
||||
from.EndAction<T2ACooking>();
|
||||
|
||||
if (from.Deleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.Map != map || !from.InRange(point, _burnRange))
|
||||
{
|
||||
from.SendLocalizedMessage(500686); // You burn the food to a crisp! It's ruined.
|
||||
return;
|
||||
}
|
||||
|
||||
var (min, max) = source switch
|
||||
{
|
||||
CookableFood food => (food.CookingLevel, 100.0),
|
||||
Dough => CraftRange(typeof(BreadLoaf)),
|
||||
SweetDough => CraftRange(typeof(Muffins)),
|
||||
_ => (0.0, 100.0)
|
||||
};
|
||||
|
||||
if (!from.CheckSkill(SkillName.Cooking, min, max))
|
||||
{
|
||||
from.SendLocalizedMessage(500686); // You burn the food to a crisp! It's ruined.
|
||||
return;
|
||||
}
|
||||
|
||||
Item result = source switch
|
||||
{
|
||||
CookableFood food => food.Cook(),
|
||||
Dough => new BreadLoaf(),
|
||||
SweetDough => new Muffins(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (result != null && GiveResult(from, result))
|
||||
{
|
||||
from.PlaySound(0x57);
|
||||
}
|
||||
}
|
||||
|
||||
// Falls back to the full range when the cooking craft system has no entry or is not initialized.
|
||||
private static (double Min, double Max) CraftRange(Type type)
|
||||
{
|
||||
var skills = DefCooking.CraftSystem?.CraftItems.SearchFor(type)?.Skills;
|
||||
|
||||
return skills is { Count: > 0 } ? (skills[0].MinSkill, skills[0].MaxSkill) : (0.0, 100.0);
|
||||
}
|
||||
|
||||
private sealed class CookTarget : Target
|
||||
{
|
||||
private readonly Item _source;
|
||||
|
||||
public CookTarget(Item source) : base(_waterRange, false, TargetFlags.None) => _source = source;
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (ValidateInPack(from, _source) && _source is IT2ACombinable combinable)
|
||||
{
|
||||
combinable.TryCombine(from, targeted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,12 @@ public static class T2ACraftSystem
|
|||
/// </summary>
|
||||
public static bool Enabled { get; set; }
|
||||
|
||||
public static bool HasMenu(CraftSystem system) =>
|
||||
system == DefBlacksmithy.CraftSystem || system == DefAlchemy.CraftSystem ||
|
||||
system == DefBowFletching.CraftSystem || system == DefCarpentry.CraftSystem ||
|
||||
system == DefCartography.CraftSystem || system == DefInscription.CraftSystem ||
|
||||
system == DefTailoring.CraftSystem || system == DefTinkering.CraftSystem;
|
||||
|
||||
public static void ShowMenu(Mobile from, CraftSystem craftSystem, BaseTool tool, Item preTarget = null)
|
||||
{
|
||||
if (!Enabled)
|
||||
|
|
@ -190,9 +196,11 @@ public static class T2ACraftSystem
|
|||
/// Checks if a player can craft a specific item, accounting for sub-resource types.
|
||||
/// When <paramref name="selectedResourceType"/> is non-null, checks against that specific sub-resource.
|
||||
/// When null, checks against ANY available sub-resource the player has sufficient skill and materials for.
|
||||
/// A non-negative <paramref name="hue"/> counts only same-hue stock for the primary resource, exactly as
|
||||
/// the hue-aware craft path consumes it.
|
||||
/// </summary>
|
||||
public static bool CanCraftItem(
|
||||
Mobile from, CraftItem itemDef, CraftSystem system, Type selectedResourceType = null
|
||||
Mobile from, CraftItem itemDef, CraftSystem system, Type selectedResourceType = null, int hue = -1
|
||||
)
|
||||
{
|
||||
var pack = from.Backpack;
|
||||
|
|
@ -214,6 +222,27 @@ public static class T2ACraftSystem
|
|||
var res = itemDef.Resources[i];
|
||||
var resType = res.ItemType;
|
||||
|
||||
if (hue >= 0 && i == 0)
|
||||
{
|
||||
if (resCol.Init && resType == resCol.ResType && selectedResourceType != null)
|
||||
{
|
||||
var subRes = resCol.SearchFor(selectedResourceType);
|
||||
if (subRes != null && from.Skills[system.MainSkill].Value < subRes.RequiredSkill)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
resType = selectedResourceType;
|
||||
}
|
||||
|
||||
if (CraftItem.GetHuedAmount(pack, resType, hue) < res.Amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this resource is the base sub-resource type (e.g. IronIngot for blacksmithing),
|
||||
// handle sub-resource substitution
|
||||
if (resCol.Init && resType == resCol.ResType)
|
||||
|
|
@ -250,17 +279,20 @@ public static class T2ACraftSystem
|
|||
/// <summary>
|
||||
/// Convenience overload that resolves a Type to a CraftItem first.
|
||||
/// </summary>
|
||||
public static bool CanCraftItem(Mobile from, Type itemType, CraftSystem system, Type selectedResourceType = null)
|
||||
public static bool CanCraftItem(
|
||||
Mobile from, Type itemType, CraftSystem system, Type selectedResourceType = null, int hue = -1
|
||||
)
|
||||
{
|
||||
var itemDef = system.CraftItems.SearchFor(itemType);
|
||||
return itemDef != null && CanCraftItem(from, itemDef, system, selectedResourceType);
|
||||
return itemDef != null && CanCraftItem(from, itemDef, system, selectedResourceType, hue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters static template entries to only those the player can craft.
|
||||
/// </summary>
|
||||
public static ItemListEntry[] FilterEntries(
|
||||
Mobile from, ItemListEntry[] staticEntries, Type[] types, CraftSystem system, Type selectedResourceType = null
|
||||
Mobile from, ItemListEntry[] staticEntries, Type[] types, CraftSystem system,
|
||||
Type selectedResourceType = null, int hue = -1
|
||||
)
|
||||
{
|
||||
var filtered = new ItemListEntry[staticEntries.Length];
|
||||
|
|
@ -271,7 +303,7 @@ public static class T2ACraftSystem
|
|||
var entry = staticEntries[i];
|
||||
var typeIndex = entry.CraftIndex;
|
||||
if (typeIndex >= 0 && typeIndex < types.Length &&
|
||||
CanCraftItem(from, types[typeIndex], system, selectedResourceType))
|
||||
CanCraftItem(from, types[typeIndex], system, selectedResourceType, hue))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
|
|
@ -294,12 +326,12 @@ public static class T2ACraftSystem
|
|||
/// Returns true if at least one item in the type array is craftable.
|
||||
/// </summary>
|
||||
public static bool AnyCraftableInCategory(
|
||||
Mobile from, Type[] types, CraftSystem system, Type selectedResourceType = null
|
||||
Mobile from, Type[] types, CraftSystem system, Type selectedResourceType = null, int hue = -1
|
||||
)
|
||||
{
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
if (CanCraftItem(from, types[i], system, selectedResourceType))
|
||||
if (CanCraftItem(from, types[i], system, selectedResourceType, hue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -308,28 +340,11 @@ public static class T2ACraftSystem
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent type pairs mirroring CraftItem.m_TypesTable — used so that menu filtering
|
||||
/// counts boards when checking for logs, hides when checking for leather, etc.
|
||||
/// </summary>
|
||||
private static readonly Type[][] _equivalentTypes =
|
||||
[
|
||||
[typeof(Log), typeof(Board)],
|
||||
[typeof(Cloth), typeof(UncutCloth)],
|
||||
[typeof(Items.Leather), typeof(Hides)]
|
||||
];
|
||||
|
||||
// Shares CraftItem's equivalence table so the menu counts exactly what the craft will consume
|
||||
private static int GetResourceAmount(Container pack, Type type)
|
||||
{
|
||||
for (var i = 0; i < _equivalentTypes.Length; i++)
|
||||
{
|
||||
if (_equivalentTypes[i][0] == type)
|
||||
{
|
||||
return pack.GetAmount(_equivalentTypes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return pack.GetAmount(type);
|
||||
var equivalents = CraftItem.GetEquivalentTypes(type);
|
||||
return equivalents != null ? pack.GetAmount(equivalents) : pack.GetAmount(type);
|
||||
}
|
||||
|
||||
private static bool HasAnySufficientSubResource(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,13 @@ public class T2ACraftToolTarget : Target
|
|||
var resIndex = lastMade.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
|
||||
var type = resIndex > -1 ? res[resIndex].ItemType : null;
|
||||
|
||||
// T2A tinkering has no colored ingots; a stale index from the gump flow must not leak in
|
||||
if (_system == DefTinkering.CraftSystem)
|
||||
{
|
||||
type = typeof(IronIngot);
|
||||
T2ACraftSystem.SetLastResourceIndex(from, _system, type);
|
||||
}
|
||||
|
||||
// Jewelry requires gem targeting — re-prompt instead of crafting directly
|
||||
if (typeof(BaseJewel).IsAssignableFrom(lastMade.ItemType))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ public class TailoringMenu : ItemListMenu
|
|||
};
|
||||
|
||||
private TailoringMenu(Mobile from, BaseTool tool, Category category, int hue = -1)
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category))
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category, hue))
|
||||
{
|
||||
_tool = tool;
|
||||
_category = category;
|
||||
|
|
@ -198,11 +198,11 @@ public class TailoringMenu : ItemListMenu
|
|||
public static ItemListEntry[] Studded() => _studdedEntries ??= BuildStaticEntries(StuddedArmorTypes, "leather");
|
||||
public static ItemListEntry[] Female() => _femaleEntries ??= BuildStaticEntries(FemaleArmorTypes, "leather");
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category, int hue)
|
||||
{
|
||||
if (category is Category.Main or Category.LeatherMain)
|
||||
{
|
||||
return BuildFilteredMainEntries(from, category);
|
||||
return BuildFilteredMainEntries(from, category, hue);
|
||||
}
|
||||
|
||||
var types = GetTypes(category);
|
||||
|
|
@ -212,10 +212,10 @@ public class TailoringMenu : ItemListMenu
|
|||
return [];
|
||||
}
|
||||
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefTailoring.CraftSystem);
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefTailoring.CraftSystem, null, hue);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from, Category mainCategory)
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from, Category mainCategory, int hue)
|
||||
{
|
||||
var system = DefTailoring.CraftSystem;
|
||||
var mainStatic = GetStaticEntries(mainCategory);
|
||||
|
|
@ -231,7 +231,7 @@ public class TailoringMenu : ItemListMenu
|
|||
{
|
||||
var entry = mainStatic[i];
|
||||
var types = GetTypes((Category)entry.CraftIndex);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system, null, hue))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ public class TinkeringMenu : ItemListMenu
|
|||
};
|
||||
|
||||
private TinkeringMenu(Mobile from, BaseTool tool, Category category, Type selectedResourceType)
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category))
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category, selectedResourceType))
|
||||
{
|
||||
_tool = tool;
|
||||
_category = category;
|
||||
|
|
@ -224,16 +224,16 @@ public class TinkeringMenu : ItemListMenu
|
|||
public static ItemListEntry[] Rings() => _ringEntries ??= BuildStaticEntries(RingTypes, "ingots");
|
||||
public static ItemListEntry[] KegItems() => _kegEntries ??= BuildStaticEntries(KegItemTypes, "kegs");
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category, Type selectedResourceType)
|
||||
{
|
||||
if (category == Category.Main)
|
||||
{
|
||||
return BuildFilteredMainEntries(from);
|
||||
return BuildFilteredMainEntries(from, selectedResourceType);
|
||||
}
|
||||
|
||||
if (category == Category.Jewelry)
|
||||
{
|
||||
return BuildFilteredJewelryEntries(from);
|
||||
return BuildFilteredJewelryEntries(from, selectedResourceType);
|
||||
}
|
||||
|
||||
var types = GetTypes(category);
|
||||
|
|
@ -243,7 +243,7 @@ public class TinkeringMenu : ItemListMenu
|
|||
return [];
|
||||
}
|
||||
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefTinkering.CraftSystem);
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefTinkering.CraftSystem, selectedResourceType);
|
||||
}
|
||||
|
||||
private static readonly ItemListEntry[] JewelrySubcategoryEntries =
|
||||
|
|
@ -253,7 +253,7 @@ public class TinkeringMenu : ItemListMenu
|
|||
new("Rings", 0x108a, 0, (int)Category.Rings)
|
||||
];
|
||||
|
||||
private static ItemListEntry[] BuildFilteredJewelryEntries(Mobile from)
|
||||
private static ItemListEntry[] BuildFilteredJewelryEntries(Mobile from, Type selectedResourceType)
|
||||
{
|
||||
var system = DefTinkering.CraftSystem;
|
||||
var filtered = new ItemListEntry[JewelrySubcategoryEntries.Length];
|
||||
|
|
@ -263,7 +263,7 @@ public class TinkeringMenu : ItemListMenu
|
|||
{
|
||||
var entry = JewelrySubcategoryEntries[i];
|
||||
var types = GetTypes((Category)entry.CraftIndex);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system, selectedResourceType))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
|
|
@ -282,7 +282,7 @@ public class TinkeringMenu : ItemListMenu
|
|||
return filtered;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from, Type selectedResourceType)
|
||||
{
|
||||
var system = DefTinkering.CraftSystem;
|
||||
var mainStatic = Main();
|
||||
|
|
@ -293,7 +293,7 @@ public class TinkeringMenu : ItemListMenu
|
|||
{
|
||||
var entry = mainStatic[i];
|
||||
var types = GetTypes((Category)entry.CraftIndex);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system, selectedResourceType))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
|
|
@ -407,9 +407,17 @@ public class TinkeringMenu : ItemListMenu
|
|||
return true;
|
||||
}
|
||||
|
||||
if (targeted is BaseIngot)
|
||||
if (targeted is BaseIngot and not IronIngot)
|
||||
{
|
||||
var menu = new TinkeringMenu(from, tool, Category.Main, targeted.GetType());
|
||||
// T2A tinkering predates colored ingots; metal items are always made from iron.
|
||||
from.SendAsciiMessage("Tinkering can only use iron ingots. Please select iron ingots or wood.");
|
||||
from.Target = new ResourceSelectTarget(from, tool);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (targeted is IronIngot)
|
||||
{
|
||||
var menu = new TinkeringMenu(from, tool, Category.Main, typeof(IronIngot));
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
|
||||
|
|
|
|||
|
|
@ -10,61 +10,77 @@ namespace Server.Engines.Harvest
|
|||
{
|
||||
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)),
|
||||
new(90.0, 80.0, 4080.0, true, Expansion.T2A, typeof(TreasureMap)),
|
||||
new(100.0, 80.0, 4080.0, true, Expansion.T2A, typeof(MessageInABottle)),
|
||||
new(
|
||||
0.0,
|
||||
125.0,
|
||||
-2375.0,
|
||||
false,
|
||||
Expansion.T2A,
|
||||
typeof(PrizedFish),
|
||||
typeof(WondrousFish),
|
||||
typeof(TrulyRareFish),
|
||||
typeof(PeculiarFish)
|
||||
),
|
||||
new(0.0, 105.0, -420.0, false, Expansion.T2A, typeof(Boots), typeof(Shoes), typeof(Sandals), typeof(ThighBoots)),
|
||||
new(0.0, 200.0, -200.0, false, Expansion.None, new Type[] { null })
|
||||
};
|
||||
// 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 = {
|
||||
private static readonly int[] bodyParts =
|
||||
[
|
||||
0x1CDD, 0x1CE5, // arm
|
||||
0x1CE0, 0x1CE8, // torso
|
||||
0x1CE1, 0x1CE9, // head
|
||||
0x1CE2, 0x1CEC // leg
|
||||
};
|
||||
];
|
||||
|
||||
private static readonly int[] boneParts = {
|
||||
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 = {
|
||||
private static readonly int[] miscItems =
|
||||
[
|
||||
0x1EB5, // unfinished barrel
|
||||
0xA2A, // stool
|
||||
0xC1F, // broken clock
|
||||
0x1047, 0x1048, // globe
|
||||
0x1EB1, 0x1EB2, 0x1EB3, 0x1EB4 // barrel staves
|
||||
};
|
||||
];
|
||||
|
||||
private Fishing()
|
||||
{
|
||||
|
|
@ -83,9 +99,9 @@ namespace Server.Engines.Harvest
|
|||
MaxRange = 4,
|
||||
ConsumedPerHarvest = 1,
|
||||
ConsumedPerFeluccaHarvest = 1,
|
||||
EffectActions = new[] { 12 },
|
||||
EffectSounds = Array.Empty<int>(),
|
||||
EffectCounts = new[] { 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.
|
||||
|
|
@ -96,29 +112,23 @@ namespace Server.Engines.Harvest
|
|||
ToolBrokeMessage = 503174 // You broke your fishing pole.
|
||||
};
|
||||
|
||||
HarvestResource[] res =
|
||||
{
|
||||
new(00.0, 00.0, 100.0, 1043297, typeof(Fish))
|
||||
};
|
||||
HarvestResource[] res = [new(00.0, 00.0, 100.0, 1043297, typeof(Fish))];
|
||||
|
||||
HarvestVein[] veins =
|
||||
{
|
||||
new(1000, 0.0, res[0], null)
|
||||
};
|
||||
HarvestVein[] veins = [new(1000, 0.0, res[0], null)];
|
||||
|
||||
fish.Resources = res;
|
||||
fish.Veins = veins;
|
||||
|
||||
if (Core.ML)
|
||||
{
|
||||
fish.BonusResources = new[]
|
||||
{
|
||||
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 = new[] { fish };
|
||||
Definitions = [fish];
|
||||
}
|
||||
|
||||
public static Fishing System => _system ??= new Fishing();
|
||||
|
|
@ -160,6 +170,13 @@ namespace Server.Engines.Harvest
|
|||
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
|
||||
|
|
@ -174,7 +191,7 @@ namespace Server.Engines.Harvest
|
|||
{
|
||||
var entry = _mutateTable[i];
|
||||
|
||||
if (Core.Expansion < entry.m_Expansion)
|
||||
if (Core.Expansion < entry.m_Expansion || Core.Expansion > entry.m_MaxExpansion)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -198,11 +215,14 @@ namespace Server.Engines.Harvest
|
|||
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)
|
||||
if (pack != null && CanFishUpShipwreck(from))
|
||||
{
|
||||
foreach (var sos in pack.FindItemsByType<SOS>())
|
||||
{
|
||||
|
|
@ -239,91 +259,67 @@ namespace Server.Engines.Harvest
|
|||
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)
|
||||
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))
|
||||
if (from.Map != sos.TargetMap || !from.InRange(sos.TargetLocation, 60))
|
||||
{
|
||||
Item preLoot = null;
|
||||
|
||||
switch (Utility.Random(8))
|
||||
{
|
||||
case 0: // Body parts
|
||||
{
|
||||
preLoot = new ShipwreckedItem(bodyParts.RandomElement());
|
||||
break;
|
||||
}
|
||||
case 1: // Bone parts
|
||||
{
|
||||
preLoot = new ShipwreckedItem(boneParts.RandomElement());
|
||||
break;
|
||||
}
|
||||
case 2: // Paintings and portraits
|
||||
{
|
||||
preLoot = new ShipwreckedItem(Utility.Random(0xE9F, 10));
|
||||
break;
|
||||
}
|
||||
case 3: // Pillows
|
||||
{
|
||||
preLoot = new ShipwreckedItem(Utility.Random(0x13A4, 11));
|
||||
break;
|
||||
}
|
||||
case 4: // Shells
|
||||
{
|
||||
preLoot = new ShipwreckedItem(Utility.Random(0xFC4, 9));
|
||||
break;
|
||||
}
|
||||
case 5: // Hats
|
||||
{
|
||||
preLoot = Utility.RandomBool() ? new SkullCap() : new TricorneHat();
|
||||
break;
|
||||
}
|
||||
case 6: // Misc
|
||||
{
|
||||
var rand = Utility.Random(miscItems.Length + 1);
|
||||
|
||||
preLoot = rand == miscItems.Length
|
||||
? new Candelabra()
|
||||
: new ShipwreckedItem(miscItems[rand]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (preLoot != null)
|
||||
{
|
||||
((IShipwreckedItem)preLoot).IsShipwreckedItem = true;
|
||||
return preLoot;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,45 +330,7 @@ namespace Server.Engines.Harvest
|
|||
{
|
||||
if (item is TreasureMap or MessageInABottle or SpecialFishingNet)
|
||||
{
|
||||
BaseCreature serp;
|
||||
|
||||
if (Utility.RandomDouble() < 0.25)
|
||||
{
|
||||
serp = new DeepSeaSerpent();
|
||||
}
|
||||
else
|
||||
{
|
||||
serp = 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.PackItem(item);
|
||||
|
||||
m.SendLocalizedMessage(503170); // Uh oh! That doesn't look like a fish!
|
||||
SurfaceSerpent(m).PackItem(item);
|
||||
|
||||
return true; // we don't want to give the item to the player, it's on the serpent
|
||||
}
|
||||
|
|
@ -380,6 +338,52 @@ namespace Server.Engines.Harvest
|
|||
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)
|
||||
|
|
@ -401,12 +405,7 @@ namespace Server.Engines.Harvest
|
|||
number = 1008124;
|
||||
name = "a mess of small fish";
|
||||
}
|
||||
else if (item is Fish)
|
||||
{
|
||||
number = 1008124;
|
||||
name = item.ItemData.Name;
|
||||
}
|
||||
else if (item is BaseShoes)
|
||||
else if (item is Fish or BaseShoes)
|
||||
{
|
||||
number = 1008124;
|
||||
name = item.ItemData.Name;
|
||||
|
|
@ -467,7 +466,7 @@ namespace Server.Engines.Harvest
|
|||
TimeSpan.FromSeconds(1.5),
|
||||
() =>
|
||||
{
|
||||
if (Core.ML)
|
||||
if (Core.T2A)
|
||||
{
|
||||
from.RevealingAction();
|
||||
}
|
||||
|
|
@ -486,10 +485,18 @@ namespace Server.Engines.Harvest
|
|||
{
|
||||
base.OnHarvestFinished(from, tool, def, vein, bank, resource, harvested);
|
||||
|
||||
if (Core.ML)
|
||||
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;
|
||||
|
|
@ -541,6 +548,7 @@ namespace Server.Engines.Harvest
|
|||
{
|
||||
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;
|
||||
|
|
@ -548,6 +556,13 @@ namespace Server.Engines.Harvest
|
|||
|
||||
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;
|
||||
|
|
@ -555,6 +570,7 @@ namespace Server.Engines.Harvest
|
|||
m_MaxSkill = maxSkill;
|
||||
m_DeepWater = deepWater;
|
||||
m_Expansion = expansion;
|
||||
m_MaxExpansion = maxExpansion;
|
||||
m_Types = types;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,16 +216,22 @@ internal static class StepCacheFile
|
|||
var path = Core.FindDataFile("tiledata.mul", false);
|
||||
if (path != null)
|
||||
{
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var hasher = HashUtility.CreateXxHash3();
|
||||
hasher.Append(fs);
|
||||
_tileDataFileFingerprint = hasher.GetCurrentHashAsUInt64();
|
||||
_tileDataFileFingerprint = HashTileDataFile(path);
|
||||
}
|
||||
|
||||
_tileDataFileFingerprintComputed = true;
|
||||
return _tileDataFileFingerprint;
|
||||
}
|
||||
|
||||
/// <summary>XxHash3 of the bytes of the tiledata file at <paramref name="path"/>.</summary>
|
||||
internal static ulong HashTileDataFile(string path)
|
||||
{
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var hasher = HashUtility.CreateXxHash3();
|
||||
hasher.Append(fs);
|
||||
return hasher.GetCurrentHashAsUInt64();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the map's chunks to <paramref name="path"/>: header, then one record per chunk, then
|
||||
/// the index trailer. IndexOffset isn't known until the records are down, so it goes in as a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue