You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Duality;
using Duality.Editor;
using Duality.Resources;
using Duality.Audio;
using Duality.Drawing;
using Duality.Components;
using Duality.Components.Renderers;
using Duality.Components.Physics;
namespace DualStickSpaceShooter
{
[RequiredComponent(typeof(Transform))]
[RequiredComponent(typeof(SpriteRenderer))]
[RequiredComponent(typeof(RigidBody))]
public class Ship : Component, ICmpUpdatable, ICmpInitializable
{
private ContentRef<ShipBlueprint> blueprint = null;
private Vector2 targetThrust = Vector2.Zero;
private float targetAngle = 0.0f;
private float targetAngleRatio = 0.0f;
private bool isDead = false;
private float hitpoints = 1.0f;
private float weaponTimer = 0.0f;
private Player owner = null;
private ParticleEffect damageEffect = null;
[DontSerialize] private SoundInstance flightLoop = null;
public ContentRef<ShipBlueprint> Blueprint
{
get { return this.blueprint; }
set { this.blueprint = value; }
}
[EditorHintFlags(MemberFlags.Invisible)]
public Vector2 TargetThrust
{
get { return this.targetThrust; }
set { this.targetThrust = value; }
}
[EditorHintFlags(MemberFlags.Invisible)]
public float TargetAngle
{
get { return this.targetAngle; }
set { this.targetAngle = value; }
}
[EditorHintFlags(MemberFlags.Invisible)]
public float TargetAngleRatio
{
get { return this.targetAngleRatio; }
set { this.targetAngleRatio = value; }
}
public bool IsDead
{
get { return this.isDead; }
}
public float Hitpoints
{
get { return this.hitpoints; }
set { this.hitpoints = value; }
}
public Player Owner
{
get { return this.owner; }
set
{
this.owner = value;
this.UpdatePlayerColor();
}
}
public void DoDamage(float damage)
{
this.hitpoints -= damage / this.blueprint.Res.MaxHitpoints;
if (this.hitpoints < 0.0f) this.Die();
if (this.owner != null)
{
CameraController camControl = Scene.Current.FindComponent<CameraController>();
camControl.ShakeScreen(MathF.Pow(damage, 0.75f));
}
}
public void Die()
{
// Ignore, if already dead
if (this.isDead) return;
this.isDead = true;
// Notify everyone who is interested that we're dead
this.SendMessage(new ShipDeathMessage());
// Spawn death effects
ShipBlueprint blueprint = this.blueprint.Res;
if (blueprint.DeathEffects != null)
{
Transform transform = this.GameObj.Transform;
RigidBody body = this.GameObj.GetComponent<RigidBody>();
for (int i = 0; i < blueprint.DeathEffects.Length; i++)
{
Prefab deathEffectPrefab = blueprint.DeathEffects[i].Res;
GameObject effectObj = deathEffectPrefab.Instantiate(transform.Pos);
ParticleEffect effect = effectObj.GetComponent<ParticleEffect>();
if (effect != null && this.owner != null)
{
foreach (ParticleEmitter emitter in effect.Emitters)
{
if (emitter == null) continue;
emitter.MaxColor = this.owner.Color.ToHsva().WithValue(emitter.MaxColor.V);
emitter.MinColor = this.owner.Color.ToHsva().WithValue(emitter.MinColor.V);
emitter.BaseVel += new Vector3(body.LinearVelocity);
}
}
Scene.Current.AddObject(effectObj);
}
}
// Safely dispose the ships GameObject and set hitpoints to zero
this.hitpoints = 0.0f;
if (this.owner != null)
this.GameObj.Active = false;
else
this.GameObj.DisposeLater();
}
public void Revive()
{
// Ignore, if not dead
if (!this.isDead) return;
this.isDead = false;
// Make sure to reset the rigidbodies movement state
RigidBody body = this.GameObj.GetComponent<RigidBody>();
body.LinearVelocity = Vector2.Zero;
body.AngularVelocity = 0.0f;
// Activate the ship again and give it back all of its hitpoints
this.hitpoints = 1.0f;
this.GameObj.Active = true;
}
public void UpdatePlayerColor()
{
SpriteRenderer sprite = this.GameObj.GetComponent<SpriteRenderer>();
if (sprite != null)
{
if (this.owner != null)
sprite.ColorTint = this.owner.Color;
else
sprite.ColorTint = ColorRgba.White;
}
}
public void FireWeapon()
{
if (this.weaponTimer > 0.0f) return;
this.weaponTimer += this.blueprint.Res.WeaponDelay;
Transform transform = this.GameObj.Transform;
RigidBody body = this.GameObj.GetComponent<RigidBody>();
this.FireBullet(body, transform, new Vector2(0.0f, -15.0f), 0.0f);
}
private void FireBullet(RigidBody body, Transform transform, Vector2 localPos, float localAngle)
{
ShipBlueprint blueprint = this.blueprint.Res;
if (blueprint.BulletType == null) return;
Bullet bullet = blueprint.BulletType.Res.CreateBullet();
Vector2 recoilImpulse;
Vector2 worldPos = transform.GetWorldPoint(localPos);
bullet.Fire(this.owner, body.LinearVelocity, worldPos, transform.Angle + localAngle, out recoilImpulse);
body.ApplyWorldImpulse(recoilImpulse);
Scene.Current.AddObject(bullet.GameObj);
SoundInstance inst = null;
if (Player.AlivePlayers.Count() > 1)
inst = DualityApp.Sound.PlaySound3D(this.owner.WeaponSound, new Vector3(worldPos));
else
inst = DualityApp.Sound.PlaySound(this.owner.WeaponSound);
inst.Volume = MathF.Rnd.NextFloat(0.6f, 1.0f);
inst.Pitch = MathF.Rnd.NextFloat(0.9f, 1.11f);
}
void ICmpUpdatable.OnUpdate()
{
Transform transform = this.GameObj.Transform;
RigidBody body = this.GameObj.GetComponent<RigidBody>();
ShipBlueprint blueprint = this.blueprint.Res;
// Heal when damaged
if (this.hitpoints < 1.0f)
{
this.hitpoints = MathF.Clamp(this.hitpoints + blueprint.HealRate * Time.SPFMult * Time.TimeMult / blueprint.MaxHitpoints, 0.0f, 1.0f);
}
// Apply force according to the desired thrust
Vector2 actualVelocity = body.LinearVelocity;
Vector2 targetVelocity = this.targetThrust * blueprint.MaxSpeed;
Vector2 velocityDiff = (targetVelocity - actualVelocity);
float sameDirectionFactor = Vector2.Dot(
velocityDiff / MathF.Max(0.001f, velocityDiff.Length),
this.targetThrust / MathF.Max(0.001f, this.targetThrust.Length));
Vector2 thrusterActivity = this.targetThrust.Length * MathF.Max(sameDirectionFactor, 0.0f) * velocityDiff / MathF.Max(velocityDiff.Length, 1.0f);
if (thrusterActivity.Length > 0.00001f) // Don't wake physics without actually doing work
{
body.ApplyWorldForce(thrusterActivity * blueprint.ThrusterPower);
}
// Turn to the desired fire angle
if (this.targetAngleRatio > 0.0f)
{
float shortestTurnDirection = MathF.TurnDir(transform.Angle, this.targetAngle);
float shortestTurnLength = MathF.CircularDist(transform.Angle, this.targetAngle);
float turnDirection;
float turnLength;
if (MathF.Abs(body.AngularVelocity) > blueprint.MaxTurnSpeed * 0.25f)
{
turnDirection = MathF.Sign(body.AngularVelocity);
turnLength = (turnDirection == shortestTurnDirection) ? shortestTurnLength : (MathF.RadAngle360 - shortestTurnLength);
}
else
{
turnDirection = shortestTurnDirection;
turnLength = shortestTurnLength;
}
float turnSpeedRatio = MathF.Min(turnLength * 0.25f, MathF.RadAngle30) / MathF.RadAngle30;
float turnVelocity = turnSpeedRatio * turnDirection * blueprint.MaxTurnSpeed * this.targetAngleRatio;
float angularVelocityChange = (turnVelocity - body.AngularVelocity) * blueprint.TurnPower;
if (MathF.Abs(angularVelocityChange) > 0.0000001f) // Don't wake physics without actually doing work
{
body.AngularVelocity += angularVelocityChange * Time.TimeMult;
}
}
// Weapon cooldown
this.weaponTimer = MathF.Max(0.0f, this.weaponTimer - Time.MsPFMult * Time.TimeMult);
// Play the owners special flight sound, when available
if (this.owner != null && this.owner.FlightLoop != null)
{
SoundListener listener = Scene.Current.FindComponent<SoundListener>();
Vector3 listenerPos = listener.GameObj.Transform.Pos;
// Determine the target panning manually, because we don't want a true 3D sound here (doppler, falloff, ...)
float targetPanning;
if (listenerPos.Xy == transform.Pos.Xy || Player.AlivePlayers.Count() <= 1)
targetPanning = 0.0f;
else
targetPanning = -Vector2.Dot(Vector2.UnitX, (listenerPos - transform.Pos).Xy.Normalized);
// Determine the target volume
float targetVolume = MathF.Clamp(this.targetThrust.Length, 0.0f, 1.0f);
// Clean up disposed flight loop
if (this.flightLoop != null && this.flightLoop.Disposed)
this.flightLoop = null;
// Start the flight loop when requested
if (targetVolume > 0.0f && this.flightLoop == null)
{
if ((int)Time.MainTimer.TotalMilliseconds % 2976 <= (int)Time.MsPFMult)
{
this.flightLoop = DualityApp.Sound.PlaySound(this.owner.FlightLoop);
this.flightLoop.Looped = true;
}
}
// Configure existing flight loop
if (this.flightLoop != null)
{
this.flightLoop.Volume += (targetVolume - this.flightLoop.Volume) * 0.05f * Time.TimeMult;
this.flightLoop.Panning += (targetPanning - this.flightLoop.Panning) * 0.05f * Time.TimeMult;
}
}
// Display the damage effect when damaged
if (this.hitpoints < 0.85f && blueprint.DamageEffect != null)
{
// Create a new damage effect instance, if not present yet
if (this.damageEffect == null)
{
GameObject damageObj = blueprint.DamageEffect.Res.Instantiate(transform.Pos);
damageObj.Parent = this.GameObj;
this.damageEffect = damageObj.GetComponent<ParticleEffect>();
if (this.damageEffect == null) throw new NullReferenceException();
}
// Configure the damage effect
foreach (ParticleEmitter emitter in this.damageEffect.Emitters)
{
if (emitter == null) continue;
emitter.BurstDelay = new Range(50.0f + this.hitpoints * 50.0f, 100.0f + this.hitpoints * 300.0f);
if (this.owner != null)
{
ColorHsva targetColor = this.owner.Color.ToHsva();
emitter.MinColor = new ColorHsva(targetColor.H, targetColor.S, emitter.MinColor.V, emitter.MinColor.A);
emitter.MaxColor = new ColorHsva(targetColor.H, targetColor.S, emitter.MaxColor.V, emitter.MaxColor.A);
}
}
}
// Get rid of existing damage effects, if no longer needed
else if (this.damageEffect != null)
{
// Stop emitting and dispose when empty
foreach (ParticleEmitter emitter in this.damageEffect.Emitters)
{
if (emitter == null) continue;
emitter.BurstDelay = float.MaxValue;
}
this.damageEffect.DisposeWhenEmpty = true;
this.damageEffect = null;
}
}
void ICmpInitializable.OnInit(Component.InitContext context) {}
void ICmpInitializable.OnShutdown(Component.ShutdownContext context)
{
if (context == ShutdownContext.Deactivate)
{
if (this.flightLoop != null)
{
this.flightLoop.Dispose();
this.flightLoop = null;
}
}
}
}
}