respawn naturals while chunk loaded

This commit is contained in:
anonoe 2026-02-10 16:51:55 +01:00
parent 63ca3220d1
commit c5d89c898a
Signed by: anonoe
SSH key fingerprint: SHA256:OnAs6gNQelOnDiY5tBpDYKQiuTgBvnmIdMo5P09cdqg
2 changed files with 114 additions and 33 deletions

View file

@ -21,7 +21,7 @@
"id": "pickup:forest_herb_01", "id": "pickup:forest_herb_01",
"item": "item:forest_herb", "item": "item:forest_herb",
"pos": [120, 140], "pos": [120, 140],
"respawn_seconds": 5 "respawn_seconds": 10
}, },
{ {
"id": "pickup:rusty_coin_01", "id": "pickup:rusty_coin_01",

View file

@ -158,11 +158,6 @@ public partial class ChunkManager : Node2D
return n; return n;
} }
private static long NowUnixSeconds()
{
return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
private void AddChunkMarkers(Node2D chunkNode, ChunkData data) private void AddChunkMarkers(Node2D chunkNode, ChunkData data)
{ {
if (_save == null || PickupMarkerScene == null) return; if (_save == null || PickupMarkerScene == null) return;
@ -173,42 +168,29 @@ public partial class ChunkManager : Node2D
foreach (var p in data.pickups) foreach (var p in data.pickups)
{ {
if (_save == null || PickupMarkerScene == null) if (_save == null || PickupMarkerScene == null) return;
return;
// Unnatural / one-time pickups
if (p.respawn_seconds <= 0) if (p.respawn_seconds <= 0)
{ {
if (_save.State.CollectedPickups.Contains(p.id)) if (_save.State.CollectedPickups.Contains(p.id))
continue; continue;
}
else SpawnPickupMarker(chunkNode, p);
{
if (_save.State.PickupRespawnAt.TryGetValue(p.id, out var respawnAt) && now < respawnAt)
continue; continue;
} }
var marker = (PickupMarker)PickupMarkerScene.Instantiate(); // Natural pickups: check cooldown
marker.Position = new Vector2(p.pos[0], p.pos[1]); //var now = NowUnixSeconds();
marker.PickupId = p.id; if (_save.State.PickupRespawnAt.TryGetValue(p.id, out var respawnAt) && now < respawnAt)
marker.ItemId = p.item;
marker.PickedUp += (pickupId, itemId) =>
{ {
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] });
if (p.respawn_seconds <= 0) continue;
{
_save.State.CollectedPickups.Add(pickupId);
}
else
{
_save.State.PickupRespawnAt[pickupId] = NowUnixSeconds() + p.respawn_seconds;
} }
_save.Save(); // Ready now -> spawn immediately
marker.QueueFree(); SpawnPickupMarker(chunkNode, p);
};
chunkNode.AddChild(marker);
} }
} }
@ -247,4 +229,103 @@ public partial class ChunkManager : Node2D
// Collision rects: gray outlines (optional later) // 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();
}
} }