Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion Scripts/Auth/AuthApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ private enum AuthRequestKind
FetchMarketplaceListings,
CreateMarketplaceListing,
CancelMarketplaceListing,
BuyMarketplaceListing
BuyMarketplaceListing,
GenerateDrops,
ClearDrops
}

[Export] public string BackendBaseUrl = BackendDefaults.BackendBaseUrl;
Expand Down Expand Up @@ -450,6 +452,39 @@ public bool BuyMarketplaceListing(string token, int listingId, Action<AuthApiRes
callback);
}

public bool GenerateDrops(string token, int playerLevel, string dropSource, string difficulty, Action<AuthApiResult> callback)
{
var payload = new Godot.Collections.Dictionary
{
["action"] = "generate",
["player_level"] = playerLevel,
["drop_source"] = dropSource,
["difficulty"] = difficulty
};
return SendRequest(
AuthRequestKind.GenerateDrops,
"/api/player/drop/",
HTTPClient.Method.Post,
payload,
token,
callback);
}

public bool ClearDrops(string token, Action<AuthApiResult> callback)
{
var payload = new Godot.Collections.Dictionary
{
["action"] = "clear"
};
return SendRequest(
AuthRequestKind.ClearDrops,
"/api/player/drop/",
HTTPClient.Method.Post,
payload,
token,
callback);
}

private bool SendRequest(
AuthRequestKind kind,
string endpointPath,
Expand Down Expand Up @@ -602,6 +637,41 @@ private AuthApiResult CreateMockResult(AuthRequestKind kind, Godot.Collections.D
}
break;

case AuthRequestKind.GenerateDrops:
result.ArrayData = new Godot.Collections.Array();
// Mock Gold Drop
result.ArrayData.Add(new Godot.Collections.Dictionary
{
["instance_id"] = Guid.NewGuid().ToString("N"),
["item_type"] = "gold",
["gold_amount"] = 35,
["item_data"] = null
});
// Mock Item Drop (Hp Potion S)
result.ArrayData.Add(new Godot.Collections.Dictionary
{
["instance_id"] = Guid.NewGuid().ToString("N"),
["item_type"] = "potion",
["gold_amount"] = 0,
["item_data"] = new Godot.Collections.Dictionary
{
["item_id"] = "hp_potion_s",
["name"] = "Small HP Potion",
["description"] = "Restores 5 HP.",
["item_type"] = "potion",
["quantity"] = 1,
["price"] = 10,
["sprite_path"] = "res://Assets/items/hp_potion_S.png",
["heal_amount"] = 5,
["removes_burn"] = false
}
});
break;

case AuthRequestKind.ClearDrops:
// ResponseCode 200 OK, empty Data/ArrayData is fine
break;

case AuthRequestKind.Logout:
result.ResponseCode = 204;
break;
Expand Down
78 changes: 71 additions & 7 deletions Scripts/Characters/Monster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -769,20 +769,85 @@ private void TrySpawnDrops()
parent.AddChild(expPickup);
GD.PrintS($"[Monster] Spawned EXP drop at {expPickup.Position}");

DifficultyLevel mapDiff = _map != null ? _map.Difficulty : DifficultyLevel.Normal;
int playerLevel = _player != null ? (int)_player.Level : (int)Level;
var manager = FindEquipmentManager();

if (Main.Instance != null)
{
string token = Main.Instance.GetAuthToken();
if (Main.Instance.PlayerDataApiClient != null && !string.IsNullOrEmpty(token))
{
Main.Instance.PlayerDataApiClient.GenerateDrops(token, playerLevel, "monster", mapDiff.ToString(), result =>
{
if (result.NetworkOk && result.ResponseCode == 200 && result.ArrayData != null)
{
SpawnServerDrops(parent, GlobalPosition, manager, result.ArrayData);
}
else
{
GD.PrintErr("[Monster] Failed to generate secure drops from server. Falling back to local generation...");
SpawnLocalDrops(parent, GlobalPosition, manager, mapDiff);
}
});
return;
}
}

SpawnLocalDrops(parent, GlobalPosition, manager, mapDiff);
}

