Merge branch 'master' into ed-25-08-2025-upstream-sync

# Conflicts:
#	Resources/Prototypes/_CP14/ModularCraft/Blade/axe.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/base.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/dagger.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/hammer.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/hoe.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/mace.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/mop.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/pickaxe.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/shovel.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/sickle.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/skimitar.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/spear.yml
#	Resources/Prototypes/_CP14/ModularCraft/Blade/sword.yml
#	Resources/Prototypes/_CP14/ModularCraft/Garde/guildmaster.yml
#	Resources/Prototypes/_CP14/ModularCraft/Garde/sharp.yml
This commit is contained in:
Ed
2025-09-08 11:33:44 +03:00
669 changed files with 13311 additions and 13653 deletions

View File

@@ -66,13 +66,19 @@ public sealed class MeleeHitEvent : HandledEntityEventArgs
/// </remarks>
public bool IsHit = true;
public MeleeHitEvent(List<EntityUid> hitEntities, EntityUid user, EntityUid weapon, DamageSpecifier baseDamage, Vector2? direction)
/// <summary>
/// CP14 Heavy attack flag.
/// </summary>
public bool CP14Heavy;
public MeleeHitEvent(List<EntityUid> hitEntities, EntityUid user, EntityUid weapon, DamageSpecifier baseDamage, Vector2? direction, bool heavy = false)
{
HitEntities = hitEntities;
User = user;
Weapon = weapon;
BaseDamage = baseDamage;
Direction = direction;
CP14Heavy = heavy; //CP14
}
}

View File

@@ -84,7 +84,7 @@ public sealed partial class MeleeWeaponComponent : Component
/// Multiplies damage by this amount for single-target attacks.
/// </summary>
[DataField, AutoNetworkedField]
public FixedPoint2 ClickDamageModifier = FixedPoint2.New(1);
public FixedPoint2 ClickDamageModifier = FixedPoint2.New(1.3); //CP14 default bonus damage
// TODO: Temporarily 1.5 until interactionoutline is adjusted to use melee, then probably drop to 1.2
/// <summary>
@@ -131,7 +131,7 @@ public sealed partial class MeleeWeaponComponent : Component
/// CrystallEdge Melee upgrade. how far away from the player the animation should be played.
/// </summary>
[DataField]
public float CPAnimationOffset = -1f;
public float CPAnimationOffset = 1f;
// Sounds

View File

@@ -498,7 +498,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
LogImpact.Low,
$"{ToPrettyString(user):actor} melee attacked (light) using {ToPrettyString(meleeUid):tool} and missed");
}
var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, null);
var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, null, heavy: false);
RaiseLocalEvent(meleeUid, missEvent);
_meleeSound.PlaySwingSound(user, meleeUid, component);
return;
@@ -507,7 +507,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
// Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}");
// Raise event before doing damage so we can cancel damage if the event is handled
var hitEvent = new MeleeHitEvent(new List<EntityUid> { target.Value }, user, meleeUid, damage, null);
var hitEvent = new MeleeHitEvent(new List<EntityUid> { target.Value }, user, meleeUid, damage, null, heavy: false);
RaiseLocalEvent(meleeUid, hitEvent);
if (hitEvent.Handled)
@@ -600,7 +600,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
LogImpact.Low,
$"{ToPrettyString(user):actor} melee attacked (heavy) using {ToPrettyString(meleeUid):tool} and missed");
}
var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, direction);
var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, direction, heavy: true);
RaiseLocalEvent(meleeUid, missEvent);
// immediate audio feedback
@@ -649,7 +649,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
// Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}");
// Raise event before doing damage so we can cancel damage if the event is handled
var hitEvent = new MeleeHitEvent(targets, user, meleeUid, damage, direction);
var hitEvent = new MeleeHitEvent(targets, user, meleeUid, damage, direction, heavy: true);
RaiseLocalEvent(meleeUid, hitEvent);
if (hitEvent.Handled)

View File

@@ -0,0 +1,7 @@
using Content.Shared.Actions;
namespace Content.Shared._CP14.Eye;
public sealed partial class CP14EyeOffsetToggleActionEvent : InstantActionEvent
{
}

View File

