private static void SharedCallOnWeaponShot(ICharacter character, IProtoItemWeapon protoWeapon) { if (IsClient) { // start firing weapon on Client-side WeaponSystemClientDisplay.OnWeaponShot(character, protoWeapon: protoWeapon, fallbackProtoCharacter: character.ProtoCharacter, fallbackPosition: character.Position); } else // if IsServer { using var observers = Api.Shared.GetTempList <ICharacter>(); var eventNetworkRadius = (byte)Math.Max( 15, Math.Ceiling(protoWeapon.SoundPresetWeaponDistance.max)); Server.World.GetCharactersInRadius(character.TilePosition, observers, radius: eventNetworkRadius, onlyPlayers: true); observers.Remove(character); Instance.CallClient(observers, _ => _.ClientRemote_OnWeaponShot(character, protoWeapon, character.ProtoCharacter, character.Position)); } }
public WeaponReloadingState( ICharacter character, IItem item, IProtoItemWeapon itemProto, IProtoItemAmmo protoItemAmmo) { this.Item = item; this.ProtoItemAmmo = protoItemAmmo; var reloadDuration = itemProto.AmmoReloadDuration; if (reloadDuration > 0) { var statName = itemProto.WeaponSkillProto?.StatNameReloadingSpeedMultiplier; if (statName.HasValue) { reloadDuration *= character.SharedGetFinalStatMultiplier(statName.Value); } reloadDuration = Api.Shared.RoundDurationByServerFrameDuration(reloadDuration); } this.SecondsToReloadRemains = reloadDuration; //Api.Logger.WriteDev($"Weapon will be reloaded in: {this.SecondsToReloadRemains:F2} seconds"); }
private void ClientRemote_OnWeaponHit(IProtoItemWeapon protoWeapon, WeaponHitData hitObject) { using (var tempList = Api.Shared.WrapObjectInTempList(hitObject)) { WeaponSystemClientDisplay.OnWeaponHit(protoWeapon, tempList); } }
protected override void ServerOnDegradeWeapon( ICharacter character, IItem weaponItem, IProtoItemWeapon protoWeapon, List <WeaponHitData> hitObjects) { if (hitObjects.Count == 0) { // no objects were hit return; } var decrease = this.DurabilityDecreasePerAction; foreach (var hit in hitObjects) { var protoObject = hit.WorldObject.ProtoWorldObject; if (protoObject is IProtoObjectWall || protoObject is IProtoObjectDoor || protoObject is IProtoObjectTradingStation) { // hit wall, door or station decrease *= 5; break; } } ItemDurabilitySystem.ServerModifyDurability( weaponItem, delta: -decrease); }
public static void TrySetWeapon( ICharacter character, IProtoItemWeapon protoWeapon, bool rebuildWeaponsCacheNow) { var privateState = character.GetPrivateState <CharacterMobPrivateState>(); var publicState = character.GetPublicState <CharacterMobPublicState>(); var weaponState = privateState.WeaponState; if (ReferenceEquals(weaponState.ProtoWeapon, protoWeapon)) { return; } if (weaponState.CooldownSecondsRemains > 0.001 || weaponState.DamageApplyDelaySecondsRemains > 0.001) { //Api.Logger.Dev("Weapon cooldown remains: " + weaponState.CooldownSecondsRemains); return; } weaponState.SharedSetWeaponProtoOnly(protoWeapon); publicState.SharedSetCurrentWeaponProtoOnly(protoWeapon); // can use the new selected mob weapon instantly weaponState.ReadySecondsRemains = weaponState.CooldownSecondsRemains = 0; if (!rebuildWeaponsCacheNow) { return; } WeaponSystem.SharedRebuildWeaponCache(character, weaponState); privateState.AttackRange = weaponState.WeaponCache.RangeMax; }
public ViewModelItemTooltipCompatibleAmmoControl(IProtoItemWeapon protoItemWeapon) { var list = protoItemWeapon.CompatibleAmmoProtos.ToList(); list.SortBy(e => e.Name); this.CompatibleAmmoProtos = list; }
public ViewModelItemTooltipCompatibleAmmoControl(IProtoItemWeapon protoItemWeapon) { this.CompatibleAmmoProtos = protoItemWeapon.CompatibleAmmoProtos .OrderBy(p => p.Name) .Select(p => p.Name) .ToList(); }
private static WeaponFinalCache ServerCreateWeaponFinalCacheForDrone( ICharacter characterOwner, IProtoItemWeapon protoMiningTool, IDynamicWorldObject objectDrone) { using var tempStatsCache = TempStatsCache.GetFromPool(); // fill only the skills cache from character (status effects have no effect) foreach (var pair in characterOwner.SharedGetSkills().Skills) { var protoSkill = pair.Key; var skillLevel = pair.Value.Level; protoSkill.FillEffectsCache(tempStatsCache, skillLevel); } var finalStatsCache = tempStatsCache.CalculateFinalStatsCache(); return(new WeaponFinalCache( characterOwner, finalStatsCache, weapon: null, protoWeapon: protoMiningTool, protoAmmo: null, damageDescription: protoMiningTool.OverrideDamageDescription, protoExplosive: null, objectDrone: objectDrone)); }
public override void ClientOnWeaponHitOrTrace( ICharacter firingCharacter, Vector2D worldPositionSource, IProtoItemWeapon protoWeapon, IProtoItemAmmo protoAmmo, IProtoCharacter protoCharacter, in Vector2Ushort fallbackCharacterPosition,
public static void OnWeaponHit(IProtoItemWeapon protoWeapon, IReadOnlyList <WeaponHitData> hitObjects) { foreach (var hitData in hitObjects) { var worldObject = hitData.WorldObject; if (worldObject?.ProtoGameObject == null) { // no such object in current context if (Api.IsEditor) { Api.Logger.Error("Unknown world object on OnWeaponHit(): " + worldObject); } else { Api.Logger.Warning("Unknown world object on OnWeaponHit(): " + worldObject); } continue; } var protoWorldObject = worldObject.ProtoWorldObject; var objectSoundMaterial = protoWorldObject.SharedGetObjectSoundMaterial(); var volume = SoundConstants.VolumeHit; // apply some volume variation volume *= RandomHelper.Range(0.8f, 1.0f); protoWeapon.SoundPresetHit.PlaySound( objectSoundMaterial, worldObject, volume: volume, pitch: RandomHelper.Range(0.95f, 1.05f)); } }
public static ItemTooltipCompatibleAmmoControl Create(IProtoItemWeapon protoItemWeapon) { return(new ItemTooltipCompatibleAmmoControl() { protoItemWeapon = protoItemWeapon }); }
protected override void ServerOnStaticObjectDestroyedByCharacter( ICharacter byCharacter, IProtoItemWeapon byWeaponProto, IStaticWorldObject targetObject) { base.ServerOnStaticObjectDestroyedByCharacter(byCharacter, byWeaponProto, targetObject); this.ServerOnExtractorDestroyedForDeposit(targetObject); }
protected override void ServerOnStaticObjectDestroyedByCharacter( ICharacter byCharacter, IProtoItemWeapon byWeaponProto, IStaticWorldObject targetObject) { base.ServerOnStaticObjectDestroyedByCharacter(byCharacter, byWeaponProto, targetObject); // it will spawn the drop items this.ServerOnDamageStageIncreased(byCharacter, byWeaponProto, targetObject, damageStage: DamageStagesCount); }
public static ItemTooltipWeaponStats Create( IItem item, IProtoItemWeapon protoItem) { return(new ItemTooltipWeaponStats() { item = item, protoItem = protoItem }); }
public DropItemContext( ICharacter character, IStaticWorldObject staticWorldObject, IProtoItemWeapon byWeaponProto = null) { this.ByWeaponProto = byWeaponProto; this.character = character; this.staticWorldObject = staticWorldObject; }
protected virtual void ServerOnStaticObjectDestroyedByCharacter( [CanBeNull] ICharacter byCharacter, [CanBeNull] IProtoItemWeapon byWeaponProto, IStaticWorldObject targetObject) { if (byCharacter != null) { ServerStaticObjectDestroyObserver.NotifyObjectDestroyed(byCharacter, targetObject); } }
public static void OnCharacterKilled( ICharacter targetCharacter, ICharacter attackerCharacter, IItem weapon, IProtoItemWeapon protoWeapon) { // killed! Api.Logger.Important( $"Character killed: {targetCharacter} by {attackerCharacter} with {weapon?.ToString() ?? protoWeapon?.ToString()}"); Api.SafeInvoke( () => CharacterKilled?.Invoke(attackerCharacter, targetCharacter)); }
protected override void ServerOnStaticObjectDestroyedByCharacter( ICharacter byCharacter, IProtoItemWeapon byWeaponProto, IStaticWorldObject targetObject) { base.ServerOnStaticObjectDestroyedByCharacter(byCharacter, byWeaponProto, targetObject); // drop chance and gained experience depends on the vegetation growth stage var growthProgressFraction = this.GrowthStagesCount > 0 ? GetPublicState(targetObject).GrowthStage / (double)this.GrowthStagesCount : 1; growthProgressFraction = MathHelper.Clamp(growthProgressFraction, 0.1, 1); try { var dropItemContext = new DropItemContext(byCharacter, targetObject); if (byWeaponProto is IProtoItemWeaponMelee) { // a melee weapon - try drop items to character var result = this.DroplistOnDestroy.TryDropToCharacter( byCharacter, dropItemContext, probabilityMultiplier: growthProgressFraction); if (result.IsEverythingCreated) { NotificationSystem.ServerSendItemsNotification(byCharacter, result); return; } result.Rollback(); } // not a melee weapon or cannot drop to character - drop on the ground only this.DroplistOnDestroy.TryDropToGround( targetObject.TilePosition, dropItemContext, probabilityMultiplier: growthProgressFraction, groundContainer: out _); } finally { if (byWeaponProto is IProtoItemToolWoodcutting) { // add experience proportional to the tree structure points (effectively - for the time spent on woodcutting) var exp = SkillWoodcutting.ExperienceAddPerStructurePoint; exp *= this.StructurePointsMax * growthProgressFraction; byCharacter?.ServerAddSkillExperience <SkillWoodcutting>(exp); } } }
private static void SetFiringAnimation( IProtoItemWeapon weaponProto, IComponentSkeleton skeletonRenderer, byte trackIndex, string fireAnimationName, double fireAnimationDuration, double fireInterval, bool mixWithCurrent, bool isLooped) { var currentFireAnimation = skeletonRenderer.GetCurrentAnimationName(trackIndex); if (currentFireAnimation == fireAnimationName) { // the same firing animation is already playing Api.Logger.Warning( "Will overwrite current attack animation: " + currentFireAnimation + " - usually it means that the DamageApplyDelaySeconds+FireIntervalSeconds is lower than the attack animation duration for " + weaponProto + " (they must be matching perfectly)."); } if (currentFireAnimation != null) { if (mixWithCurrent) { skeletonRenderer.SetAnimationLoopMode(trackIndex, isLooped: false); skeletonRenderer.RemoveAnimationTrackNextEntries(trackIndex); } else { skeletonRenderer.RemoveAnimationTrack(trackIndex); } } // cooldown is a padding duration which makes animation "stuck" on the last frame for the specified duration var cooldownDuration = isLooped // in looped animation we need to match its total duration to fire interval by using cooldown ? Math.Max(0, fireInterval - fireAnimationDuration) : 0; // non-looped animation no cooldown is necessary skeletonRenderer.AddAnimation( trackIndex, animationName: fireAnimationName, isLooped: isLooped, customDuration: (float)fireAnimationDuration, cooldownDuration: (float)cooldownDuration); }
private void ClientExplodeAt( IProtoItemWeapon protoWeapon, Vector2D shotSourcePosition, Vector2D explosionWorldPosition) { var timeToHit = WeaponSystemClientDisplay.SharedCalculateTimeToHit(protoWeapon.FireTracePreset ?? this.FireTracePreset, shotSourcePosition, explosionWorldPosition); ClientTimersSystem.AddAction(timeToHit, () => ExplosionHelper.ClientExplode(explosionWorldPosition, this.ExplosionPreset, this.ExplosionVolume)); }
/// <summary> /// Returns compatible with weapon ammo group by ammo type. /// </summary> private static List <IGrouping <IProtoItemAmmo, IItem> > SharedGetCompatibleAmmoGroups( ICharacter character, IProtoItemWeapon protoWeapon) { var compatibleAmmoProtos = protoWeapon.CompatibleAmmoProtos; var containerInventory = character.SharedGetPlayerContainerInventory(); var containerHotbar = character.SharedGetPlayerContainerHotbar(); var allItems = containerInventory.Items.Concat(containerHotbar.Items); return(allItems .Where(i => compatibleAmmoProtos.Contains(i.ProtoGameObject)) .GroupBy(a => (IProtoItemAmmo)a.ProtoItem) .ToList()); }
private static void SelectAiWeapon( ICharacter character, double distanceToTarget, IReadOnlyList <AiWeaponPreset> weaponList, out IProtoItemWeapon desiredProtoWeapon, out bool isWithinRange) { desiredProtoWeapon = null; isWithinRange = false; var privateState = character.GetPrivateState <CharacterMobPrivateState>(); var weaponState = privateState.WeaponState; if (weaponState.CooldownSecondsRemains > 0.001 || weaponState.DamageApplyDelaySecondsRemains > 0.001) { // cannot switch weapon now, try to use the currently selected weapon //Api.Logger.Dev("Weapon cooldown remains: " + weaponState.CooldownSecondsRemains // + " damageApplyDelaySecondsRemains: " + weaponState.DamageApplyDelaySecondsRemains); desiredProtoWeapon = weaponState.ProtoWeapon; foreach (var weaponPreset in weaponList) { if (ReferenceEquals(weaponPreset.ProtoWeapon, desiredProtoWeapon)) { isWithinRange = distanceToTarget < weaponPreset.MaxAttackRange; break; } } return; } // try to select the weapon from the list foreach (var weaponPreset in weaponList) { isWithinRange = distanceToTarget < weaponPreset.MaxAttackRange; if (!isWithinRange) { continue; } desiredProtoWeapon = weaponPreset.ProtoWeapon; ServerMobWeaponHelper.TrySetWeapon(character, desiredProtoWeapon, rebuildWeaponsCacheNow: false); break; } }
private void ClientRemote_OnWeaponShot( ICharacter whoFires, IProtoItemWeapon protoWeapon, IProtoCharacter fallbackProtoCharacter, Vector2D fallbackPosition) { if (whoFires != null && !whoFires.IsInitialized) { whoFires = null; } WeaponSystemClientDisplay.OnWeaponShot(whoFires, protoWeapon, fallbackProtoCharacter, fallbackPosition); }
private void ServerOnDamageStageIncreased( [CanBeNull] ICharacter byCharacter, IProtoItemWeapon byWeaponProto, IStaticWorldObject mineralObject, int damageStage) { Logger.Info( $"{mineralObject} current damage stage changed to {damageStage}. Dropping items for that stage", byCharacter); try { var dropItemsList = this.DropItemsConfig.GetForStage(damageStage); var dropItemContext = new DropItemContext(byCharacter, mineralObject, byWeaponProto); if (byWeaponProto is IProtoItemWeaponMelee) { var result = dropItemsList.TryDropToCharacter(byCharacter, dropItemContext); if (result.IsEverythingCreated) { NotificationSystem.ServerSendItemsNotification( byCharacter, result); return; } result.Rollback(); } // not a melee weapon or cannot drop to the character inventory - drop on the ground only dropItemsList.TryDropToGround(mineralObject.TilePosition, dropItemContext, out _); } finally { if (byWeaponProto is IProtoItemToolMining) { // add experience proportional to the mineral structure points (effectively - for the time spent on mining) var exp = SkillMining.ExperienceAddPerStructurePoint; exp *= this.StructurePointsMax / DamageStagesCount; byCharacter?.ServerAddSkillExperience <SkillMining>(exp); } } }
private void ClientRemote_OnWeaponHitOrTrace( ICharacter firingCharacter, IProtoItemWeapon protoWeapon, IProtoItemAmmo protoAmmo, IProtoCharacter protoCharacter, Vector2Ushort fallbackCharacterPosition, WeaponHitData[] hitObjects, Vector2D endPosition, bool endsWithHit) { WeaponSystemClientDisplay.ClientOnWeaponHitOrTrace(firingCharacter, protoWeapon, protoAmmo, protoCharacter, fallbackCharacterPosition, hitObjects, endPosition, endsWithHit); }
/// <summary> /// Some weapons, such as flintlock pistol or a musket/double-barreled shotgun /// might have a desync issue when they're reloaded on the server /// right after receiving a command to stop firing. /// So the shots done on the client will be not done on the server. /// To prevent this issue, this method detects such weapons and keeps the shots flow. /// </summary> public static bool IsResetsShotsDoneNumberOnReload(IProtoItemWeapon protoWeapon) { if (protoWeapon.AmmoCapacity == 0 || protoWeapon.AmmoConsumptionPerShot == 0) { return(true); } var shotsPerMagazine = protoWeapon.AmmoCapacity / protoWeapon.AmmoConsumptionPerShot; if (shotsPerMagazine <= 1 || shotsPerMagazine <= 2 && protoWeapon.FireInterval < 0.5) { // don't reset the shots done number for this weapon //Logger.Dev("Don't reset - shotsPerMagazine: " + shotsPerMagazine + " - " + protoWeapon.ShortId); return(false); } //Logger.Dev("Reset - shotsPerMagazine: " + shotsPerMagazine + " - " + protoWeapon.ShortId); return(true); }
private static void SharedCallOnWeaponShot( ICharacter character, IProtoItemWeapon protoWeapon) { if (IsClient) { // start firing weapon on Client-side WeaponSystemClientDisplay.ClientOnWeaponShot(character, partyId: 0, // not relevant here as it's the current player firing the weapon protoWeapon: protoWeapon, protoCharacter: character.ProtoCharacter, fallbackPosition: character.Position.ToVector2Ushort()); } else // if IsServer { using var observers = Shared.GetTempList <ICharacter>(); var eventNetworkRadius = (byte)Math.Max( 15, Math.Ceiling(protoWeapon.SoundPresetWeaponDistance.max)); Server.World.GetCharactersInRadius(character.TilePosition, observers, radius: eventNetworkRadius, onlyPlayers: true); observers.Remove(character); if (observers.Count > 0) { var partyId = PartySystem.ServerGetParty(character)?.Id ?? 0; Instance.CallClient(observers.AsList(), _ => _.ClientRemote_OnWeaponShot(character, partyId, protoWeapon, character.ProtoCharacter, character.Position.ToVector2Ushort())); } } }
protected override void ClientExplodeAt( IProtoItemWeapon protoWeapon, Vector2D shotSourcePosition, Vector2D explosionWorldPosition) { var timeToHit = WeaponSystemClientDisplay.SharedCalculateTimeToHit(protoWeapon.FireTracePreset ?? this.FireTracePreset, shotSourcePosition, explosionWorldPosition); ClientTimersSystem.AddAction( timeToHit, () => { SharedExplosionHelper.ClientExplode(explosionWorldPosition, this.ExplosionPreset, this.VolumeExplosion); // add more explosions in the X pattern as it's a fragmentation grenade ClientTimersSystem.AddAction( 0.2, () => { SharedExplosionHelper.ClientExplode(explosionWorldPosition + (1, 1), this.ExplosionPreset, volume: 0); SharedExplosionHelper.ClientExplode(explosionWorldPosition + (1, -1), this.ExplosionPreset, volume: 0); SharedExplosionHelper.ClientExplode(explosionWorldPosition + (-1, 1), this.ExplosionPreset, volume: 0); SharedExplosionHelper.ClientExplode(explosionWorldPosition + (-1, -1), this.ExplosionPreset, volume: 0); });
private static void RefreshStaticAttackAnimation( IComponentSkeleton skeletonRenderer, IProtoItemWeapon weapon, string firingAnimationName) { // ReSharper disable once CanExtractXamlLocalizableStringCSharp var staticFiringAnimationName = firingAnimationName + "_Static"; var lastStaticAnimationName = skeletonRenderer.GetLatestAddedAnimationName( AnimationTrackIndexes.ItemFiringStatic); if (lastStaticAnimationName == staticFiringAnimationName) { return; } if (skeletonRenderer.GetLatestAddedAnimationName(AnimationTrackIndexes.ItemFiring) == null) { // no current firing animation - no need the static animation return; } // need to update the static attack animation SetFiringAnimation( weapon, skeletonRenderer, AnimationTrackIndexes.ItemFiringStatic, staticFiringAnimationName, weapon.FireAnimationDuration, weapon.FireInterval, mixWithCurrent: true, isLooped: weapon.IsLoopedAttackAnimation); // synchronize static with non-static animation tracks skeletonRenderer.SetAnimationTime( AnimationTrackIndexes.ItemFiringStatic, skeletonRenderer.GetAnimationTime(AnimationTrackIndexes.ItemFiring)); }
public static void OnWeaponHit(IProtoItemWeapon protoWeapon, IReadOnlyList <WeaponHitData> hitObjects) { foreach (var hitData in hitObjects) { var worldObject = hitData.WorldObject; if (worldObject != null && !worldObject.IsInitialized) { worldObject = null; } var protoWorldObject = hitData.FallbackProtoWorldObject; var objectSoundMaterial = protoWorldObject.SharedGetObjectSoundMaterial(); var volume = SoundConstants.VolumeHit; // apply some volume variation volume *= RandomHelper.Range(0.8f, 1.0f); var pitch = RandomHelper.Range(0.95f, 1.05f); if (worldObject != null) { protoWeapon.SoundPresetHit.PlaySound( objectSoundMaterial, worldObject, volume: volume, pitch: pitch); } else { protoWeapon.SoundPresetHit.PlaySound( objectSoundMaterial, protoWorldObject, worldPosition: hitData.FallbackTilePosition.ToVector2D(), volume: volume, pitch: pitch); } } }