#W# Merge: Merged with upstream. Conflicts resolved.
This commit is contained in:
commit
f6f50470a9
276 changed files with 6742 additions and 8845 deletions
|
|
@ -224,9 +224,17 @@ public static class ManagedArchive
|
|||
{
|
||||
using var tarReader = new TarReader(stream);
|
||||
|
||||
var root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(outputDirectory)) + Path.DirectorySeparatorChar;
|
||||
var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
|
||||
while (tarReader.GetNextEntry() is { } entry)
|
||||
{
|
||||
var destPath = Path.Combine(outputDirectory, entry.Name);
|
||||
// Rooted names and ".." segments would otherwise let the archive write anywhere the process can.
|
||||
var destPath = Path.GetFullPath(Path.Combine(root, entry.Name));
|
||||
if (!destPath.StartsWith(root, comparison))
|
||||
{
|
||||
throw new InvalidDataException($"Archive entry '{entry.Name}' resolves outside '{root}'");
|
||||
}
|
||||
|
||||
var destDir = Path.GetDirectoryName(destPath);
|
||||
if (destDir != null)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using ModernUO.Serialization;
|
||||
using Server.Engines.Craft.T2A;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public abstract partial class CookableFood : Item
|
||||
public abstract partial class CookableFood : Item, IT2ACombinable
|
||||
{
|
||||
[SerializableField(0)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
|
|
@ -14,6 +15,21 @@ public abstract partial class CookableFood : Item
|
|||
|
||||
public abstract Food Cook();
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
T2ACooking.Prompt(from, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnDoubleClick(from);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCombine(Mobile from, object target) =>
|
||||
IsHeatSource(target) ? T2ACooking.BeginHeat(from, this, target) : T2ACooking.CannotCombine(from);
|
||||
|
||||
public static bool IsHeatSource(object targeted)
|
||||
{
|
||||
int itemID;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
using System;
|
||||
using ModernUO.Serialization;
|
||||
using Server.Engines.Craft.T2A;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class Dough : Item
|
||||
public partial class Dough : Item, IT2ACombinable
|
||||
{
|
||||
[Constructible]
|
||||
public Dough() : base(0x103d)
|
||||
|
|
@ -14,10 +15,68 @@ public partial class Dough : Item
|
|||
}
|
||||
|
||||
public override double DefaultWeight => 1.0;
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
T2ACooking.Prompt(from, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnDoubleClick(from);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCombine(Mobile from, object target)
|
||||
{
|
||||
if (CookableFood.IsHeatSource(target))
|
||||
{
|
||||
return T2ACooking.BeginHeat(from, this, target);
|
||||
}
|
||||
|
||||
if (target is not Item item)
|
||||
{
|
||||
return T2ACooking.CannotCombine(from);
|
||||
}
|
||||
|
||||
if (!T2ACooking.ValidateInPack(from, item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Item result = item switch
|
||||
{
|
||||
JarHoney => new SweetDough(),
|
||||
Pear => new UnbakedFruitPie(),
|
||||
Peach => new UnbakedPeachCobbler(),
|
||||
Apple => new UnbakedApplePie(),
|
||||
Pumpkin or SmallPumpkin => new UnbakedPumpkinPie(),
|
||||
Eggs => new UnbakedQuiche(),
|
||||
Ham or Spam or CookedBird or FishSteak => new UnbakedMeatPie(),
|
||||
Sausage => new UncookedSausagePizza(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return T2ACooking.CannotCombine(from);
|
||||
}
|
||||
|
||||
return T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
item,
|
||||
result,
|
||||
item is JarHoney
|
||||
? "You mix the dough with honey to make sweet dough."
|
||||
: "You combine the dough with the ingredient."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class SweetDough : Item
|
||||
public partial class SweetDough : Item, IT2ACombinable
|
||||
{
|
||||
[Constructible]
|
||||
public SweetDough() : base(0x103d)
|
||||
|
|
@ -28,10 +87,79 @@ public partial class SweetDough : Item
|
|||
|
||||
public override double DefaultWeight => 1.0;
|
||||
public override int LabelNumber => 1041340; // sweet dough
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
T2ACooking.Prompt(from, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnDoubleClick(from);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCombine(Mobile from, object target)
|
||||
{
|
||||
if (CookableFood.IsHeatSource(target))
|
||||
{
|
||||
return T2ACooking.BeginHeat(from, this, target);
|
||||
}
|
||||
|
||||
switch (target)
|
||||
{
|
||||
case JarHoney honey:
|
||||
{
|
||||
return T2ACooking.ValidateInPack(from, honey) && T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
honey,
|
||||
new CookieMix(),
|
||||
"You mix the sweet dough with honey to make cookie mix."
|
||||
);
|
||||
}
|
||||
case SackFlour sack:
|
||||
{
|
||||
if (!T2ACooking.ValidateInPack(from, sack))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sack.IsOpen)
|
||||
{
|
||||
from.SendAsciiMessage("You must open the sack of flour first.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
sack,
|
||||
new CakeMix(),
|
||||
"You mix the flour with sweet dough to make cake mix."
|
||||
);
|
||||
}
|
||||
case BowlFlour bowl:
|
||||
{
|
||||
return T2ACooking.ValidateInPack(from, bowl) && T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
bowl,
|
||||
new CakeMix(),
|
||||
"You mix the flour with sweet dough to make cake mix."
|
||||
);
|
||||
}
|
||||
default:
|
||||
{
|
||||
return T2ACooking.CannotCombine(from);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class JarHoney : Item
|
||||
public partial class JarHoney : Item, IT2ACombinable
|
||||
{
|
||||
[Constructible]
|
||||
public JarHoney() : base(0x9ec)
|
||||
|
|
@ -40,10 +168,44 @@ public partial class JarHoney : Item
|
|||
}
|
||||
|
||||
public override double DefaultWeight => 1.0;
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
T2ACooking.Prompt(from, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnDoubleClick(from);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCombine(Mobile from, object target)
|
||||
{
|
||||
return target switch
|
||||
{
|
||||
Dough dough => T2ACooking.ValidateInPack(from, dough) && T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
dough,
|
||||
new SweetDough(),
|
||||
"You mix the dough with honey to make sweet dough."
|
||||
),
|
||||
SweetDough sweetDough => T2ACooking.ValidateInPack(from, sweetDough) && T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
sweetDough,
|
||||
new CookieMix(),
|
||||
"You mix the sweet dough with honey to make cookie mix."
|
||||
),
|
||||
_ => T2ACooking.CannotCombine(from)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class BowlFlour : Item
|
||||
public partial class BowlFlour : Item, IT2ACombinable
|
||||
{
|
||||
[Constructible]
|
||||
public BowlFlour() : base(0xa1e)
|
||||
|
|
@ -51,6 +213,41 @@ public partial class BowlFlour : Item
|
|||
}
|
||||
|
||||
public override double DefaultWeight => 1.0;
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
T2ACooking.Prompt(from, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnDoubleClick(from);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCombine(Mobile from, object target)
|
||||
{
|
||||
return target switch
|
||||
{
|
||||
SweetDough sweetDough => T2ACooking.ValidateInPack(from, sweetDough) && T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
sweetDough,
|
||||
new CakeMix(),
|
||||
"You mix the flour with sweet dough to make cake mix."
|
||||
),
|
||||
Item item when T2ACooking.GetWaterSource(item) is { } water =>
|
||||
T2ACooking.CanReachWater(from, item) && T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
water,
|
||||
new Dough(),
|
||||
"You mix the flour with water to make dough."
|
||||
),
|
||||
_ => T2ACooking.CannotCombine(from)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
|
|
@ -66,7 +263,7 @@ public partial class WoodenBowl : Item
|
|||
|
||||
[TypeAlias("Server.Items.SackFlourOpen")]
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class SackFlour : Item, IHasQuantity
|
||||
public partial class SackFlour : Item, IHasQuantity, IT2ACombinable
|
||||
{
|
||||
[Constructible]
|
||||
public SackFlour() : base(0x1039)
|
||||
|
|
@ -104,7 +301,52 @@ public partial class SackFlour : Item, IHasQuantity
|
|||
{
|
||||
++ItemID;
|
||||
}
|
||||
|
||||
if (!T2ACraftSystem.Enabled || !T2ACooking.ValidateInPack(from, this))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsOpen)
|
||||
{
|
||||
T2ACooking.Prompt(from, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendAsciiMessage("You must open the sack of flour first.");
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCombine(Mobile from, object target)
|
||||
{
|
||||
if (!IsOpen)
|
||||
{
|
||||
from.SendAsciiMessage("You must open the sack of flour first.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return target switch
|
||||
{
|
||||
SweetDough sweetDough => T2ACooking.ValidateInPack(from, sweetDough) && T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
sweetDough,
|
||||
new CakeMix(),
|
||||
"You mix the flour with sweet dough to make cake mix."
|
||||
),
|
||||
Item item when T2ACooking.GetWaterSource(item) is { } water =>
|
||||
T2ACooking.CanReachWater(from, item) && T2ACooking.Combine(
|
||||
from,
|
||||
this,
|
||||
water,
|
||||
new Dough(),
|
||||
"You mix the flour with water to make dough."
|
||||
),
|
||||
_ => T2ACooking.CannotCombine(from)
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsOpen => ItemID is not (0x1039 or 0x1045);
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public partial class BigFish : Item, ICarvable
|
|||
public BigFish() : base(0x09CC)
|
||||
{
|
||||
// TODO: Find correct formula. max on OSI currently 200, OSI dev says it's not 200 as max, and ~ 1/1,000,000 chance to get highest
|
||||
Weight = Utility.RandomMinMax(3, 200);
|
||||
Weight = Core.SE ? Utility.RandomMinMax(3, 200) : 100;
|
||||
Hue = Utility.RandomBool() ? 0x847 : 0x58C;
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +23,8 @@ public partial class BigFish : Item, ICarvable
|
|||
|
||||
public void Carve(Mobile from, Item item)
|
||||
{
|
||||
ScissorHelper(from, new RawFishSteak(), Math.Max(16, (int)Weight) / 4, false);
|
||||
var steaks = Core.SE ? Math.Max(4, (int)Weight / 5) : Math.Max(16, (int)Weight) / 4;
|
||||
ScissorHelper(from, new RawFishSteak(), steaks, false);
|
||||
}
|
||||
|
||||
public override void GetProperties(IPropertyList list)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ public abstract partial class BaseMagicFish : Item
|
|||
|
||||
public virtual bool Apply(Mobile from)
|
||||
{
|
||||
var applied = SpellHelper.AddStatOffset(from, Type, Bonus, TimeSpan.FromMinutes(1.0));
|
||||
var duration = Core.SE ? TimeSpan.FromSeconds(12.0) : TimeSpan.FromMinutes(1.0);
|
||||
var applied = SpellHelper.AddStatOffset(from, Type, Bonus, duration);
|
||||
|
||||
if (!applied)
|
||||
{
|
||||
|
|
@ -50,7 +51,7 @@ public partial class PrizedFish : BaseMagicFish
|
|||
{
|
||||
}
|
||||
|
||||
public override int Bonus => 5;
|
||||
public override int Bonus => Core.SE ? 1 : 5;
|
||||
public override StatType Type => StatType.Int;
|
||||
|
||||
public override int LabelNumber => 1041073; // prized fish
|
||||
|
|
@ -64,7 +65,7 @@ public partial class WondrousFish : BaseMagicFish
|
|||
{
|
||||
}
|
||||
|
||||
public override int Bonus => 5;
|
||||
public override int Bonus => Core.SE ? 1 : 5;
|
||||
public override StatType Type => StatType.Dex;
|
||||
|
||||
public override int LabelNumber => 1041074; // wondrous fish
|
||||
|
|
@ -78,7 +79,7 @@ public partial class TrulyRareFish : BaseMagicFish
|
|||
{
|
||||
}
|
||||
|
||||
public override int Bonus => 5;
|
||||
public override int Bonus => Core.SE ? 1 : 5;
|
||||
public override StatType Type => StatType.Str;
|
||||
|
||||
public override int LabelNumber => 1041075; // truly rare fish
|
||||
|
|
|
|||
|
|
@ -12,12 +12,6 @@ public partial class SOS : Item
|
|||
// By default let's use the old gump, because we can.
|
||||
private static bool UseNewMessages = false;
|
||||
|
||||
private static int[] WaterTiles =
|
||||
{
|
||||
0x00A8, 0x00AB,
|
||||
0x0136, 0x0137
|
||||
};
|
||||
|
||||
private static Rectangle2D[] BritRegions = { new(0, 0, 5120, 4096) };
|
||||
|
||||
private static Rectangle2D[] IlshRegions =
|
||||
|
|
@ -179,34 +173,7 @@ public partial class SOS : Item
|
|||
var x = Utility.Random(reg.X, reg.Width);
|
||||
var y = Utility.Random(reg.Y, reg.Height);
|
||||
|
||||
if (!ValidateDeepWater(map, x, y))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var valid = true;
|
||||
|
||||
for (int j = 1, offset = 5; valid && j <= 5; ++j, offset += 5)
|
||||
{
|
||||
if (!ValidateDeepWater(map, x + offset, y + offset))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
else if (!ValidateDeepWater(map, x + offset, y - offset))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
else if (!ValidateDeepWater(map, x - offset, y + offset))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
else if (!ValidateDeepWater(map, x - offset, y - offset))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (valid)
|
||||
if (SpecialFishingNet.FullValidation(map, x, y))
|
||||
{
|
||||
return new Point3D(x, y, 0);
|
||||
}
|
||||
|
|
@ -215,19 +182,6 @@ public partial class SOS : Item
|
|||
return Point3D.Zero;
|
||||
}
|
||||
|
||||
private static bool ValidateDeepWater(Map map, int x, int y)
|
||||
{
|
||||
var tileID = map.Tiles.GetLandTile(x, y).ID;
|
||||
var water = false;
|
||||
|
||||
for (var i = 0; !water && i < WaterTiles.Length; i += 2)
|
||||
{
|
||||
water = tileID >= WaterTiles[i] && tileID <= WaterTiles[i + 1];
|
||||
}
|
||||
|
||||
return water;
|
||||
}
|
||||
|
||||
private class MessageGump : StaticGump<MessageGump>
|
||||
{
|
||||
private readonly string _fmt;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ namespace Server.Items;
|
|||
[SerializationGenerator(0, false)]
|
||||
public partial class SpecialFishingNet : Item
|
||||
{
|
||||
private static int[] _hues =
|
||||
{
|
||||
private static readonly int[] _hues =
|
||||
[
|
||||
0x09B,
|
||||
0x0CD,
|
||||
0x0D3,
|
||||
|
|
@ -26,25 +26,19 @@ public partial class SpecialFishingNet : Item
|
|||
0x492,
|
||||
0x4B5,
|
||||
0x8AA
|
||||
};
|
||||
];
|
||||
|
||||
private static int[] _waterTiles =
|
||||
{
|
||||
0x00A8, 0x00AB,
|
||||
0x0136, 0x0137
|
||||
};
|
||||
private static readonly int[] _waterTiles = [0x00A8, 0x00AB, 0x0136, 0x0137 ];
|
||||
|
||||
private static int[] _shallowWaterTiles =
|
||||
{
|
||||
0x1797, 0x179C
|
||||
};
|
||||
private static readonly int[] _shallowWaterTiles = [0x1797, 0x179C];
|
||||
|
||||
[SerializableField(0)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private bool _inUse;
|
||||
|
||||
[Constructible]
|
||||
public SpecialFishingNet() : base(0x0DCA) => Hue = Utility.RandomDouble() < 0.01 ? _hues.RandomElement() : 0x8A0;
|
||||
public SpecialFishingNet() : base(0x0DCA) =>
|
||||
Hue = Core.LBR && Utility.RandomDouble() < 0.1 ? _hues.RandomElement() : 0x8A0;
|
||||
|
||||
public override double DefaultWeight => 1.0;
|
||||
|
||||
|
|
@ -322,31 +316,15 @@ public partial class SpecialFishingNet : Item
|
|||
|
||||
public static bool FullValidation(Map map, int x, int y)
|
||||
{
|
||||
if (!ValidateDeepWater(map, x, y))
|
||||
// Every tile closer than 14 must be water, so land and the edge of the map are at least 14 tiles away.
|
||||
for (var ty = y - 13; ty <= y + 13; ++ty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int j = 1, offset = 5; j <= 5; ++j, offset += 5)
|
||||
{
|
||||
if (!ValidateDeepWater(map, x + offset, y + offset))
|
||||
for (var tx = x - 13; tx <= x + 13; ++tx)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateDeepWater(map, x + offset, y - offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateDeepWater(map, x - offset, y + offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateDeepWater(map, x - offset, y - offset))
|
||||
{
|
||||
return false;
|
||||
if (!ValidateDeepWater(map, tx, ty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem
|
|||
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
|
||||
private AosSkillBonuses _skillBonuses;
|
||||
|
||||
[InvalidateProperties]
|
||||
[SerializableField(8)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private int _spellCount;
|
||||
|
|
@ -135,7 +136,6 @@ public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem
|
|||
|
||||
[SerializableField(7, fieldChanged: nameof(OnContentChanged))]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
[InvalidateProperties]
|
||||
private ulong _content;
|
||||
|
||||
private void OnContentChanged(ulong oldValue, ulong newValue)
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ public abstract partial class BaseTool : Item, IUsesRemaining, ICraftable
|
|||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
}
|
||||
else if (T2ACraftSystem.Enabled)
|
||||
else if (T2ACraftSystem.Enabled && T2ACraftSystem.HasMenu(system))
|
||||
{
|
||||
from.Target = new T2ACraftToolTarget(this, system);
|
||||
from.SendAsciiMessage("Target this tool to make last item, or any other target to begin crafting.");
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ namespace Server.Items
|
|||
|
||||
public override bool ValidatesDuringHit => false;
|
||||
|
||||
// Mana is only checked here; OnHit/OnMiss charge it, since they run only for a shot that actually fired.
|
||||
public override bool OnBeforeSwing(Mobile attacker, Mobile defender) =>
|
||||
Validate(attacker) && CheckMana(attacker, true);
|
||||
Validate(attacker) && CheckMana(attacker, false);
|
||||
|
||||
public override void OnMiss(Mobile attacker, Mobile defender)
|
||||
{
|
||||
|
|
@ -20,7 +21,10 @@ namespace Server.Items
|
|||
|
||||
ClearCurrentAbility(attacker);
|
||||
|
||||
attacker.SendLocalizedMessage(1060089); // You fail to execute your special move
|
||||
if (CheckMana(attacker, true))
|
||||
{
|
||||
attacker.SendLocalizedMessage(1060089); // You fail to execute your special move
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnHit(Mobile attacker, Mobile defender, int damage, WorldLocation worldLocation)
|
||||
|
|
@ -29,7 +33,10 @@ namespace Server.Items
|
|||
|
||||
ClearCurrentAbility(attacker);
|
||||
|
||||
attacker.SendLocalizedMessage(1060216); // Your shot was successful
|
||||
if (CheckMana(attacker, true))
|
||||
{
|
||||
attacker.SendLocalizedMessage(1060216); // Your shot was successful
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ namespace Server.Items
|
|||
var result = base.OnCraft(quality, makersMark, from, craftSystem, typeRes, tool, craftItem, resHue);
|
||||
if (Core.ML)
|
||||
{
|
||||
ApplyWoodBonuses(typeRes);
|
||||
ApplyWoodBonuses(typeRes ?? craftItem.Resources[0].ItemType);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
199
Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v12.json
generated
Normal file
199
Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v12.json
generated
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
{
|
||||
"version": 12,
|
||||
"type": "Server.Engines.CannedEvil.ChampionSpawn",
|
||||
"properties": [
|
||||
{
|
||||
"name": "Level",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ActivatedByProximity",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NextProximityTime",
|
||||
"type": "System.DateTime",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"AnchoredTime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MaxLevel",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ActivatedByValor",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DamageEntries",
|
||||
"type": "System.Collections.Generic.Dictionary\u003CServer.Mobile, int\u003E",
|
||||
"rule": "DictionaryMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Server.Mobile",
|
||||
"SerializableInterfaceMigrationRule",
|
||||
"0",
|
||||
"int",
|
||||
"PrimitiveTypeMigrationRule",
|
||||
"1",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ConfinedRoaming",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Idol",
|
||||
"type": "Server.Engines.CannedEvil.IdolOfTheChampion",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "HasBeenAdvanced",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SpawnArea",
|
||||
"type": "Server.Rectangle2D",
|
||||
"rule": "PrimitiveUOTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Rect2D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RandomizeType",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Kills",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Active",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
"type": "Server.Engines.CannedEvil.ChampionSpawnType",
|
||||
"rule": "EnumMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Creatures",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"@Tidy",
|
||||
"Server.Mobile",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RedSkulls",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"@Tidy",
|
||||
"Server.Item",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "WhiteSkulls",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"@Tidy",
|
||||
"Server.Item",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Platform",
|
||||
"type": "Server.Engines.CannedEvil.ChampionPlatform",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Altar",
|
||||
"type": "Server.Engines.CannedEvil.ChampionAltar",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "ExpireDelay",
|
||||
"type": "System.TimeSpan",
|
||||
"rule": "PrimitiveTypeMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "ExpireTime",
|
||||
"type": "System.DateTime",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"AnchoredTime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Champion",
|
||||
"type": "Server.Mobile",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "RestartDelay",
|
||||
"type": "System.TimeSpan",
|
||||
"rule": "PrimitiveTypeMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "RestartTime",
|
||||
"type": "System.DateTime",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"AnchoredTime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Minions",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"@Tidy",
|
||||
"Server.Mobile",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ namespace Server.Misc
|
|||
|
||||
private static bool Enabled;
|
||||
private static bool SaveBackup;
|
||||
private static bool RestartServer; // Disable this if using a daemon/service
|
||||
private static bool RestartServer; // Under a daemon/service, set server.restartExitCode rather than disabling this
|
||||
private static bool GenerateReport;
|
||||
|
||||
public static void Configure()
|
||||
|
|
|
|||
|
|
@ -736,7 +736,7 @@ namespace Server.Items
|
|||
/// </summary>
|
||||
public static CraftResource GetFromType(Type resourceType)
|
||||
{
|
||||
if (m_TypeTable == null)
|
||||
if (resourceType == null || m_TypeTable == null)
|
||||
{
|
||||
return CraftResource.None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,6 +262,11 @@ namespace Server.Misc
|
|||
|
||||
private static void SendKarmaMessage(Mobile m, int offset)
|
||||
{
|
||||
if (offset == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = offset switch
|
||||
{
|
||||
> 40 => 1019062, // You have gained a lot of karma.
|
||||
|
|
|
|||
|
|
@ -616,8 +616,8 @@ public class MageAI : BaseAI
|
|||
}
|
||||
|
||||
var del = ScaleBySkill(3.0, SkillName.Magery);
|
||||
var min = 6.0 - del * 0.75;
|
||||
var max = 6.0 - del * 1.25;
|
||||
var min = 6.0 - del * 1.25;
|
||||
var max = 6.0 - del * 0.75;
|
||||
|
||||
return TimeSpan.FromSeconds(min + (max - min) * Utility.RandomDouble());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ModernUO.CodeGeneratedEvents;
|
||||
|
||||
namespace Server.Mobiles;
|
||||
|
|
@ -9,8 +8,6 @@ namespace Server.Mobiles;
|
|||
/// </summary>
|
||||
public abstract class MonsterAbility
|
||||
{
|
||||
private Dictionary<BaseCreature, long> _nextTriggerTicks;
|
||||
|
||||
public virtual MonsterAbilityType AbilityType => MonsterAbilityType.Generic;
|
||||
|
||||
public virtual MonsterAbilityTrigger AbilityTrigger => MonsterAbilityTrigger.None;
|
||||
|
|
@ -30,18 +27,9 @@ public abstract class MonsterAbility
|
|||
return false;
|
||||
}
|
||||
|
||||
if (_nextTriggerTicks?.TryGetValue(source, out var nextTrigger) == true)
|
||||
if (source.IsAbilityOnCooldown(this))
|
||||
{
|
||||
if (nextTrigger - Core.TickCount > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_nextTriggerTicks.Remove(source);
|
||||
if (_nextTriggerTicks.Count == 0)
|
||||
{
|
||||
_nextTriggerTicks = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var c = ChanceToTrigger;
|
||||
|
|
@ -65,10 +53,7 @@ public abstract class MonsterAbility
|
|||
? MinTriggerCooldown
|
||||
: Utility.RandomMinMax(MinTriggerCooldown, MaxTriggerCooldown);
|
||||
|
||||
var nextTrigger = Core.TickCount + (long)randomAmount.TotalMilliseconds;
|
||||
|
||||
_nextTriggerTicks ??= new Dictionary<BaseCreature, long>();
|
||||
_nextTriggerTicks[source] = nextTrigger;
|
||||
source.SetAbilityCooldown(this, randomAmount);
|
||||
}
|
||||
|
||||
public virtual void AlterMeleeDamageFrom(BaseCreature source, Mobile target, ref int damage)
|
||||
|
|
@ -100,18 +85,5 @@ public abstract class MonsterAbility
|
|||
}
|
||||
|
||||
[OnEvent(nameof(CreatureEvents.CreatureDeathEvent))]
|
||||
[OnEvent(nameof(CreatureEvents.CreatureDeletedEvent))]
|
||||
public static void InvalidateNextAbilityTriggers(BaseCreature source)
|
||||
{
|
||||
var abilities = source.GetMonsterAbilities();
|
||||
if (abilities == null || abilities.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < abilities.Length; i++)
|
||||
{
|
||||
abilities[i]._nextTriggerTicks?.Remove(source);
|
||||
}
|
||||
}
|
||||
public static void ResetCooldownsOnDeath(BaseCreature source) => source.ClearAbilityCooldowns();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ public class SummonPixiesCounter : MonsterAbility
|
|||
var amount = Utility.RandomMinMax(3, 6);
|
||||
for (var i = 0; i < amount; i++)
|
||||
{
|
||||
var pixie = new Pixie { Team = source.Team, FightMode = FightMode.Closest };
|
||||
pixie.MoveToWorld(source.Map.GetRandomNearbyLocation(source.Location), source.Map);
|
||||
pixie.Combatant = target;
|
||||
if (!source.SpawnMinion(new Pixie { FightMode = FightMode.Closest }, target))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
base.Trigger(trigger, source, target);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using ModernUO.Serialization;
|
||||
using Server.Collections;
|
||||
using Server.ContextMenus;
|
||||
using Server.Engines.CannedEvil;
|
||||
using Server.Engines.ConPVP;
|
||||
using Server.Engines.MLQuests;
|
||||
using Server.Engines.Quests.Doom;
|
||||
|
|
@ -1315,6 +1316,8 @@ namespace Server.Mobiles
|
|||
|
||||
public virtual int TreasureMapLevel => -1;
|
||||
|
||||
public virtual double TreasureMapChance => TreasureMap.LootChance;
|
||||
|
||||
public virtual bool IgnoreYoungProtection => false;
|
||||
|
||||
public bool NoKillAwards { get; set; }
|
||||
|
|
@ -1503,10 +1506,47 @@ namespace Server.Mobiles
|
|||
public List<MLQuest> MLQuests =>
|
||||
(m_MLQuests ??= StaticMLQuester ? MLQuestSystem.FindQuestList(GetType()) : ConstructQuestList()) ?? MLQuestSystem.EmptyList;
|
||||
|
||||
/// <summary>
|
||||
/// The abilities this creature has. Must be a pure, stable query: it is called from lookups, damage
|
||||
/// calculations and cleanup, so it must not spawn, mutate state, or change with the creature's state.
|
||||
/// Gate abilities that should only work some of the time with <see cref="CanUseAbility" />.
|
||||
/// </summary>
|
||||
public virtual MonsterAbility[] GetMonsterAbilities() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <paramref name="ability" /> may trigger or be looked up right now. Must be a pure read.
|
||||
/// </summary>
|
||||
public virtual bool CanUseAbility(MonsterAbility ability) => true;
|
||||
|
||||
private MonsterAbilityTrigger _activeTriggers;
|
||||
|
||||
// Deadlines in Core.TickCount, keyed by ability. Lives on the creature so it is collected with it.
|
||||
private Dictionary<MonsterAbility, long> _abilityCooldowns;
|
||||
|
||||
public bool IsAbilityOnCooldown(MonsterAbility ability)
|
||||
{
|
||||
if (_abilityCooldowns?.TryGetValue(ability, out var nextTrigger) != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextTrigger - Core.TickCount > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_abilityCooldowns.Remove(ability);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetAbilityCooldown(MonsterAbility ability, TimeSpan cooldown)
|
||||
{
|
||||
_abilityCooldowns ??= new Dictionary<MonsterAbility, long>();
|
||||
_abilityCooldowns[ability] = Core.TickCount + (long)cooldown.TotalMilliseconds;
|
||||
}
|
||||
|
||||
public void ClearAbilityCooldowns() => _abilityCooldowns = null;
|
||||
|
||||
public virtual MonsterAbility GetAbility(MonsterAbilityType type)
|
||||
{
|
||||
var abilities = GetMonsterAbilities();
|
||||
|
|
@ -1519,6 +1559,11 @@ namespace Server.Mobiles
|
|||
for (var i = 0; i < abilities.Length; i++)
|
||||
{
|
||||
var ability = abilities[i];
|
||||
if (!CanUseAbility(ability))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ability is MonsterAbilityGroup group)
|
||||
{
|
||||
ability = group.GetAbilityWithType(type);
|
||||
|
|
@ -1582,7 +1627,7 @@ namespace Server.Mobiles
|
|||
for (var i = 0; i < abilities.Length; i++)
|
||||
{
|
||||
var ability = abilities[i];
|
||||
if (ability.CanTrigger(this, trigger))
|
||||
if (CanUseAbility(ability) && ability.CanTrigger(this, trigger))
|
||||
{
|
||||
ability.Trigger(trigger, this, defender);
|
||||
triggered = true;
|
||||
|
|
@ -1609,7 +1654,7 @@ namespace Server.Mobiles
|
|||
for (var i = 0; i < abilities.Length; i++)
|
||||
{
|
||||
var ability = abilities[i];
|
||||
if (ability.CanTrigger(this, trigger))
|
||||
if (CanUseAbility(ability) && ability.CanTrigger(this, trigger))
|
||||
{
|
||||
ability.Move(this, d);
|
||||
}
|
||||
|
|
@ -1628,7 +1673,7 @@ namespace Server.Mobiles
|
|||
for (var i = 0; i < abilities.Length; i++)
|
||||
{
|
||||
var ability = abilities[i];
|
||||
if (ability.CanTrigger(this, trigger))
|
||||
if (CanUseAbility(ability) && ability.CanTrigger(this, trigger))
|
||||
{
|
||||
if ((trigger & MonsterAbilityTrigger.GiveMeleeDamage) != 0)
|
||||
{
|
||||
|
|
@ -1669,7 +1714,7 @@ namespace Server.Mobiles
|
|||
for (var i = 0; i < abilities.Length; i++)
|
||||
{
|
||||
var ability = abilities[i];
|
||||
if (ability.CanTrigger(this, trigger))
|
||||
if (CanUseAbility(ability) && ability.CanTrigger(this, trigger))
|
||||
{
|
||||
if ((trigger & MonsterAbilityTrigger.GiveSpellDamage) != 0)
|
||||
{
|
||||
|
|
@ -3242,7 +3287,7 @@ namespace Server.Mobiles
|
|||
{
|
||||
PackItem(new ParagonChest(Name, treasureLevel));
|
||||
}
|
||||
else if ((Map == Map.Felucca || Map == Map.Trammel) && Utility.RandomDouble() < TreasureMap.LootChance)
|
||||
else if ((Map == Map.Felucca || Map == Map.Trammel) && Utility.RandomDouble() < TreasureMapChance)
|
||||
{
|
||||
PackItem(new TreasureMap(treasureLevel, Map));
|
||||
}
|
||||
|
|
@ -5834,6 +5879,47 @@ namespace Server.Mobiles
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equips <paramref name="item" /> in place of whatever an inherited outfit put on its layer.
|
||||
/// AddItem would stack both on one layer, and EquipItem would refuse and orphan the item.
|
||||
/// </summary>
|
||||
public void ReplaceEquipment(Item item)
|
||||
{
|
||||
FindItemOnLayer(item.Layer)?.Delete();
|
||||
|
||||
if (!EquipItem(item))
|
||||
{
|
||||
item.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Brings <paramref name="minion" /> into the world on this creature's side. A minion of a champion spawn's
|
||||
/// champion or creatures belongs to that spawn, which deletes it when it clears its creatures.
|
||||
/// </summary>
|
||||
/// <returns>False, and the minion is deleted, if this creature is not in the world.</returns>
|
||||
public bool SpawnMinion(BaseCreature minion, Mobile target = null, Point3D? location = null)
|
||||
{
|
||||
var map = Map;
|
||||
|
||||
if (Deleted || map == null || map == Map.Internal)
|
||||
{
|
||||
minion.Delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
minion.Team = Team;
|
||||
ChampionSpawn.FindSpawnOf(this)?.AddMinion(minion);
|
||||
minion.MoveToWorld(location ?? map.GetRandomNearbyLocation(Location), map);
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
minion.Combatant = target;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual void HealStart(Mobile patient)
|
||||
{
|
||||
StopHeal();
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ public partial class HireRanger : BaseHire
|
|||
{
|
||||
Body = 0x190;
|
||||
Name = NameList.RandomName("male");
|
||||
EquipItem(new ShortPants(Utility.RandomNeutralHue()));
|
||||
}
|
||||
|
||||
EquipItem(new Shoes(Utility.RandomNeutralHue()));
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ namespace Server.Mobiles
|
|||
pack.DropItem(new Bolt(Utility.RandomMinMax(10, 20)));
|
||||
pack.DropItem(new Bolt(Utility.RandomMinMax(10, 20)));
|
||||
|
||||
AddItem(
|
||||
// The crossbow is wielded; the sword would share its hands, so it is carried as loot.
|
||||
pack.DropItem(
|
||||
Utility.Random(6) switch
|
||||
{
|
||||
0 => new Longsword(),
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ namespace Server.Mobiles
|
|||
Fame = 18000;
|
||||
Karma = -18000;
|
||||
|
||||
AddItem(new Shoes(0x537));
|
||||
AddItem(new Robe(0x452));
|
||||
ReplaceEquipment(new Shoes(0x537));
|
||||
ReplaceEquipment(new Robe(0x452));
|
||||
|
||||
for (var i = 0; i < 2; ++i)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ namespace Server.Mobiles
|
|||
|
||||
public override int TreasureMapLevel => 5;
|
||||
|
||||
public override double TreasureMapChance => 0.25;
|
||||
|
||||
public override bool OnBeforeDeath()
|
||||
{
|
||||
PackItem(new MessageInABottle(Map == Map.Felucca ? Map.Felucca : Map.Trammel));
|
||||
|
|
@ -147,7 +149,7 @@ namespace Server.Mobiles
|
|||
{
|
||||
base.OnDeath(c);
|
||||
|
||||
if (Fisher != null && Utility.Random(100) < 25)
|
||||
if (Core.SE && Fisher?.Skills.Fishing.Base >= 100.0 && Utility.Random(100) < 25)
|
||||
{
|
||||
GiveArtifactTo(Fisher);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,11 +176,8 @@ namespace Server.Mobiles
|
|||
{
|
||||
_disguiseTimerToken.Cancel();
|
||||
|
||||
if (!IsBodyMod)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// BodyMod is not saved, so after a restart a disguised kitsune comes back without it but still
|
||||
// wearing the disguise; strip it regardless.
|
||||
Name = null;
|
||||
Title = null;
|
||||
BodyMod = 0;
|
||||
|
|
|
|||
|
|
@ -131,9 +131,7 @@ public partial class Barracoon : BaseChampion
|
|||
|
||||
public void SpawnRatmen(Mobile target)
|
||||
{
|
||||
var map = Map;
|
||||
|
||||
if (map == null)
|
||||
if (Map == null || Map == Map.Internal)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -162,9 +160,7 @@ public partial class Barracoon : BaseChampion
|
|||
_ => new Ratman()
|
||||
};
|
||||
|
||||
rat.Team = Team;
|
||||
rat.MoveToWorld(map.GetRandomNearbyLocation(Location), map);
|
||||
rat.Combatant = target;
|
||||
SpawnMinion(rat, target);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,11 @@ public partial class LordOaks : BaseChampion
|
|||
public override Poison PoisonImmune => Poison.Deadly;
|
||||
|
||||
private static MonsterAbility[] _abilities = { MonsterAbilities.SummonPixiesCounter };
|
||||
public override MonsterAbility[] GetMonsterAbilities() => CheckQueen() ? _abilities : MonsterAbilities.Empty;
|
||||
public override MonsterAbility[] GetMonsterAbilities() => _abilities;
|
||||
|
||||
public override bool CanUseAbility(MonsterAbility ability) => IsQueenAlive;
|
||||
|
||||
private bool IsQueenAlive => _queen?.Deleted == false;
|
||||
|
||||
public override void GenerateLoot()
|
||||
{
|
||||
|
|
@ -103,36 +107,42 @@ public partial class LordOaks : BaseChampion
|
|||
|
||||
public override int GetDeathSound() => 0x2F7;
|
||||
|
||||
public bool CheckQueen()
|
||||
// Aggression lands before the damage it causes, so the queen is up before the first hit is scaled.
|
||||
public override void AggressiveAction(Mobile aggressor, bool criminal)
|
||||
{
|
||||
if (Map == null)
|
||||
base.AggressiveAction(aggressor, criminal);
|
||||
SpawnQueen();
|
||||
}
|
||||
|
||||
public override void OnCombatantChange()
|
||||
{
|
||||
base.OnCombatantChange();
|
||||
|
||||
if (Combatant != null)
|
||||
{
|
||||
return false;
|
||||
SpawnQueen();
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnQueen()
|
||||
{
|
||||
if (_spawnedQueen || Deleted || Map == null || Map == Map.Internal)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_spawnedQueen)
|
||||
{
|
||||
Say(1042153); // Come forth my queen!
|
||||
Say(1042153); // Come forth my queen!
|
||||
|
||||
_queen = new Silvani { Team = Team };
|
||||
_queen.MoveToWorld(Location, Map);
|
||||
var queen = new Silvani();
|
||||
SpawnMinion(queen, location: Location);
|
||||
|
||||
_spawnedQueen = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_queen?.Deleted != false)
|
||||
{
|
||||
_queen = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
Queen = queen;
|
||||
SpawnedQueen = true;
|
||||
}
|
||||
|
||||
public override void AlterDamageScalarFrom(Mobile caster, ref double scalar)
|
||||
{
|
||||
if (CheckQueen())
|
||||
if (IsQueenAlive)
|
||||
{
|
||||
scalar *= 0.1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,13 +163,13 @@ public partial class Neira : BaseChampion
|
|||
}
|
||||
}
|
||||
|
||||
public override void AlterDamageScalarFrom(Mobile caster, ref double scalar)
|
||||
public override void OnDamagedBySpell(Mobile from, int damage)
|
||||
{
|
||||
base.AlterDamageScalarFrom(caster, ref scalar);
|
||||
base.OnDamagedBySpell(from, damage);
|
||||
|
||||
if (Utility.RandomDouble() < 0.1) // 10% chance to throw an unholy bone
|
||||
{
|
||||
AddUnholyBone(caster, 1.0);
|
||||
AddUnholyBone(from, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,15 +33,6 @@ public partial class Merchant : BaseEscortable
|
|||
|
||||
public override void InitOutfit()
|
||||
{
|
||||
if (Female)
|
||||
{
|
||||
AddItem(new PlainDress());
|
||||
}
|
||||
else
|
||||
{
|
||||
AddItem(new Shirt(GetRandomHue()));
|
||||
}
|
||||
|
||||
var lowHue = GetRandomHue();
|
||||
|
||||
AddItem(new ThighBoots());
|
||||
|
|
|
|||
|
|
@ -58,14 +58,6 @@ namespace Server.Mobiles
|
|||
AddItem(new FullApron(Utility.RandomBrightHue()));
|
||||
}
|
||||
|
||||
AddItem(
|
||||
Utility.Random(3) switch
|
||||
{
|
||||
0 => new Bandana(Utility.RandomBrightHue()),
|
||||
_ => new JesterHat(Utility.RandomBrightHue()) // 1-2
|
||||
}
|
||||
);
|
||||
|
||||
AddItem(new Bascinet());
|
||||
AddItem(new SmithHammer());
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ namespace Server.Mobiles
|
|||
{
|
||||
base.InitOutfit();
|
||||
|
||||
AddItem(new FancyShirt(0x3E4));
|
||||
AddItem(new LongPants(0x192));
|
||||
ReplaceEquipment(new FancyShirt(0x3E4));
|
||||
ReplaceEquipment(new LongPants(0x192));
|
||||
AddItem(new Pickaxe());
|
||||
AddItem(new ThighBoots(0x283));
|
||||
ReplaceEquipment(new ThighBoots(0x283));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ namespace Server.Mobiles
|
|||
{
|
||||
base.InitOutfit();
|
||||
|
||||
AddItem(new Shirt(Utility.RandomNeutralHue()));
|
||||
AddItem(new LongPants(Utility.RandomNeutralHue()));
|
||||
ReplaceEquipment(new Shirt(Utility.RandomNeutralHue()));
|
||||
ReplaceEquipment(new LongPants(Utility.RandomNeutralHue()));
|
||||
AddItem(new Bow());
|
||||
AddItem(new ThighBoots(Utility.RandomNeutralHue()));
|
||||
ReplaceEquipment(new ThighBoots(Utility.RandomNeutralHue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ namespace Server.Mobiles
|
|||
{
|
||||
base.InitOutfit();
|
||||
|
||||
AddItem(new Shirt(Utility.RandomNeutralHue()));
|
||||
AddItem(new LongPants(Utility.RandomNeutralHue()));
|
||||
ReplaceEquipment(new Shirt(Utility.RandomNeutralHue()));
|
||||
ReplaceEquipment(new LongPants(Utility.RandomNeutralHue()));
|
||||
AddItem(new Dagger());
|
||||
AddItem(new ThighBoots(Utility.RandomNeutralHue()));
|
||||
ReplaceEquipment(new ThighBoots(Utility.RandomNeutralHue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,18 +40,20 @@
|
|||
<ProjectReference Include="..\Server\Server.csproj" Private="false" PrivateAssets="All" IncludeAssets="None">
|
||||
<IncludeInPackage>false</IncludeInPackage>
|
||||
</ProjectReference>
|
||||
|
||||
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="[10.0.13, )" />
|
||||
<PackageReference Include="LibDeflate.Bindings" Version="1.0.4" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.12" />
|
||||
<PackageReference Include="MailKit" Version="4.18.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.12" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.13" />
|
||||
<PackageReference Include="MailKit" Version="4.18.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.13" />
|
||||
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.2" />
|
||||
<PackageReference Include="Argon2.Bindings" Version="1.20.0" />
|
||||
<PackageReference Include="ModernUO.CodeGeneratedEvents.Annotations" Version="1.0.0" />
|
||||
<PackageReference Include="ModernUO.CodeGeneratedEvents.Generator" Version="1.0.3.2" PrivateAssets="all" />
|
||||
<PackageReference Include="ZstdNet" Version="1.5.7" />
|
||||
|
||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="4.2.0" />
|
||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="4.2.0" PrivateAssets="all" />
|
||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="4.3.0" />
|
||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="4.3.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Migrations/*.v*.json" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue