## Summary Overhauls the fame and karma system to be more era-accurate, based on original design documents and publish notes. ### Karma on player kill → karma on murder report - Removes karma gain/loss on player kill (was immediate on death) - Karma is now set to `Kills * -1000` on murder **report** instead - Fame on player kill now uses the same formula as monster kills (`Fame / 100`) This karma loss on murder report behaviour was tested on both the demo and live servers, behaviour was matching in terms of karma loss on report. Official UO servers karma loss AMOUNT match with my memory of T2A/UOR with one caveat - it's doubled on live servers (-2000 * kill count). I'm not sure when this changed and this behaviour has always had very poor and incorrect documentation, even 10-20 years ago. I was obsessed with the dread lord title on OSI and the only way I knew how to get it was reach 10 kills then macro them off. Even in publish 16 (when "The Murderer" title was removed) it still required 10 kills. Maybe it changed to -2000*Kills in AOS - that's where I've put the era gating diff. ### Era gates - **Karma lock** (ankh toggle + auto-lock on negative karma) gated to `Core.UOTD && !Core.AOS` — [didn't exist before Jan 28, 2001](https://web.archive.org/web/20010128092700/http://update.uo.com/design_300.html) - **Felucca fame/karma +30% bonus** gated to `Core.LBR` — [added in Publish 16, July 2002](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/) - **Fame/karma splitting** among damage dealers gated to `Core.UOR` — pre-UOR awards go to last hit only ### Skill karma penalties - **Provocation** on innocent NPC: NPC says cliloc 501591, karma loss (floor -7500) - **Stealing** attempt: karma loss on every attempt (floor -5000). Stealing did not cause karma loss at all before! - **Summon Daemon**: karma loss on successful cast (floor -7000) - **Corpse carving** (human): -70 for innocent corpses (floor -7000), -20 for freely-aggressable (floor -2000) - **Bounty head turn-in**: karma gain capped at 2000 (was awarding flat +2000) ### Beneficial action karma - **Beneficial spells** (heal, cure, etc.): `AwardKarma(caster, target.Karma / 5)` — healing good targets raises karma, healing evil targets lowers it - source is UO98 demo scripts - **Bandages**: same formula but gain only (skipped if target karma ≤ 0) - see stratics link ("only ever gain karma, not lose it") Sources: UO98 Demo scripts and playing, [Fame and Karma wiki](https://uo.com/wiki/ultima-online-wiki/player/fame-and-karma/), [UO design doc (Jan 2001)](https://web.archive.org/web/20010128092700/http://update.uo.com/design_300.html), [Publish 16](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/), [Stratics healing reference](https://web.archive.org/web/20001209014200fw_/http://uo.stratics.com/heal.shtml) ### Refactoring - Extracted `Titles.ComputeKillAwards(killed, map)` shared by player kill and creature kill paths - Extracted `Titles.SetKarma(m, value, message)` for direct karma assignment (used by murder report) - Extracted `SendKarmaMessage` and `CheckKarmaLock` helpers from `AwardKarma`
403 lines
11 KiB
C#
403 lines
11 KiB
C#
using ModernUO.Serialization;
|
|
using Server.Collections;
|
|
using Server.ContextMenus;
|
|
using Server.Gumps;
|
|
using Server.Mobiles;
|
|
|
|
namespace Server.Items
|
|
{
|
|
public static class Ankhs
|
|
{
|
|
public const int ResurrectRange = 2;
|
|
public const int TitheRange = 2;
|
|
public const int LockRange = 2;
|
|
|
|
public static void GetContextMenuEntries(Mobile from, Item item, ref PooledRefList<ContextMenuEntry> list)
|
|
{
|
|
if (Core.UOTD && !Core.AOS && from is PlayerMobile mobile)
|
|
{
|
|
list.Add(new LockKarmaEntry(mobile.KarmaLocked));
|
|
}
|
|
|
|
list.Add(new ResurrectEntry(from.Alive));
|
|
|
|
if (Core.AOS)
|
|
{
|
|
list.Add(new TitheEntry(from.Alive));
|
|
}
|
|
}
|
|
|
|
public static void Resurrect(Mobile m, Item item)
|
|
{
|
|
if (m.Alive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!m.InRange(item.GetWorldLocation(), ResurrectRange))
|
|
{
|
|
m.SendLocalizedMessage(500446); // That is too far away.
|
|
}
|
|
else if (m.Map?.CanFit(m.Location, 16, false, false) == true)
|
|
{
|
|
m.SendGump(new ResurrectGump(m, ResurrectMessage.VirtueShrine));
|
|
}
|
|
else
|
|
{
|
|
m.SendLocalizedMessage(502391); // Thou can not be resurrected there!
|
|
}
|
|
}
|
|
|
|
private class ResurrectEntry : ContextMenuEntry
|
|
{
|
|
public ResurrectEntry(bool enabled) : base(6195, ResurrectRange) => Enabled = enabled;
|
|
|
|
public override void OnClick(Mobile from, IEntity target)
|
|
{
|
|
if (target is Item item)
|
|
{
|
|
Resurrect(from, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class LockKarmaEntry : ContextMenuEntry
|
|
{
|
|
public LockKarmaEntry(bool karmaLocked) : base(karmaLocked ? 6197 : 6196, LockRange)
|
|
{
|
|
}
|
|
|
|
public override void OnClick(Mobile from, IEntity target)
|
|
{
|
|
if (from is not PlayerMobile pm || target is not Item item)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!from.InRange(item.GetWorldLocation(), 2))
|
|
{
|
|
from.SendLocalizedMessage(500446); // That is too far away.
|
|
}
|
|
|
|
pm.KarmaLocked = !pm.KarmaLocked;
|
|
|
|
if (pm.KarmaLocked)
|
|
{
|
|
// Your karma has been locked. Your karma can no longer be raised.
|
|
pm.SendLocalizedMessage(1060192);
|
|
}
|
|
else
|
|
{
|
|
pm.SendLocalizedMessage(1060191); // Your karma has been unlocked. Your karma can be raised again.
|
|
}
|
|
}
|
|
}
|
|
|
|
private class TitheEntry : ContextMenuEntry
|
|
{
|
|
public TitheEntry(bool enabled) : base(6198, TitheRange) => Enabled = enabled;
|
|
|
|
public override void OnClick(Mobile from, IEntity target)
|
|
{
|
|
if (from.CheckAlive())
|
|
{
|
|
from.SendGump(new TithingGump(from));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[SerializationGenerator(0, false)]
|
|
public partial class AnkhWest : Item
|
|
{
|
|
[SerializableField(0, getter: "private", setter: "private")]
|
|
private InternalItem _item;
|
|
|
|
[Constructible]
|
|
public AnkhWest(bool bloodied = false) : base(bloodied ? 0x1D98 : 0x3)
|
|
{
|
|
Movable = false;
|
|
_item = new InternalItem(bloodied, this);
|
|
}
|
|
|
|
public override bool HandlesOnMovement => true; // Tell the core that we implement OnMovement
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public override int Hue
|
|
{
|
|
get => base.Hue;
|
|
set
|
|
{
|
|
base.Hue = value;
|
|
if (_item.Hue != value)
|
|
{
|
|
_item.Hue = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnMovement(Mobile m, Point3D oldLocation)
|
|
{
|
|
if (Parent == null && Utility.InRange(Location, m.Location, 1) && !Utility.InRange(Location, oldLocation, 1))
|
|
{
|
|
Ankhs.Resurrect(m, this);
|
|
}
|
|
}
|
|
|
|
public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
|
{
|
|
base.GetContextMenuEntries(from, ref list);
|
|
Ankhs.GetContextMenuEntries(from, this, ref list);
|
|
}
|
|
|
|
public override void OnDoubleClickDead(Mobile m)
|
|
{
|
|
Ankhs.Resurrect(m, this);
|
|
}
|
|
|
|
public override void OnLocationChange(Point3D oldLocation)
|
|
{
|
|
if (_item != null)
|
|
{
|
|
_item.Location = new Point3D(X, Y + 1, Z);
|
|
}
|
|
}
|
|
|
|
public override void OnMapChange()
|
|
{
|
|
if (_item != null)
|
|
{
|
|
_item.Map = Map;
|
|
}
|
|
}
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
base.OnAfterDelete();
|
|
|
|
_item?.Delete();
|
|
}
|
|
|
|
[SerializationGenerator(0, false)]
|
|
private partial class InternalItem : Item
|
|
{
|
|
[SerializableField(0)]
|
|
private AnkhWest _item;
|
|
|
|
public InternalItem(bool bloodied, AnkhWest item) : base(bloodied ? 0x1D97 : 0x2)
|
|
{
|
|
Movable = false;
|
|
_item = item;
|
|
}
|
|
|
|
public override bool HandlesOnMovement => true; // Tell the core that we implement OnMovement
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public override int Hue
|
|
{
|
|
get => base.Hue;
|
|
set
|
|
{
|
|
base.Hue = value;
|
|
if (_item.Hue != value)
|
|
{
|
|
_item.Hue = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnLocationChange(Point3D oldLocation)
|
|
{
|
|
if (_item != null)
|
|
{
|
|
_item.Location = new Point3D(X, Y - 1, Z);
|
|
}
|
|
}
|
|
|
|
public override void OnMapChange()
|
|
{
|
|
if (_item != null)
|
|
{
|
|
_item.Map = Map;
|
|
}
|
|
}
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
base.OnAfterDelete();
|
|
|
|
_item?.Delete();
|
|
}
|
|
|
|
public override void OnMovement(Mobile m, Point3D oldLocation)
|
|
{
|
|
if (Parent == null && Utility.InRange(Location, m.Location, 1) && !Utility.InRange(Location, oldLocation, 1))
|
|
{
|
|
Ankhs.Resurrect(m, this);
|
|
}
|
|
}
|
|
|
|
public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
|
{
|
|
base.GetContextMenuEntries(from, ref list);
|
|
Ankhs.GetContextMenuEntries(from, this, ref list);
|
|
}
|
|
|
|
public override void OnDoubleClickDead(Mobile m)
|
|
{
|
|
Ankhs.Resurrect(m, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
[TypeAlias("Server.Items.AnkhEast")]
|
|
[SerializationGenerator(0, false)]
|
|
public partial class AnkhNorth : Item
|
|
{
|
|
[SerializableField(0, getter: "private", setter: "private")]
|
|
private InternalItem _item;
|
|
|
|
[Constructible]
|
|
public AnkhNorth(bool bloodied = false) : base(bloodied ? 0x1E5D : 0x4)
|
|
{
|
|
Movable = false;
|
|
|
|
_item = new InternalItem(bloodied, this);
|
|
}
|
|
|
|
public override bool HandlesOnMovement => true; // Tell the core that we implement OnMovement
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public override int Hue
|
|
{
|
|
get => base.Hue;
|
|
set
|
|
{
|
|
base.Hue = value;
|
|
if (_item.Hue != value)
|
|
{
|
|
_item.Hue = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnMovement(Mobile m, Point3D oldLocation)
|
|
{
|
|
if (Parent == null && Utility.InRange(Location, m.Location, 1) && !Utility.InRange(Location, oldLocation, 1))
|
|
{
|
|
Ankhs.Resurrect(m, this);
|
|
}
|
|
}
|
|
|
|
public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
|
{
|
|
base.GetContextMenuEntries(from, ref list);
|
|
Ankhs.GetContextMenuEntries(from, this, ref list);
|
|
}
|
|
|
|
public override void OnDoubleClickDead(Mobile m)
|
|
{
|
|
Ankhs.Resurrect(m, this);
|
|
}
|
|
|
|
public override void OnLocationChange(Point3D oldLocation)
|
|
{
|
|
if (_item != null)
|
|
{
|
|
_item.Location = new Point3D(X + 1, Y, Z);
|
|
}
|
|
}
|
|
|
|
public override void OnMapChange()
|
|
{
|
|
if (_item != null)
|
|
{
|
|
_item.Map = Map;
|
|
}
|
|
}
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
base.OnAfterDelete();
|
|
|
|
_item?.Delete();
|
|
}
|
|
|
|
[TypeAlias("Server.Items.AnkhEast+InternalItem")]
|
|
[SerializationGenerator(0, false)]
|
|
private partial class InternalItem : Item
|
|
{
|
|
[SerializableField(0)]
|
|
private AnkhNorth _item;
|
|
|
|
public InternalItem(bool bloodied, AnkhNorth item)
|
|
: base(bloodied ? 0x1E5C : 0x5)
|
|
{
|
|
Movable = false;
|
|
_item = item;
|
|
}
|
|
|
|
public override bool HandlesOnMovement => true; // Tell the core that we implement OnMovement
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public override int Hue
|
|
{
|
|
get => base.Hue;
|
|
set
|
|
{
|
|
base.Hue = value;
|
|
if (_item.Hue != value)
|
|
{
|
|
_item.Hue = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnLocationChange(Point3D oldLocation)
|
|
{
|
|
if (_item != null)
|
|
{
|
|
_item.Location = new Point3D(X - 1, Y, Z);
|
|
}
|
|
}
|
|
|
|
public override void OnMapChange()
|
|
{
|
|
if (_item != null)
|
|
{
|
|
_item.Map = Map;
|
|
}
|
|
}
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
base.OnAfterDelete();
|
|
|
|
_item?.Delete();
|
|
}
|
|
|
|
public override void OnMovement(Mobile m, Point3D oldLocation)
|
|
{
|
|
if (Parent == null && Utility.InRange(Location, m.Location, 1) && !Utility.InRange(Location, oldLocation, 1))
|
|
{
|
|
Ankhs.Resurrect(m, this);
|
|
}
|
|
}
|
|
|
|
public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
|
{
|
|
base.GetContextMenuEntries(from, ref list);
|
|
Ankhs.GetContextMenuEntries(from, this, ref list);
|
|
}
|
|
|
|
public override void OnDoubleClickDead(Mobile m)
|
|
{
|
|
Ankhs.Resurrect(m, this);
|
|
}
|
|
}
|
|
}
|
|
}
|