private void SpawnServerDrops(Node parent, Vector2 centerPosition, EquipmentManager manager, Godot.Collections.Array drops)
{
var rng = new RandomNumberGenerator();
rng.Randomize();

for (int i = 0; i < drops.Count; i++)
{
if (!(drops[i] is Godot.Collections.Dictionary drop))
{
continue;
}

string instanceId = drop.Contains("instance_id") ? drop["instance_id"]?.ToString() : string.Empty;
string itemType = drop.Contains("item_type") ? drop["item_type"]?.ToString() : string.Empty;

if (itemType == "gold")
{
int goldAmount = drop.Contains("gold_amount") ? Convert.ToInt32(drop["gold_amount"]) : 0;
var coinDrop = new QuestFantasy.Items.CoinDrop();
coinDrop.InitializeSecure(instanceId, goldAmount, _player);
coinDrop.Position = centerPosition + new Vector2(rng.Randf() * 40f - 20f, rng.Randf() * 40f - 20f);
parent.AddChild(coinDrop);
GD.PrintS($"[Monster] Spawned Secure Coin drop of value {goldAmount} at {coinDrop.Position}");
}
else if (drop.Contains("item_data") && drop["item_data"] is Godot.Collections.Dictionary itemDataDict)
{
itemDataDict["instance_id"] = instanceId;
Item item = PlayerItemSnapshotCodec.Decode(itemDataDict);
if (item != null)
{
float pscale = manager != null ? manager.PickupSpriteScale : 0.5f;
var itemPos = centerPosition + new Vector2(rng.Randf() * 100f - 50f, rng.Randf() * 100f - 50f);
LootItemFactory.SpawnPickup(parent, item, itemPos, pscale, "secure_monster");
GD.PrintS($"[Monster] Spawned secure pickup: {item.Name} at {itemPos}");
}
}
}
}

private void SpawnLocalDrops(Node parent, Vector2 centerPosition, EquipmentManager manager, DifficultyLevel mapDiff)
{
var coinDrop = new QuestFantasy.Items.CoinDrop();
int pLevel = _player != null ? (int)_player.Level : (int)Level;
DifficultyLevel mapDiff = _map != null ? _map.Difficulty : DifficultyLevel.Normal;
coinDrop.Initialize(pLevel, mapDiff, 0.3f, _player);
coinDrop.Position = GlobalPosition + new Vector2((float)_random.NextDouble() * 40f - 20f, (float)_random.NextDouble() * 40f - 20f);
coinDrop.Position = centerPosition + new Vector2((float)_random.NextDouble() * 40f - 20f, (float)_random.NextDouble() * 40f - 20f);
parent.AddChild(coinDrop);
GD.PrintS($"[Monster] Spawned Coin drop at {coinDrop.Position}");

var rng = new RandomNumberGenerator();
rng.Randomize();

// Find EquipmentManager early so consumable/ticket drops can match equipment scale
var manager = FindEquipmentManager();

float itemDropChance = Mathf.Clamp(ItemDropChance, 0f, 1f);
if (rng.Randf() >= itemDropChance)
{
Expand All @@ -791,15 +856,14 @@ private void TrySpawnDrops()
}

Item itemDrop = RollSingleItemDrop(rng, manager, mapDiff);

if (itemDrop == null)
{
GD.PrintS("[Monster] Item drop roll passed, but no item was available.");
return;
}

float itemScale = manager != null ? manager.PickupSpriteScale : 0.5f;
var itemPos = GlobalPosition + new Vector2(rng.Randf() * 100f - 50f, rng.Randf() * 100f - 50f);
var itemPos = centerPosition + new Vector2(rng.Randf() * 100f - 50f, rng.Randf() * 100f - 50f);
LootItemFactory.SpawnPickup(parent, itemDrop, itemPos, itemScale, "monster_item");
GD.PrintS($"[Monster] Spawned item drop: {itemDrop.Name} at {itemPos}");
}
Expand Down
102 changes: 72 additions & 30 deletions Scripts/Environment/TreasureChest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,33 +111,91 @@ public object OpenAndGetEquipment(int playerLevel = 1)
// Returns the list of spawned EquipmentPickup nodes.
public Godot.Collections.Array OpenChest(Node parent, Vector2 centerPosition, EquipmentManager manager, int playerLevel)
{
var spawned = new Godot.Collections.Array();
if (manager == null || parent == null)
return spawned;
DifficultyLevel mapDiff = DifficultyLevel.Normal;
if (parent is Map parentMap) mapDiff = parentMap.Difficulty;

if (Main.Instance != null)
{
string token = Main.Instance.GetAuthToken();
if (Main.Instance.PlayerDataApiClient != null && !string.IsNullOrEmpty(token))
{
Main.Instance.PlayerDataApiClient.GenerateDrops(token, playerLevel, "chest", mapDiff.ToString(), result =>
{
if (result.NetworkOk && result.ResponseCode == 200 && result.ArrayData != null)
{
SpawnServerDrops(parent, centerPosition, manager, result.ArrayData);
}
else
{
GD.PrintErr("[TreasureChest] Failed to generate secure drops from server. Falling back to local generation...");
SpawnLocalDrops(parent, centerPosition, manager, playerLevel, mapDiff);
}
});
return new Godot.Collections.Array();
}
}

SpawnLocalDrops(parent, centerPosition, manager, playerLevel, mapDiff);
return new Godot.Collections.Array();
}

