From c5d89c898a82341eacee0b265ca0c244abf90319 Mon Sep 17 00:00:00 2001 From: anonoe <8bdd1ef7-1633-4e26-83a0-8dda8605bcd0@aleeas.com> Date: Tue, 10 Feb 2026 16:51:55 +0100 Subject: [PATCH] respawn naturals while chunk loaded --- packs/base/data/world/chunks/0_0.json | 2 +- scripts/world/ChunkManager.cs | 145 ++++++++++++++++++++------ 2 files changed, 114 insertions(+), 33 deletions(-) diff --git a/packs/base/data/world/chunks/0_0.json b/packs/base/data/world/chunks/0_0.json index 3ceb19a..c7bfde8 100644 --- a/packs/base/data/world/chunks/0_0.json +++ b/packs/base/data/world/chunks/0_0.json @@ -21,7 +21,7 @@ "id": "pickup:forest_herb_01", "item": "item:forest_herb", "pos": [120, 140], - "respawn_seconds": 5 + "respawn_seconds": 10 }, { "id": "pickup:rusty_coin_01", diff --git a/scripts/world/ChunkManager.cs b/scripts/world/ChunkManager.cs index 1e1761a..961dab8 100644 --- a/scripts/world/ChunkManager.cs +++ b/scripts/world/ChunkManager.cs @@ -158,11 +158,6 @@ public partial class ChunkManager : Node2D return n; } - private static long NowUnixSeconds() - { - return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - } - private void AddChunkMarkers(Node2D chunkNode, ChunkData data) { if (_save == null || PickupMarkerScene == null) return; @@ -173,42 +168,29 @@ public partial class ChunkManager : Node2D foreach (var p in data.pickups) { - if (_save == null || PickupMarkerScene == null) - return; + if (_save == null || PickupMarkerScene == null) return; + // Unnatural / one-time pickups if (p.respawn_seconds <= 0) { if (_save.State.CollectedPickups.Contains(p.id)) continue; - } - else - { - if (_save.State.PickupRespawnAt.TryGetValue(p.id, out var respawnAt) && now < respawnAt) - continue; + + SpawnPickupMarker(chunkNode, p); + continue; } - var marker = (PickupMarker)PickupMarkerScene.Instantiate(); - marker.Position = new Vector2(p.pos[0], p.pos[1]); - marker.PickupId = p.id; - marker.ItemId = p.item; - - marker.PickedUp += (pickupId, itemId) => + // Natural pickups: check cooldown + //var now = NowUnixSeconds(); + if (_save.State.PickupRespawnAt.TryGetValue(p.id, out var respawnAt) && now < respawnAt) { - GD.Print($"Picked up {itemId} ({pickupId})"); + // Chunk is loaded but pickup is still cooling down -> schedule its appearance. + SchedulePickupRespawn(chunkNode, p.id, p.item, p.respawn_seconds, respawnAt, new int[] { p.pos[0], p.pos[1] }); + continue; + } - if (p.respawn_seconds <= 0) - { - _save.State.CollectedPickups.Add(pickupId); - } - else - { - _save.State.PickupRespawnAt[pickupId] = NowUnixSeconds() + p.respawn_seconds; - } - - _save.Save(); - marker.QueueFree(); - }; - chunkNode.AddChild(marker); + // Ready now -> spawn immediately + SpawnPickupMarker(chunkNode, p); } } @@ -247,4 +229,103 @@ public partial class ChunkManager : Node2D // Collision rects: gray outlines (optional later) } + + private static long NowUnixSeconds() + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + // Timer nodes are attached to the chunk node so they automatically die when the chunk unloads. + private const string RespawnTimerPrefix = "RespawnTimer_"; + private static string RespawnTimerName(string pickupId) => $"{RespawnTimerPrefix}{pickupId.Replace(":", "_")}"; + // Spawn a pickup marker in world-space (y-sort friendly later). + private void SpawnPickupMarker(Node2D chunkNode, ChunkPickup p) + { + if (_save == null || PickupMarkerScene == null) return; + + var marker = (PickupMarker)PickupMarkerScene.Instantiate(); + marker.Position = new Vector2(p.pos[0], p.pos[1]); + marker.PickupId = p.id; + marker.ItemId = p.item; + + // Capture only the values we need (avoid relying on foreach capture semantics). + var respawnSeconds = p.respawn_seconds; + var pickupId = p.id; + var itemId = p.item; + var posCopy = new int[] { p.pos[0], p.pos[1] }; + + marker.PickedUp += (_, __) => + { + if (_save == null) return; + + // Permanent pickups (unnatural): one-time removal. + if (respawnSeconds <= 0) + { + _save.State.CollectedPickups.Add(pickupId); + _save.Save(); + marker.QueueFree(); + return; + } + + // Natural pickups: schedule respawn by absolute timestamp. + var respawnAt = NowUnixSeconds() + respawnSeconds; + _save.State.PickupRespawnAt[pickupId] = respawnAt; + _save.Save(); + + marker.QueueFree(); + + // If chunk stays loaded, respawn should still appear. + SchedulePickupRespawn(chunkNode, pickupId, itemId, respawnSeconds, respawnAt, posCopy); + }; + + chunkNode.AddChild(marker); + } + + // Schedule a respawn for a pickup that is currently on cooldown. + private void SchedulePickupRespawn( + Node2D chunkNode, + string pickupId, + string itemId, + int respawnSeconds, + long respawnAtUnix, + int[] pos) + { + // Deduplicate: one timer per pickup per loaded chunk. + var timerName = RespawnTimerName(pickupId); + if (chunkNode.HasNode(timerName)) + return; + + var now = NowUnixSeconds(); + var wait = Math.Max(0.1, respawnAtUnix - now); // remaining time (accounts for time while chunk was unloaded) + + var timer = new Timer + { + Name = timerName, + OneShot = true, + WaitTime = wait + }; + + timer.Timeout += () => + { + if (_save == null) return; + + // Re-check at fire time (handles clock changes / save edits). + var now2 = NowUnixSeconds(); + if (_save.State.PickupRespawnAt.TryGetValue(pickupId, out var ra) && now2 < ra) + { + // Still not ready: reschedule with the updated remaining time. + timer.QueueFree(); + SchedulePickupRespawn(chunkNode, pickupId, itemId, respawnSeconds, ra, pos); + return; + } + + // Ready: remove the timer node name and spawn the pickup. + timer.QueueFree(); + + var p = new ChunkPickup(pickupId, itemId, pos, respawnSeconds); + SpawnPickupMarker(chunkNode, p); + }; + + chunkNode.AddChild(timer); + timer.Start(); + } }