167 lines
4.6 KiB
TypeScript
167 lines
4.6 KiB
TypeScript
import { type Item, type ActorType } from "../../core/types";
|
|
import {
|
|
createMeleeWeapon,
|
|
createArmour,
|
|
createConsumable,
|
|
createAmmo,
|
|
MELEE_WEAPONS,
|
|
ARMOUR
|
|
} from "../../core/config/Items";
|
|
import {
|
|
WEAPON_VARIANTS,
|
|
ARMOUR_VARIANTS,
|
|
type WeaponVariantId,
|
|
type ArmourVariantId
|
|
} from "../../core/config/ItemVariants";
|
|
import { UpgradeManager } from "../systems/UpgradeManager";
|
|
|
|
/**
|
|
* Loot drop configuration.
|
|
* Chances are cumulative (checked in order).
|
|
*/
|
|
export const LOOT_CONFIG = {
|
|
// Base chance any item drops at all (per enemy)
|
|
baseDropChance: 0.25,
|
|
|
|
// Type weights (what kind of item drops)
|
|
typeWeights: {
|
|
weapon: 30,
|
|
armour: 25,
|
|
consumable: 35,
|
|
ammo: 10,
|
|
},
|
|
|
|
// Rarity chances (applied after type is chosen)
|
|
rarityChances: {
|
|
base: 0.60, // 60% just base item
|
|
variant: 0.30, // 30% has a variant
|
|
upgraded: 0.10, // 10% has upgrade applied
|
|
},
|
|
|
|
// Per-enemy type drop chance modifiers
|
|
enemyDropModifiers: {
|
|
rat: 0.8,
|
|
bat: 0.9,
|
|
// Add more enemy types as needed
|
|
} as Record<ActorType, number>,
|
|
};
|
|
|
|
/**
|
|
* Generate a random loot item based on the loot configuration.
|
|
* Returns null if no item drops.
|
|
*/
|
|
export function generateLoot(
|
|
random: () => number,
|
|
enemyType?: ActorType,
|
|
floorLevel: number = 1
|
|
): Item | null {
|
|
// Check base drop chance (modified by enemy type)
|
|
let dropChance: number = LOOT_CONFIG.baseDropChance;
|
|
if (enemyType && LOOT_CONFIG.enemyDropModifiers[enemyType]) {
|
|
dropChance *= LOOT_CONFIG.enemyDropModifiers[enemyType];
|
|
}
|
|
|
|
// Higher floor = slightly more drops
|
|
dropChance += floorLevel * 0.02;
|
|
dropChance = Math.min(dropChance, 0.6); // Cap at 60%
|
|
|
|
if (random() > dropChance) {
|
|
return null;
|
|
}
|
|
|
|
// Determine item type
|
|
const itemType = pickWeightedRandom(LOOT_CONFIG.typeWeights, random);
|
|
|
|
// Determine rarity tier
|
|
const rarityRoll = random();
|
|
let hasVariant = false;
|
|
let hasUpgrade = false;
|
|
|
|
if (rarityRoll >= (1 - LOOT_CONFIG.rarityChances.upgraded)) {
|
|
// Top 10%: upgraded (implies has variant too)
|
|
hasVariant = true;
|
|
hasUpgrade = true;
|
|
} else if (rarityRoll >= (1 - LOOT_CONFIG.rarityChances.upgraded - LOOT_CONFIG.rarityChances.variant)) {
|
|
// Next 30%: variant only
|
|
hasVariant = true;
|
|
}
|
|
// Otherwise: base item (60%)
|
|
|
|
// Generate the item
|
|
let item: Item | null = null;
|
|
|
|
switch (itemType) {
|
|
case "weapon": {
|
|
const weaponIds = Object.keys(MELEE_WEAPONS) as (keyof typeof MELEE_WEAPONS)[];
|
|
const weaponId = weaponIds[Math.floor(random() * weaponIds.length)];
|
|
|
|
let variant: WeaponVariantId | undefined;
|
|
if (hasVariant) {
|
|
const variantIds = Object.keys(WEAPON_VARIANTS) as WeaponVariantId[];
|
|
variant = variantIds[Math.floor(random() * variantIds.length)];
|
|
}
|
|
|
|
item = createMeleeWeapon(weaponId, variant);
|
|
break;
|
|
}
|
|
|
|
case "armour": {
|
|
const armourIds = Object.keys(ARMOUR) as (keyof typeof ARMOUR)[];
|
|
const armourId = armourIds[Math.floor(random() * armourIds.length)];
|
|
|
|
let variant: ArmourVariantId | undefined;
|
|
if (hasVariant) {
|
|
const variantIds = Object.keys(ARMOUR_VARIANTS) as ArmourVariantId[];
|
|
variant = variantIds[Math.floor(random() * variantIds.length)];
|
|
}
|
|
|
|
item = createArmour(armourId, variant);
|
|
break;
|
|
}
|
|
|
|
case "consumable": {
|
|
// Only drop health potions and throwing daggers, not upgrade scrolls
|
|
const droppableConsumables = ["health_potion", "throwing_dagger"] as const;
|
|
const consumableId = droppableConsumables[Math.floor(random() * droppableConsumables.length)];
|
|
const quantity = 1 + Math.floor(random() * 2); // 1-2
|
|
|
|
item = createConsumable(consumableId, quantity);
|
|
break;
|
|
}
|
|
|
|
case "ammo": {
|
|
const quantity = 5 + Math.floor(random() * 10); // 5-14
|
|
item = createAmmo("ammo_9mm", quantity);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Apply upgrade if rolled
|
|
if (item && hasUpgrade) {
|
|
UpgradeManager.applyUpgrade(item);
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* Pick from weighted options.
|
|
*/
|
|
function pickWeightedRandom(
|
|
weights: Record<string, number>,
|
|
random: () => number
|
|
): string {
|
|
const entries = Object.entries(weights);
|
|
const total = entries.reduce((sum, [, w]) => sum + w, 0);
|
|
let roll = random() * total;
|
|
|
|
for (const [key, weight] of entries) {
|
|
roll -= weight;
|
|
if (roll <= 0) {
|
|
return key;
|
|
}
|
|
}
|
|
|
|
return entries[entries.length - 1][0];
|
|
}
|