private void SpawnServerDrops(Node parent, Vector2 centerPosition, EquipmentManager manager, Godot.Collections.Array drops)
{
var rng = new RandomNumberGenerator();
rng.Randomize();

for (int i = 0; i < drops.Count; i++)
{
if (!(drops[i] is Godot.Collections.Dictionary drop))
{
continue;
}

string instanceId = drop.Contains("instance_id") ? drop["instance_id"]?.ToString() : string.Empty;
string itemType = drop.Contains("item_type") ? drop["item_type"]?.ToString() : string.Empty;

if (itemType == "gold")
{
int goldAmount = drop.Contains("gold_amount") ? Convert.ToInt32(drop["gold_amount"]) : 0;
var player = FindPlayerRecursive(parent);
var coinDrop = new QuestFantasy.Items.CoinDrop();
coinDrop.InitializeSecure(instanceId, goldAmount, player);
coinDrop.Position = centerPosition + new Vector2(rng.Randf() * 40f - 20f, rng.Randf() * 40f - 20f);
parent.AddChild(coinDrop);
GD.PrintS($"[TreasureChest] Spawned Secure Coin drop of value {goldAmount} at {coinDrop.Position}");
}
else if (drop.Contains("item_data") && drop["item_data"] is Godot.Collections.Dictionary itemDataDict)
{
itemDataDict["instance_id"] = instanceId;
Item item = PlayerItemSnapshotCodec.Decode(itemDataDict);
if (item != null)
{
float pscale = manager != null ? manager.PickupSpriteScale : 0.5f;
var offset = new Vector2(rng.Randf() * 200f - 100f, rng.Randf() * 200f - 100f);
var itemPos = centerPosition + offset;
LootItemFactory.SpawnPickup(parent, item, itemPos, pscale, "secure_chest");
GD.PrintS($"[TreasureChest] Spawned secure pickup: {item.Name} at {itemPos}");
}
}
}
}