@@ -1,9 +1,9 @@
using Content.Shared.Examine;
using Content.Shared.Ghost;
using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
@@ -15,7 +15,7 @@ public abstract class CP14SharedIdentityRecognitionSystem : EntitySystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedIdentitySystem _identity = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
@@ -68,11 +68,17 @@ public abstract class CP14SharedIdentityRecognitionSystem : EntitySystem
Priority = 2,
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/sentient.svg.192dpi.png")),
Text = Loc.GetString("cp14-remember-name-verb"),
Disabled = seeAttemptEv.Cancelled,
Act = () =>
{
_uiSystem.SetUiState(_args.User, CP14RememberNameUiKey.Key, new CP14RememberNameUiState(GetNetEntity(ent)));
_uiSystem.TryToggleUi(_args.User, CP14RememberNameUiKey.Key, actor.PlayerSession);
if (seeAttemptEv.Cancelled)
{
_popup.PopupClient(Loc.GetString("cp14-remember-fail-mask"), _args.Target, _args.User);
}
else
{
_uiSystem.SetUiState(_args.User, CP14RememberNameUiKey.Key, new CP14RememberNameUiState(GetNetEntity(ent)));
_uiSystem.TryToggleUi(_args.User, CP14RememberNameUiKey.Key, actor.PlayerSession);
}
},
};
args.Verbs.Add(verb);

View File

@@ -0,0 +1,27 @@
using Content.Shared.Stunnable;
namespace Content.Shared._CP14.MagicSpell.Spells;
public sealed partial class CP14SpellKnockdown : CP14SpellEffect
{
[DataField]
public float ThrowPower = 10f;
[DataField]
public TimeSpan Time = TimeSpan.FromSeconds(1f);
[DataField]
public bool DropItems = false;
public override void Effect(EntityManager entManager, CP14SpellEffectBaseArgs args)
{
if (args.Target is null || args.User is null)
return;
var targetEntity = args.Target.Value;
var stun = entManager.System<SharedStunSystem>();
stun.TryKnockdown(args.Target.Value, Time, true, true, DropItems);
}
}

View File

@@ -63,5 +63,29 @@ public sealed partial class CP14SpellPointerToVampireClan : CP14SpellEffect
transform.SetWorldRotation(pointer, angle + Angle.FromDegrees(90));
}
var heartsInRange = lookup.GetEntitiesInRange<CP14VampireClanHeartComponent>(originEntPosition, SearchRange);
foreach (var heart in heartsInRange)
{
if (!Inversed)
{
if (heart.Comp.Faction != vampireComponent.Faction)
continue;
}
else
{
if (heart.Comp.Faction == vampireComponent.Faction)
continue;
}
var targetPosition = transform.GetWorldPosition(heart);
//Calculate the rotation
Angle angle = new(targetPosition - originPosition);
var pointer = entManager.Spawn(PointerEntity, new MapCoordinates(originPosition, transform.GetMapId(originEntPosition)));
transform.SetWorldRotation(pointer, angle + Angle.FromDegrees(90));
}
}
}

View File

@@ -44,8 +44,7 @@ public sealed partial class CP14SpellTeleportToVampireSingleton : CP14SpellEffec
if (singleton.Key != indexedVampireFaction.SingletonTeleportKey)
continue;
var randomOffset = new Vector2(random.Next(-1, 1), random.Next(-1, 1));
var second = entManager.SpawnAtPosition(PortalProto, xform.Coordinates.Offset(randomOffset));
var second = entManager.SpawnAtPosition(PortalProto, xform.Coordinates);
linkSys.TryLink(first, second, true);
return;

View File

