async spawnGroundItem(
itemId: string,
quantity: number,
position: { x: number; y: number; z: number },
options: GroundItemOptions,
): Promise<string> {
// Server authority check
if (!this.world.isServer) {
console.error(`[GroundItemSystem] Client attempted ground item spawn - BLOCKED`);
return "";
}
// Global limit check
if (this.groundItems.size >= this.MAX_GLOBAL_ITEMS) {
console.warn(`[GroundItemSystem] Global item limit reached, rejecting spawn`);
return "";
}
const item = getItem(itemId);
if (!item) return "";
const currentTick = this.world.currentTick;
// OSRS: Untradeable items ALWAYS despawn in 3 min
const despawnTicks = item.tradeable === false
? COMBAT_CONSTANTS.UNTRADEABLE_DESPAWN_TICKS
: msToTicks(options.despawnTime);
// Snap to tile center
const tile = worldToTile(position.x, position.z);
const tileKey = this.getTileKey(tile);
const tileCenter = tileToWorld(tile);
// Ground to terrain height
const groundedPosition = groundToTerrain(this.world, {
x: tileCenter.x,
y: position.y,
z: tileCenter.z,
}, 0.2, Infinity);
// Check for existing pile
const existingPile = this.groundItemPiles.get(tileKey);
// OSRS: If pile full, remove oldest item
if (existingPile && existingPile.items.length >= this.MAX_PILE_SIZE) {
const oldestItem = existingPile.items.pop();
if (oldestItem) {
this.groundItems.delete(oldestItem.entityId);
this.entityManager.destroyEntity(oldestItem.entityId);
}
}
// OSRS: Merge stackable items
if (item.stackable && existingPile) {
const existingStack = existingPile.items.find(
i => i.itemId === itemId &&
(!i.lootProtectionTick || i.droppedBy === options.droppedBy)
);
if (existingStack) {
existingStack.quantity += quantity;
existingStack.despawnTick = currentTick + despawnTicks;
// Update entity properties
const entity = this.world.entities.get(existingStack.entityId);
entity?.setProperty("quantity", existingStack.quantity);
return existingStack.entityId;
}
}
// Create new item entity
const dropId = `ground_item_${this.nextItemId++}`;
const itemEntity = await this.entityManager.spawnEntity({
id: dropId,
name: item.name,
type: EntityType.ITEM,
position: groundedPosition,
itemId: item.id,
quantity: quantity,
stackable: item.stackable ?? false,
// ... other properties
});
// Track ground item
const groundItemData: GroundItemData = {
entityId: dropId,
itemId,
quantity,
position: groundedPosition,
despawnTick: currentTick + despawnTicks,
droppedBy: options.droppedBy,
lootProtectionTick: options.lootProtection
? currentTick + msToTicks(options.lootProtection)
: undefined,
spawnedAt: Date.now(),
};
this.groundItems.set(dropId, groundItemData);
// Manage pile visibility
if (existingPile) {
// Hide previous top item
this.setItemVisibility(existingPile.topItemEntityId, false);
existingPile.items.unshift(groundItemData);
existingPile.topItemEntityId = dropId;
} else {
// Create new pile
this.groundItemPiles.set(tileKey, {
tileKey,
tile,
items: [groundItemData],
topItemEntityId: dropId,
});
}
return dropId;
}