#W# Merge: Merged with upstream. Conflicts resolved.

This commit is contained in:
WarrentyExpired 2026-10-27 19:56:03 -04:00
commit f6f50470a9
276 changed files with 6742 additions and 8845 deletions

View file

@ -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)

View file

@ -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>();

View file

@ -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;
}

View 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);
}
}
}
}

View file

@ -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(

View file

@ -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))
{

View file

@ -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;
}

View file

@ -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.");

View file

@ -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;
}
}

View file

@ -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

View file

@ -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;

View file

@ -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)]

View file

@ -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)

View file

@ -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

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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)

View file

@ -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.");

View file

@ -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
}
}
}
}

View file

@ -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;

View 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"
]
}
]
}

View file

@ -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()

View file

@ -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;
}

View file

@ -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.

View file

@ -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());
}

View file

@ -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();
}

View file

@ -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);

View file

@ -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();

View file

@ -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()));

View file

@ -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(),

View file

@ -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)
{

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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());

View file

@ -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());

View file

@ -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));
}
}
}

View file

@ -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()));
}
}
}

View file

@ -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()));
}
}
}

View file

@ -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" />