// namespace Supercell.Laser.Logic.Battle.Objects // { // using Supercell.Laser.Logic.Battle.Level; // using Supercell.Laser.Logic.Data; // using Supercell.Laser.Titan.DataStream; // using Supercell.Laser.Titan.Debug; // using Supercell.Laser.Titan.Math; // public class Projectile : GameObject // { // public ProjectileData ProjectileData => DataTables.Get(DataType.Projectile).GetDataByGlobalId(DataId); // private List AlreadyDamagedObjectsGlobalIds; // private GameObject Source; // private int Angle; // private int Damage; // private int CastingTime; // private int SpawnX; // private int SpawnY; // private int TicksActive; // private bool ShouldDestructImmediately; // private bool IsUltiWeapon; // private LogicVector2 TargetPosition; // private int m_destroyedTicks; // private CharacterData SummonedCharacter; // public int MaxRange; // public Projectile(int classId, int instanceId) : base(classId, instanceId) // { // TicksActive = -1; // FullTravelTicks = -1; // Z = 500; // TargetPosition = new LogicVector2(); // AlreadyDamagedObjectsGlobalIds = new List(); // } // private int m_totalDelta; // public override void Tick() // { // if (IsDestroyed()) // { // if (m_destroyedTicks < 1) // { // TargetReached(); // } // m_destroyedTicks++; // return; // } // if (!ProjectileData.Indirect) // { // if (m_totalDelta > CastingTime * 180) return; // } // int deltaX = (int)(((float)LogicMath.Cos(Angle) / 20000) * ProjectileData.Speed); // int deltaY = (int)(((float)LogicMath.Sin(Angle) / 20000) * ProjectileData.Speed); // m_totalDelta += ProjectileData.Speed / 20; // Position.X += deltaX; // Position.Y += deltaY; // TileMap tileMap = GameObjectManager.GetBattle().GetTileMap(); // Tile tile = tileMap.GetTile(TileMap.LogicToTile(Position.X), TileMap.LogicToTile(Position.Y), true); // if (tile == null) // { // ShouldDestructImmediately = true; // return; // } // if (!ProjectileData.Indirect) // { // if (!tile.IsDestructed() && tile.Data.BlocksProjectiles && !(tile.Data.IsDestructibleNormalWeapon || (tile.Data.IsDestructible && ProjectileData.PiercesEnvironment))) // { // ShouldDestructImmediately = true; // } // else if (tile.Data.IsDestructibleNormalWeapon) // { // tile.Destruct(); // } // else if (tile.Data.IsDestructible && ProjectileData.PiercesEnvironment) // { // tile.Destruct(); // } // } // if (Position.X <= 250 || Position.Y <= 250) ShouldDestructImmediately = true; // if (!ProjectileData.Indirect) // { // HandleCollisions(); // } // if (ProjectileData.Indirect) // { // if (TicksActive < (FullTravelTicks / 2)) // { // Z += (ProjectileData.Gravity / 20) * (FullTravelTicks - TicksActive); // } // else // { // int tmp = FullTravelTicks - TicksActive; // int deltaZ = (ProjectileData.Gravity / 20) * (TicksActive - tmp); // if (deltaZ > 0) Z -= deltaZ; // } // if (TicksActive >= FullTravelTicks - 1) ShouldDestructImmediately = true; // } // if (!GameObjectManager.GetBattle().IsInPlayArea(Position.X, Position.Y)) // { // ShouldDestructImmediately = true; // SetForcedInvisible(); // } // TicksActive++; // } // private int FullTravelTicks; // private bool IsPiperMainAttack() // { // Character sourceCharacter = Source as Character; // if (sourceCharacter == null || sourceCharacter.CharacterData == null) // { // return false; // } // return !IsUltiWeapon && // sourceCharacter.CharacterData.Name == "Sniper" && // (ProjectileData.Name == "SniperProjectile" || ProjectileData.Name == "Piper002Projectile"); // } // private int GetScaledDamage() // { // if (!IsPiperMainAttack()) // { // return Damage; // } // int traveledDistance = Position.GetDistance(new LogicVector2(SpawnX, SpawnY)); // int maxTravelDistance = LogicMath.Max(1, CastingTime * 180); // float travelRatio = LogicMath.Min(1000, (traveledDistance * 1000) / maxTravelDistance) / 1000f; // float damageMultiplier = 0.30f + (0.70f * travelRatio); // return LogicMath.Max(1, (int)(Damage * damageMultiplier)); // } // public void SetTargetPosition(int x, int y) // { // TargetPosition.Set(x, y); // } // public void SetSummonedCharacter(CharacterData data) // { // SummonedCharacter = data; // } // private void HandleCollisions() // { // foreach (GameObject gameObject in GameObjectManager.GetGameObjects()) // { // if (gameObject == null) continue; // if (gameObject.GetObjectType() != 1) continue; // if (!gameObject.IsAlive()) continue; // if (AlreadyDamagedObjectsGlobalIds.Contains(gameObject.GetGlobalID())) continue; // int teamIndex = gameObject.GetIndex() / 16; // if (teamIndex == this.GetIndex() / 16 && ProjectileData.HealOwnPercent == 0) continue; // if (teamIndex != this.GetIndex() / 16 && ProjectileData.HealOwnPercent != 0) continue; // int radius1 = gameObject.GetRadius(); // int radius2 = this.GetRadius(); // if (Position.GetDistance(gameObject.GetPosition()) <= radius1 + radius2) // { // // Collision! // if (!ProjectileData.PiercesCharacters) ShouldDestructImmediately = true; // AlreadyDamagedObjectsGlobalIds.Add(gameObject.GetGlobalID()); // Character character = (Character)gameObject; // int appliedDamage = GetScaledDamage(); // if (ProjectileData.Name == "RocketGirlProjectile") continue; // if (ProjectileData.HealOwnPercent == 0) character.CauseDamage((Character)Source, appliedDamage); // else character.CauseDamage((Character)Source, -appliedDamage); // if (ProjectileData.PoisonType != 0) // { // character.AddPoison((Character)Source, ((int)(((float)ProjectileData.PoisonDamagePercent / 100) * (float)appliedDamage)), 4, ProjectileData.PoisonType, false); // } // if (ProjectileData.Name == "ShotgunGirlUltiProjectile") // { // character.ApplyKnockback(Source.GetX(), Source.GetY(), 200); // character.ApplySlow(12); // } // if (!ProjectileData.PiercesCharacters) return; // } // } // } // private void TargetReached() // { // if (ProjectileData.SpawnAreaEffectObject != null) // { // CreateAreaEffect(ProjectileData.SpawnAreaEffectObject); // } // if (ProjectileData.SpawnAreaEffectObject2 != null) // { // CreateAreaEffect(ProjectileData.SpawnAreaEffectObject2); // } // if (SummonedCharacter != null) // { // Character character = new Character(16, SummonedCharacter.GetInstanceId()); // character.SetPosition(GetX(), GetY(), 0); // character.SetIndex(Source.GetIndex()); // GameObjectManager.AddGameObject(character); // } // } // public override void OnDestruct() // { // ; // } // private void CreateAreaEffect(string name) // { // AreaEffectData data = DataTables.Get(DataType.AreaEffect).GetData(name); // AreaEffect effect = new AreaEffect(17, data.GetInstanceId()); // effect.SetPosition(GetX(), GetY(), 0); // effect.SetIndex(GetIndex()); // effect.SetDamage(Damage); // effect.SetSource((Character)Source); // GameObjectManager.AddGameObject(effect); // } // private bool IsDestroyed() // { // return ((m_totalDelta > CastingTime * 180 && !ProjectileData.Indirect) || ShouldDestructImmediately); // } // public override bool ShouldDestruct() // { // return ((m_totalDelta > CastingTime * 180 && !ProjectileData.Indirect) || ShouldDestructImmediately) && m_destroyedTicks > 2; // } // public override void Encode(BitStream bitStream, bool isOwnObject, int visionTeam) // { // base.Encode(bitStream, isOwnObject, visionTeam); // int effect = 0; // if (m_totalDelta > CastingTime * 180 && !ProjectileData.Indirect) effect = 1; // if (ShouldDestructImmediately) effect = 3; // bitStream.WritePositiveIntMax7(effect); // next effect // bitStream.WriteBoolean(ProjectileData.IsBouncing); // if (ProjectileData.IsBouncing) // { // bitStream.WritePositiveIntMax1023(0); // } // if (ProjectileData.TriggerWithDelayMs != 0 || ProjectileData.PreExplosionTimeMs != 0) // bitStream.WritePositiveVIntMax65535(0); // if (ProjectileData.PreExplosionTimeMs != 0) // { // bitStream.WritePositiveVIntMax65535(0); // bitStream.WritePositiveVIntMax65535(0); // } // bitStream.WritePositiveIntMax1023(0); // Total path // if (ProjectileData.Rendering != "DoNotRotateClip") // bitStream.WritePositiveIntMax511(Angle); // bitStream.WriteBoolean(false); // } // public void ShootProjectile(int angle, GameObject owner, int damage, int castingTime, bool isUlti) // { // TicksActive = 0; // Angle = angle; // Damage = damage; // CastingTime = castingTime; // Source = owner; // SetIndex(owner.GetIndex()); // Position.X = owner.GetX(); // Position.Y = owner.GetY(); // SpawnX = Position.X; // SpawnY = Position.Y; // IsUltiWeapon = isUlti; // // Вычисляем FullTravelTicks сразу при спавне, пока позиция совпадает со стартовой // if (ProjectileData.Indirect) // { // int distance = LogicMath.Max(1, new LogicVector2(SpawnX, SpawnY).GetDistance(TargetPosition)); // int speedPerTick = LogicMath.Max(1, ProjectileData.Speed / 20); // FullTravelTicks = distance / speedPerTick; // FullTravelTicks = LogicMath.Min(FullTravelTicks, MaxRange); // } // } // public override int GetObjectType() // { // return 2; // } // } // } namespace Supercell.Laser.Logic.Battle.Objects { using System.Collections.Generic; using Supercell.Laser.Logic.Battle.Component; using Supercell.Laser.Logic.Battle.Level; using Supercell.Laser.Logic.Data; using Supercell.Laser.Titan.DataStream; using Supercell.Laser.Titan.Debug; using Supercell.Laser.Titan.Math; public class Projectile : GameObject { public ProjectileData ProjectileData => DataTables.Get(DataType.Projectile).GetDataByGlobalId(DataId); private List AlreadyDamagedObjectsGlobalIds; private GameObject Source; private int Angle; private int Damage; private int CastingTime; private int SpawnX; private int SpawnY; private int TicksActive; private bool ShouldDestructImmediately; private bool IsUltiWeapon; private LogicVector2 TargetPosition; private int m_destroyedTicks; private CharacterData SummonedCharacter; public int MaxRange; private int m_totalDelta; private int FullTravelTicks; // Флаг возвращения (для Карла) private bool m_isReturning; public Projectile(int classId, int instanceId) : base(classId, instanceId) { TicksActive = -1; FullTravelTicks = -1; Z = 500; TargetPosition = new LogicVector2(); AlreadyDamagedObjectsGlobalIds = new List(); } public override void Tick() { if (IsDestroyed()) { if (m_destroyedTicks < 1) { TargetReached(); } m_destroyedTicks++; return; } // ЛОГИКА ВОЗВРАТА БУМЕРАНГА if (ProjectileData.IsBoomerang && m_isReturning) { if (Source == null || !Source.IsAlive()) { ShouldDestructImmediately = true; return; } // Пересчитываем угол прямо на владельца Angle = LogicMath.GetAngle(Source.GetX() - Position.X, Source.GetY() - Position.Y); // Летим обратно (чуть быстрее, как в оригинале) int speed = ProjectileData.Speed + 300; int deltaXRet = (int)(((float)LogicMath.Cos(Angle) / 20000) * speed); int deltaYRet = (int)(((float)LogicMath.Sin(Angle) / 20000) * speed); Position.X += deltaXRet; Position.Y += deltaYRet; // Если долетел до владельца if (Position.GetDistance(Source.GetPosition()) <= Source.GetRadius() + GetRadius() + 100) { // Выставляем флаг на уничтожение. // В следующем тике вызовется IsDestroyed -> TargetReached -> Выдастся патрон ShouldDestructImmediately = true; return; } HandleCollisions(); TicksActive++; return; // На обратном пути игнорируем стены } // ОБЫЧНЫЙ ПОЛЕТ ТУДА if (!ProjectileData.Indirect) { // Если достиг максимальной дальности if (m_totalDelta > CastingTime * 180) { if (ProjectileData.IsBoomerang) { TurnAroundBoomerang(); return; } else { ShouldDestructImmediately = true; return; } } } int deltaX = (int)(((float)LogicMath.Cos(Angle) / 20000) * ProjectileData.Speed); int deltaY = (int)(((float)LogicMath.Sin(Angle) / 20000) * ProjectileData.Speed); m_totalDelta += ProjectileData.Speed / 20; Position.X += deltaX; Position.Y += deltaY; TileMap tileMap = GameObjectManager.GetBattle().GetTileMap(); Tile tile = tileMap.GetTile(TileMap.LogicToTile(Position.X), TileMap.LogicToTile(Position.Y), true); if (tile == null) { if (ProjectileData.IsBoomerang) TurnAroundBoomerang(); else ShouldDestructImmediately = true; return; } if (!ProjectileData.Indirect) { // Столкновение со стеной if (!tile.IsDestructed() && tile.Data.BlocksProjectiles && !(tile.Data.IsDestructibleNormalWeapon || (tile.Data.IsDestructible && ProjectileData.PiercesEnvironment))) { if (ProjectileData.IsBoomerang) TurnAroundBoomerang(); else ShouldDestructImmediately = true; } else if (tile.Data.IsDestructibleNormalWeapon || (tile.Data.IsDestructible && ProjectileData.PiercesEnvironment)) { tile.Destruct(); } } // Вылет за границы карты if (Position.X <= 250 || Position.Y <= 250) { if (ProjectileData.IsBoomerang) TurnAroundBoomerang(); else ShouldDestructImmediately = true; } if (!ProjectileData.Indirect) { HandleCollisions(); } if (ProjectileData.Indirect) { if (TicksActive < (FullTravelTicks / 2)) { Z += (ProjectileData.Gravity / 20) * (FullTravelTicks - TicksActive); } else { int tmp = FullTravelTicks - TicksActive; int deltaZ = (ProjectileData.Gravity / 20) * (TicksActive - tmp); if (deltaZ > 0) Z -= deltaZ; } if (TicksActive >= FullTravelTicks - 1) ShouldDestructImmediately = true; } if (!GameObjectManager.GetBattle().IsInPlayArea(Position.X, Position.Y)) { if (ProjectileData.IsBoomerang) TurnAroundBoomerang(); else { ShouldDestructImmediately = true; SetForcedInvisible(); } } TicksActive++; } // РАЗВОРОТ БУМЕРАНГА private void TurnAroundBoomerang() { if (!m_isReturning) { m_isReturning = true; // Очищаем цели, чтобы кирка нанесла урон еще раз на обратном пути AlreadyDamagedObjectsGlobalIds.Clear(); } } private bool IsPiperMainAttack() { Character sourceCharacter = Source as Character; if (sourceCharacter == null || sourceCharacter.CharacterData == null) { return false; } return !IsUltiWeapon && sourceCharacter.CharacterData.Name == "Sniper" && (ProjectileData.Name == "SniperProjectile" || ProjectileData.Name == "Piper002Projectile"); } private int GetScaledDamage() { if (!IsPiperMainAttack()) { return Damage; } int traveledDistance = Position.GetDistance(new LogicVector2(SpawnX, SpawnY)); int maxTravelDistance = LogicMath.Max(1, CastingTime * 180); float travelRatio = LogicMath.Min(1000, (traveledDistance * 1000) / maxTravelDistance) / 1000f; float damageMultiplier = 0.30f + (0.70f * travelRatio); return LogicMath.Max(1, (int)(Damage * damageMultiplier)); } public void SetTargetPosition(int x, int y) { TargetPosition.Set(x, y); } public void SetSummonedCharacter(CharacterData data) { SummonedCharacter = data; } private void HandleCollisions() { foreach (GameObject gameObject in GameObjectManager.GetGameObjects()) { if (gameObject == null || gameObject.GetObjectType() != 1 || !gameObject.IsAlive()) continue; if (AlreadyDamagedObjectsGlobalIds.Contains(gameObject.GetGlobalID())) continue; int teamIndex = gameObject.GetIndex() / 16; if (teamIndex == this.GetIndex() / 16 && ProjectileData.HealOwnPercent == 0 && ProjectileData.Name != "IceDudeProjectile") continue; if (teamIndex != this.GetIndex() / 16 && ProjectileData.HealOwnPercent != 0) continue; int radius1 = gameObject.GetRadius(); int radius2 = this.GetRadius(); if (Position.GetDistance(gameObject.GetPosition()) <= radius1 + radius2) { // Бумеранг пролетает сквозь врагов (как пронзающий снаряд) if (!ProjectileData.PiercesCharacters && !ProjectileData.IsBoomerang) ShouldDestructImmediately = true; AlreadyDamagedObjectsGlobalIds.Add(gameObject.GetGlobalID()); Character target = (Character)gameObject; Character sourceChar = (Character)Source; int appliedDamage = GetScaledDamage(); if (ProjectileData.Name == "RocketGirlProjectile") continue; if (ProjectileData.HealOwnPercent == 0) { target.CauseDamage(sourceChar, appliedDamage); } else { int healAmount = (appliedDamage * ProjectileData.HealOwnPercent) / 100; if (healAmount == 0) healAmount = appliedDamage; target.CauseDamage(sourceChar, -healAmount); } if (ProjectileData.PoisonType != 0) { target.AddPoison(sourceChar, (int)(((float)ProjectileData.PoisonDamagePercent / 100) * (float)appliedDamage), 4, ProjectileData.PoisonType, false); } if (ProjectileData.Name == "ShotgunGirlUltiProjectile") { target.ApplyKnockback(Source.GetX(), Source.GetY(), 200); target.ApplySlow(12); } if (!ProjectileData.PiercesCharacters && !ProjectileData.IsBoomerang) return; } } } private void TargetReached() { // Выдаем патрон, когда снаряд уничтожается if (ProjectileData.IsBoomerang && Source != null && Source.IsAlive()) { Character sourceChar = Source as Character; if (sourceChar != null && sourceChar.GetWeaponSkill() != null) { sourceChar.GetWeaponSkill().RestoreCharge(); } } if (ProjectileData.SpawnAreaEffectObject != null) { CreateAreaEffect(ProjectileData.SpawnAreaEffectObject); } if (ProjectileData.SpawnAreaEffectObject2 != null) { CreateAreaEffect(ProjectileData.SpawnAreaEffectObject2); } if (SummonedCharacter != null) { Character character = new Character(16, SummonedCharacter.GetInstanceId()); character.SetPosition(GetX(), GetY(), 0); character.SetIndex(Source.GetIndex()); GameObjectManager.AddGameObject(character); } } public override void OnDestruct() { } private void CreateAreaEffect(string name) { AreaEffectData data = DataTables.Get(DataType.AreaEffect).GetData(name); if (data == null) return; AreaEffect effect = new AreaEffect(17, data.GetInstanceId()); effect.SetPosition(GetX(), GetY(), 0); effect.SetIndex(GetIndex()); effect.SetDamage(Damage); effect.SetSource((Character)Source); GameObjectManager.AddGameObject(effect); } private bool IsDestroyed() { // Бумеранг не уничтожается просто потому что пролетел макс. дистанцию (уничтожается только вручную ShouldDestructImmediately=true) if (ProjectileData.IsBoomerang) return ShouldDestructImmediately; return ((m_totalDelta > CastingTime * 180 && !ProjectileData.Indirect) || ShouldDestructImmediately); } public override bool ShouldDestruct() { if (ProjectileData.IsBoomerang) return ShouldDestructImmediately && m_destroyedTicks > 2; return ((m_totalDelta > CastingTime * 180 && !ProjectileData.Indirect) || ShouldDestructImmediately) && m_destroyedTicks > 2; } public override void Encode(BitStream bitStream, bool isOwnObject, int visionTeam) { base.Encode(bitStream, isOwnObject, visionTeam); int effect = 0; if (m_totalDelta > CastingTime * 180 && !ProjectileData.Indirect && !ProjectileData.IsBoomerang) effect = 1; if (ShouldDestructImmediately) effect = 3; bitStream.WritePositiveIntMax7(effect); // next effect bitStream.WriteBoolean(ProjectileData.IsBouncing); if (ProjectileData.IsBouncing) { bitStream.WritePositiveIntMax1023(0); } if (ProjectileData.TriggerWithDelayMs != 0 || ProjectileData.PreExplosionTimeMs != 0) bitStream.WritePositiveVIntMax65535(0); if (ProjectileData.PreExplosionTimeMs != 0) { bitStream.WritePositiveVIntMax65535(0); bitStream.WritePositiveVIntMax65535(0); } bitStream.WritePositiveIntMax1023(0); // Total path if (ProjectileData.Rendering != "DoNotRotateClip") bitStream.WritePositiveIntMax511(Angle); bitStream.WriteBoolean(false); } public void ShootProjectile(int angle, GameObject owner, int damage, int castingTime, bool isUlti) { TicksActive = 0; Angle = angle; Damage = damage; CastingTime = castingTime; Source = owner; SetIndex(owner.GetIndex()); Position.X = owner.GetX(); Position.Y = owner.GetY(); SpawnX = Position.X; SpawnY = Position.Y; IsUltiWeapon = isUlti; m_isReturning = false; // При спавне сбрасываем if (ProjectileData.Indirect) { int distance = LogicMath.Max(1, new LogicVector2(SpawnX, SpawnY).GetDistance(TargetPosition)); int speedPerTick = LogicMath.Max(1, ProjectileData.Speed / 20); FullTravelTicks = distance / speedPerTick; FullTravelTicks = LogicMath.Min(FullTravelTicks, MaxRange); } } public override int GetObjectType() { return 2; } } }