@@ -0,0 +1,28 @@
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.MeleeWeapon.Components;
/// <summary>
/// Adds bonus damage to weapons if targets are at a certain distance from the attacker.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class CP14BonusDistanceMeleeDamageComponent : Component
{
[DataField]
public DamageSpecifier BonusDamage = new();
[DataField]
public float MinDistance = 1f;
[DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/_CP14/Effects/critical.ogg")
{
Params = AudioParams.Default.WithVariation(0.125f),
};
[DataField]
public EntProtoId VFX = "CP14MeleeCritEffect";
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.MeleeWeapon.Components;
/// <summary>
/// After several wide attacks, a light attack deals additional damage.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class CP14ComboBonusMeleeDamageComponent : Component
{
[DataField]
public DamageSpecifier BonusDamage = new();
[DataField]
public int HeavyAttackNeed = 2;
[DataField, AutoNetworkedField]
public int CurrentHeavyAttacks = 0;
[DataField, AutoNetworkedField]
public HashSet<EntityUid> HitEntities = new();
[DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/_CP14/Effects/critical_sword.ogg")
{
Params = AudioParams.Default.WithVariation(0.125f),
};
[DataField]
public EntProtoId VFX = "CP14MeleeCritEffect";
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.GameStates;
namespace Content.Shared._CP14.MeleeWeapon.Components;
/// <summary>
/// After several wide attacks, a light attack deals additional damage.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class CP14LightMeleeKnockdownComponent : Component
{
[DataField]
public float ThrowDistance = 0.5f;
[DataField]
public TimeSpan KnockdownTime = TimeSpan.FromSeconds(0.25f);
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Audio;
namespace Content.Shared._CP14.MeleeWeapon.Components;
/// <summary>
/// allows this item to be knocked out of your hands by a successful parry
/// </summary>
[RegisterComponent]
public sealed partial class CP14MeleeParriableComponent : Component
{
[DataField]
public TimeSpan LastMeleeHit = TimeSpan.Zero;
[DataField]
public SoundSpecifier ParrySound = new SoundPathSpecifier("/Audio/_CP14/Effects/parry1.ogg", AudioParams.Default.WithVariation(0.2f));
}

View File

@@ -0,0 +1,14 @@
namespace Content.Shared._CP14.MeleeWeapon.Components;
/// <summary>
/// attacks with this item may knock CP14ParriableComponent items out of your hand on a hit
/// </summary>
[RegisterComponent]
public sealed partial class CP14MeleeParryComponent : Component
{
[DataField]
public TimeSpan ParryWindow = TimeSpan.FromSeconds(1f);
[DataField]
public float ParryPower = 1f;
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.GameStates;
namespace Content.Shared._CP14.MeleeWeapon.Components;
/// <summary>
/// Using this weapon damages the wearer's stamina.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class CP14MeleeWeaponStaminaCostComponent : Component
{
[DataField]
public float Stamina = 10f;
}

View File

@@ -1,24 +0,0 @@
using Content.Shared._CP14.MeleeWeapon.Components;
using Content.Shared.Damage;
using Content.Shared.Weapons.Melee.Events;
namespace Content.Shared._CP14.MeleeWeapon.EntitySystems;
public sealed class CP14MeleeSelfDamageSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageable = default!;
public override void Initialize()
{
SubscribeLocalEvent<CP14MeleeSelfDamageComponent, MeleeHitEvent>(OnMeleeHit);
}
private void OnMeleeHit(Entity<CP14MeleeSelfDamageComponent> ent, ref MeleeHitEvent args)
{
if (!args.IsHit)
return;
if (args.HitEntities.Count == 0)
return;
_damageable.TryChangeDamage(ent, ent.Comp.DamageToSelf);
}
}

View File

@@ -0,0 +1,210 @@
using System.Numerics;
using Content.Shared._CP14.MeleeWeapon.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared._CP14.MeleeWeapon.EntitySystems;
public sealed class CP14MeleeWeaponSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly ThrowingSystem _throw = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedStaminaSystem _stamina = default!;
public override void Initialize()
{
SubscribeLocalEvent<CP14MeleeSelfDamageComponent, MeleeHitEvent>(OnMeleeHit);
SubscribeLocalEvent<CP14BonusDistanceMeleeDamageComponent, MeleeHitEvent>(OnDistanceBonusDamage);
SubscribeLocalEvent<CP14ComboBonusMeleeDamageComponent, MeleeHitEvent>(OnComboBonusDamage);
SubscribeLocalEvent<CP14LightMeleeKnockdownComponent, MeleeHitEvent>(OnKnockdownAttack);
SubscribeLocalEvent<CP14MeleeParryComponent, MeleeHitEvent>(OnMeleeParryHit);
SubscribeLocalEvent<CP14MeleeParriableComponent, AttemptMeleeEvent>(OnMeleeParriableHitAttmpt);
SubscribeLocalEvent<CP14MeleeWeaponStaminaCostComponent, MeleeHitEvent>(OnMeleeStaminaCost);
}
private void OnMeleeStaminaCost(Entity<CP14MeleeWeaponStaminaCostComponent> ent, ref MeleeHitEvent args)
{
_stamina.TakeStaminaDamage(args.User, ent.Comp.Stamina);
}
private void OnMeleeParryHit(Entity<CP14MeleeParryComponent> ent, ref MeleeHitEvent args)
{
if (args.HitEntities.Count != 1)
return;
var target = args.HitEntities[0];
var activeTargetHand = _hands.GetActiveHand(target);
var heldItem = _hands.GetHeldItem(target, activeTargetHand);
if (heldItem is null)
return;
if (!TryComp<CP14MeleeParriableComponent>(heldItem, out var meleeParriable))
return;
if (_timing.CurTime > meleeParriable.LastMeleeHit + ent.Comp.ParryWindow)
return;
_hands.TryDrop(target, heldItem.Value);
_throw.TryThrow(heldItem.Value, _random.NextAngle().ToWorldVec(), ent.Comp.ParryPower, target);
_popup.PopupPredicted( Loc.GetString("cp14-successful-parry"), args.User, args.User);
_audio.PlayPredicted(meleeParriable.ParrySound, heldItem.Value, args.User);
}
private void OnMeleeParriableHitAttmpt(Entity<CP14MeleeParriableComponent> ent, ref AttemptMeleeEvent args)
{
ent.Comp.LastMeleeHit = _timing.CurTime;
}
private void OnKnockdownAttack(Entity<CP14LightMeleeKnockdownComponent> ent, ref MeleeHitEvent args)
{
if (args.CP14Heavy)
return;
foreach (var hit in args.HitEntities)
{
_stun.TryKnockdown(hit, ent.Comp.KnockdownTime, true, drop: false);
// Vector from splitter to item
var direction = Transform(hit).Coordinates.Position - Transform(args.User).Coordinates.Position;
if (direction != Vector2.Zero)
{
var dir = direction.Normalized() * ent.Comp.ThrowDistance;
_throw.TryThrow(hit, dir, 3);
}
}
}
private void OnComboBonusDamage(Entity<CP14ComboBonusMeleeDamageComponent> ent, ref MeleeHitEvent args)
{
// Resets combo state
void Reset()
{
ent.Comp.HitEntities.Clear();
ent.Comp.CurrentHeavyAttacks = 0;
Dirty(ent);
}
// No hits this swing → reset
if (args.HitEntities.Count == 0)
{
Reset();
return;
}
var comp = ent.Comp;
// Not enough heavy attacks accumulated yet
if (comp.CurrentHeavyAttacks < comp.HeavyAttackNeed)
{
// Light attack before threshold → reset combo
if (!args.CP14Heavy)
{
Reset();
return;
}
// Heavy attack: track overlapping targets across swings
if (comp.HitEntities.Count == 0)
{
// First heavy: initialize the set with current hits
comp.HitEntities.UnionWith(args.HitEntities);
}
else
{
// Subsequent heavy: keep only targets hit every time
comp.HitEntities.IntersectWith(args.HitEntities);
// Diverged to different targets → reset
if (comp.HitEntities.Count == 0)
{
Reset();
return;
}
}
comp.CurrentHeavyAttacks++;
Dirty(ent);
return;
}
// Light attack after enough heavies → check if it hits any tracked target
if (comp.HitEntities.Overlaps(args.HitEntities) && !args.CP14Heavy)
{
if (_timing.IsFirstTimePredicted)
{
_audio.PlayPredicted(comp.Sound, ent, args.User);
args.BonusDamage += comp.BonusDamage;
// Visual feedback on every hit entity this swing
foreach (var hit in args.HitEntities)
{
PredictedSpawnAtPosition(comp.VFX, Transform(hit).Coordinates);
}
}
Reset();
}
}
private void OnDistanceBonusDamage(Entity<CP14BonusDistanceMeleeDamageComponent> ent, ref MeleeHitEvent args)
{
var critical = true;
if (args.HitEntities.Count == 0)
return;
var userPos = _transform.GetWorldPosition(args.User);
//Crit only if all targets are at distance
foreach (var hit in args.HitEntities)
{
var targetPos = _transform.GetWorldPosition(hit);
var distance = (userPos - targetPos).Length();
if (distance < ent.Comp.MinDistance)
{
critical = false;
break;
}
}
if (!critical)
return;
if (!_timing.IsFirstTimePredicted)
return;
_audio.PlayPredicted(ent.Comp.Sound, ent, args.User);
args.BonusDamage += ent.Comp.BonusDamage;
//Visual effect!
foreach (var hit in args.HitEntities)
{
PredictedSpawnAtPosition(ent.Comp.VFX, Transform(hit).Coordinates);
}
}
private void OnMeleeHit(Entity<CP14MeleeSelfDamageComponent> ent, ref MeleeHitEvent args)
{
if (!args.IsHit)
return;
if (args.HitEntities.Count == 0)
return;
_damageable.TryChangeDamage(ent, ent.Comp.DamageToSelf);
}
}

View File

@@ -54,7 +54,7 @@ public abstract partial class CP14SharedTradingPlatformSystem : EntitySystem
var repComp = EnsureComp<CP14TradingReputationComponent>(args.User);
repComp.Reputation.TryAdd(ent.Comp.Faction, 0);
_audio.PlayLocal(new SoundCollectionSpecifier("CP14CoinImpact"), args.User, args.User);
_popup.PopupPredicted(Loc.GetString("cp14-trading-contract-use", ("name", Loc.GetString(indexedFaction.Name))), args.User, args.User);
_popup.PopupClient(Loc.GetString("cp14-trading-contract-use", ("name", Loc.GetString(indexedFaction.Name))), args.User, args.User);
if (_net.IsServer)
QueueDel(ent);

View File

@@ -32,7 +32,7 @@ public sealed partial class CP14VampireClanHeartComponent : Component
public FixedPoint2 Level4 = 21f;
[DataField]
public FixedPoint2 EssenceRegenPerLevel = 0.1f;
public FixedPoint2 EssenceRegen = 0.2f;
[DataField]
public TimeSpan RegenFrequency = TimeSpan.FromMinutes(1);