diff --git a/src/Objects/Bullet.cs b/src/Objects/Bullet.cs index 7baa1d0..885527b 100644 --- a/src/Objects/Bullet.cs +++ b/src/Objects/Bullet.cs @@ -34,17 +34,23 @@ public class Bullet : Node2D { // TODO: Add a global game setting to specify whether shooter or server announces successful hit. // For now, server is the most straight-forward. Eventually, support client predictive movement? - if (!(this.GetGame() is Server) || !(obj.GetNodeOrNull("Sprite") is Sprite sprite)) return; + if (!(this.GetGame() is Server)) return; var world = this.GetWorld(); - var path = world.GetPathTo(sprite); var color = new Color(Color, (1 + Color.a) / 2); - RPC.Reliable(world.GetPlayersTracking(BlockPos.FromVector(obj.GlobalPosition).ToChunkPos()), - world.SpawnHit, path, hitPosition, color); - if (obj is Player player) { - var rangeFactor = Math.Min(1.0F, (MaximumRange - _distance) / (MaximumRange - EffectiveRange)); - player.Health -= Damage * rangeFactor; + if (obj.GetParent() is Chunk chunk) { + var path = world.GetPathTo(chunk); + var to = world.GetPlayersTracking(chunk.ChunkPos); + RPC.Reliable(to, world.SpawnHit, path, hitPosition, color); + } else if (obj.GetNodeOrNull("Sprite") is Sprite sprite) { + var path = world.GetPathTo(sprite); + var to = world.GetPlayersTracking(BlockPos.FromVector(obj.GlobalPosition).ToChunkPos()); + RPC.Reliable(to, world.SpawnHit, path, hitPosition, color); + if (obj is Player player) { + var rangeFactor = Math.Min(1.0F, (MaximumRange - _distance) / (MaximumRange - EffectiveRange)); + player.Health -= Damage * rangeFactor; + } + // TODO: Also spawn a ghost of the player who was hit so they can see where they got shot? } - // TODO: Also spawn a ghost of the player who was hit so they can see where they got shot? } public override void _Ready() diff --git a/src/Objects/HitDecal.cs b/src/Objects/HitDecal.cs index 00758f7..f553d11 100644 --- a/src/Objects/HitDecal.cs +++ b/src/Objects/HitDecal.cs @@ -34,4 +34,37 @@ public class HitDecal : Sprite if (Modulate.a <= 0) this.RemoveFromParent(); } } + + public static void Spawn(World world, NodePath path, Vector2 hitPosition, Color color) + { + var decal = GD.Load("res://gfx/hit_decal.png"); + var node = world.GetNode(path); + switch (node) { + case Sprite sprite: + node.AddChild(new HitDecal(decal, sprite.Texture, hitPosition, color)); + break; + case Chunk chunk: + hitPosition += chunk.Position; + var start = BlockPos.FromVector((hitPosition - decal.GetSize() / 2).Floor()); + var end = BlockPos.FromVector((hitPosition + decal.GetSize() / 2).Ceil()); + for (var x = start.X; x <= end.X; x++) + for (var y = start.Y; y <= end.Y; y++) { + var blockPos = new BlockPos(x, y); + var texture = world[blockPos].Get().Texture; + if (texture == null) continue; + world[blockPos].GetOrCreate().AddChild(new HitDecal( + decal, texture, hitPosition - blockPos.ToVector(), color)); + } + break; + } + } +} + +public class HitDecals : Node2D, INotifyChildRemoved +{ + public void OnChildRemoved(Node child) + { + if (GetChildCount() == 0) + this.RemoveFromParent(); + } } diff --git a/src/Utility/Extensions.cs b/src/Utility/Extensions.cs index 424a099..4a12dfc 100644 --- a/src/Utility/Extensions.cs +++ b/src/Utility/Extensions.cs @@ -16,6 +16,10 @@ public static class Extensions public static IEnumerable GetChildren(this Node node) => node.GetChildren().Cast(); + public static T GetOrCreateChild(this Node node) + where T : Node, new() => node.GetOrCreateChild(typeof(T).Name, () => new T()); + public static T GetOrCreateChild(this Node node, string name) + where T : Node, new() => node.GetOrCreateChild(name, () => new T()); public static T GetOrCreateChild(this Node node, string name, Func createFunc) where T : Node { @@ -39,8 +43,10 @@ public static class Extensions } public static void RemoveFromParent(this Node node) { + var notifyParent = node.GetParent() as INotifyChildRemoved; node.GetParent().RemoveChild(node); node.QueueFree(); + notifyParent?.OnChildRemoved(node); } public static float NextFloat(this Random random) @@ -59,3 +65,11 @@ public static class Extensions public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } } + +/// When a child is removed from this Node using +/// , +/// the OnChildRemoved method is called. +public interface INotifyChildRemoved +{ + void OnChildRemoved(Node child); +} diff --git a/src/World/Block/BlockEntity.cs b/src/World/Block/BlockEntity.cs index 4f5c973..226a404 100644 --- a/src/World/Block/BlockEntity.cs +++ b/src/World/Block/BlockEntity.cs @@ -1,6 +1,19 @@ using Godot; -public class BlockEntity : Node2D +// TODO: Add saving of block entities. +public class BlockEntity : Node2D, INotifyChildRemoved { + public BlockRef Block { get; } + public BlockEntity(BlockRef block) + { + Block = block; + Position = block.Position.GlobalToChunkRel().ToVector(); + } + + public void OnChildRemoved(Node child) + { + if (GetChildCount() == 0) + this.RemoveFromParent(); + } } diff --git a/src/World/Block/BlockRef.cs b/src/World/Block/BlockRef.cs index a93c87f..3810f2e 100644 --- a/src/World/Block/BlockRef.cs +++ b/src/World/Block/BlockRef.cs @@ -21,6 +21,8 @@ public class BlockRef public BlockEntity GetEntity(bool create) => GetChunk(create)?.GetBlockEntity(Position.GlobalToChunkRel(), create); + public void RemoveEntity() + => GetChunk(false)?.RemoveBlockEntity(Position.GlobalToChunkRel()); public T Get() @@ -47,4 +49,8 @@ public class BlockRef entity.AddChild((Node)(object)value); } else throw new ArgumentException($"Unable to access {typeof(T).Name} on a Block", nameof(T)); } + + // TODO: Clear block entity when last child is removed? + public void Remove() where T : Node + => GetEntity(false)?.GetNode(typeof(T).Name)?.RemoveFromParent(); } diff --git a/src/World/Chunk/Chunk.cs b/src/World/Chunk/Chunk.cs index 79d2f27..043d266 100644 --- a/src/World/Chunk/Chunk.cs +++ b/src/World/Chunk/Chunk.cs @@ -49,17 +49,30 @@ public partial class Chunk : Node2D } return layer; } - public void OnLayerChanged(IChunkLayer layer) - => _dirty = true; + + public void OnLayerChanged(IChunkLayer layer, BlockPos pos) + { + _dirty = true; + // Clear block entity if block is changed. + if (layer is BlockLayer) RemoveBlockEntity(pos); + } public BlockEntity GetBlockEntity(BlockPos pos, bool create) { EnsureWithinBounds(pos); - return create ? this.GetOrCreateChild(pos.ToString(), () => new BlockEntity()) + return create ? this.GetOrCreateChild(pos.ToString(), () => + new BlockEntity(new BlockRef(this.GetWorld(), + pos.ChunkRelToGlobal(ChunkPos)))) : GetNode(pos.ToString()); } + public void RemoveBlockEntity(BlockPos pos) + { + EnsureWithinBounds(pos); + GetNode(pos.ToString())?.RemoveFromParent(); + } + public static void EnsureWithinBounds(BlockPos pos) { diff --git a/src/World/Chunk/ChunkLayer.cs b/src/World/Chunk/ChunkLayer.cs index 7466fac..d742d1f 100644 --- a/src/World/Chunk/ChunkLayer.cs +++ b/src/World/Chunk/ChunkLayer.cs @@ -12,14 +12,14 @@ public interface IChunkLayer : IDeSerializable { Type AccessType { get; } bool IsDefault { get; } - event Action Changed; + event Action Changed; } public interface IChunkLayer : IChunkLayer { T this[BlockPos pos] { get; set; } T this[int x, int y] { get; set; } - T this[int index] { get; set; } + T this[int index] { get; } } public class ArrayChunkLayer : IChunkLayer @@ -32,26 +32,24 @@ public class ArrayChunkLayer : IChunkLayer public Type AccessType => typeof(T); public bool IsDefault => NonDefaultCount == 0; - public event Action Changed; + public event Action Changed; - public T this[BlockPos pos] { - get => this[Chunk.GetIndex(pos)]; - set => this[Chunk.GetIndex(pos)] = value; - } + public T this[int index] => _data[index]; public T this[int x, int y] { get => this[Chunk.GetIndex(x, y)]; - set => this[Chunk.GetIndex(x, y)] = value; + set => this[new BlockPos(x, y)] = value; } - public T this[int index] { - get => _data[index]; + public T this[BlockPos pos] { + get => this[Chunk.GetIndex(pos.X, pos.Y)]; set { + var index = Chunk.GetIndex(pos.X, pos.Y); var previous = _data[index]; if (COMPARER.Equals(value, previous)) return; _data[index] = value; if (!COMPARER.Equals(previous, default)) NonDefaultCount--; if (!COMPARER.Equals(value, default)) NonDefaultCount++; - Changed?.Invoke(this); + Changed?.Invoke(this, pos); } } @@ -78,10 +76,10 @@ public class TranslationLayer : IChunkLayer public Type AccessType => typeof(TAccess); public bool IsDefault => _data.IsDefault; - public event Action Changed { add => _data.Changed += value; remove => _data.Changed -= value; } + public event Action Changed { add => _data.Changed += value; remove => _data.Changed -= value; } public TAccess this[BlockPos pos] { get => _from(_data[pos]); set => _data[pos] = _to(value); } public TAccess this[int x, int y] { get => _from(_data[x, y]); set => _data[x, y] = _to(value); } - public TAccess this[int index] { get => _from(_data[index]); set => _data[index] = _to(value); } + public TAccess this[int index] => _from(_data[index]); public void Serialize(ref MessagePackWriter writer, MessagePackSerializerOptions options) => _data.Serialize(ref writer, options); diff --git a/src/World/World.cs b/src/World/World.cs index 70b2351..f3fdee1 100644 --- a/src/World/World.cs +++ b/src/World/World.cs @@ -97,13 +97,8 @@ public partial class World : Node } [Puppet] - public void SpawnHit(NodePath spritePath, Vector2 hitPosition, Color color) - { - var texture = GD.Load("res://gfx/hit_decal.png"); - var sprite = this.GetWorld().GetNode(spritePath); - var hit = new HitDecal(texture, sprite.Texture, hitPosition, color); - sprite.AddChild(hit); - } + public void SpawnHit(NodePath path, Vector2 hitPosition, Color color) + => HitDecal.Spawn(this.GetWorld(), path, hitPosition, color); [PuppetSync] public void Despawn(NodePath path, bool errorIfMissing)