private void SpawnLocalDrops(Node parent, Vector2 centerPosition, EquipmentManager manager, int playerLevel, DifficultyLevel mapDiff)
{
int minD = Math.Max(0, MinDrops);
int maxD = Math.Max(minD, MaxDrops);
// Use RandomNumberGenerator to avoid casting overflow from GD.Randi
var rng = new RandomNumberGenerator();
rng.Randomize();
int drops = rng.RandiRange(minD, maxD);

GD.PrintS($"[TreasureChest] drop range min={minD} max={maxD} -> drops={drops}");

// Use the provided manager to get equipment options (avoid relying on _manager field)
var options = manager.GetEquipmentSet(OptionCount, playerLevel, LevelOffset);
GD.PrintS($"[TreasureChest] Opening chest: drops={drops}, options={options.Count}");

// Convert options to a typed list
var options = manager != null ? manager.GetEquipmentSet(OptionCount, playerLevel, LevelOffset) : new System.Collections.Generic.List<Item>();
var optList = new System.Collections.Generic.List<object>();
foreach (var o in options)
{
optList.Add(o);
}

// Shuffle optList and take unique items up to available count
var shuffled = new System.Collections.Generic.List<object>(optList);
// Fisher-Yates shuffle
for (int s = shuffled.Count - 1; s > 0; s--)
{
int j = rng.RandiRange(0, s);
Expand All @@ -155,11 +213,10 @@ public Godot.Collections.Array OpenChest(Node parent, Vector2 centerPosition, Eq

var pickup = new EquipmentPickup();
pickup.ItemData = it;
pickup.SpriteScale = manager.PickupSpriteScale;
pickup.SpriteScale = manager != null ? manager.PickupSpriteScale : 0.1f;
var offset = new Vector2(rng.Randf() * 200f - 100f, rng.Randf() * 200f - 100f);
pickup.Position = centerPosition + offset;

// deterministic node name based on sprite/resource name
string baseName = "equipment";
var spriteTex = (pickup.ItemData is QuestFantasy.Core.Data.Items.Equipment pe) ? pe.Sprite : (pickup.ItemData is QuestFantasy.Core.Data.Items.Weapon pw ? pw.Sprite : null);
if (spriteTex != null)
Expand All @@ -172,24 +229,14 @@ public Godot.Collections.Array OpenChest(Node parent, Vector2 centerPosition, Eq
}
pickup.Name = $"Pickup_{baseName}_{i}";
parent.AddChild(pickup);
spawned.Add(pickup);
GD.PrintS($"[TreasureChest] Spawned pickup: {pickup.Name} at {pickup.Position}");
if (pickup.ItemData == null || (pickup.ItemData is QuestFantasy.Core.Data.Items.Equipment e2 && e2.Sprite == null) || (pickup.ItemData is QuestFantasy.Core.Data.Items.Weapon w2 && w2.Sprite == null))
{
GD.PrintS($"[TreasureChest] WARNING: pickup {pickup.Name} has no sprite or item is null");
}
}

DifficultyLevel mapDiff = DifficultyLevel.Normal;
if (parent is Map parentMap) mapDiff = parentMap.Difficulty;

Item potionDrop = LootItemFactory.RollPotion(rng, 0.18f);
if (potionDrop != null)
{
var potionPos = centerPosition + new Vector2(rng.Randf() * 180f - 90f, rng.Randf() * 180f - 90f);
float pscale = manager != null ? manager.PickupSpriteScale : 0.5f;
LootItemFactory.SpawnPickup(parent, potionDrop, potionPos, pscale, "chest_potion");
GD.PrintS($"[TreasureChest] Spawned potion drop: {potionDrop.Name} at {potionPos}");
}

Item ticketDrop = LootItemFactory.RollTicket(rng, mapDiff, 2f);
Expand All @@ -198,17 +245,12 @@ public Godot.Collections.Array OpenChest(Node parent, Vector2 centerPosition, Eq
var ticketPos = centerPosition + new Vector2(rng.Randf() * 180f - 90f, rng.Randf() * 180f - 90f);
float tscale = manager != null ? manager.PickupSpriteScale : 0.5f;
LootItemFactory.SpawnPickup(parent, ticketDrop, ticketPos, tscale, "chest_ticket");
GD.PrintS($"[TreasureChest] Spawned ticket drop: {ticketDrop.Name} at {ticketPos}");
}

// Spawn CoinDrop
var player = FindPlayerRecursive(parent);
var coinDrop = new QuestFantasy.Items.CoinDrop();
coinDrop.Initialize(playerLevel, mapDiff, 1.0f, player);
coinDrop.Position = centerPosition + new Vector2(rng.Randf() * 40f - 20f, rng.Randf() * 40f - 20f);
parent.AddChild(coinDrop);
GD.PrintS($"[TreasureChest] Spawned Coin drop at {coinDrop.Position}");

return spawned;
}
}
Loading
Loading