feat: Enemies drop loot on death
This commit is contained in:
166
src/engine/systems/LootSystem.ts
Normal file
166
src/engine/systems/LootSystem.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
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];
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { SystemRegistry } from "../engine/ecs/System";
|
||||
import { TriggerSystem } from "../engine/ecs/systems/TriggerSystem";
|
||||
import { StatusEffectSystem } from "../engine/ecs/systems/StatusEffectSystem";
|
||||
import { EventBus } from "../engine/ecs/EventBus";
|
||||
import { generateLoot } from "../engine/systems/LootSystem";
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private world!: World;
|
||||
@@ -690,6 +691,15 @@ export class GameScene extends Phaser.Scene {
|
||||
this.dungeonRenderer.showHeal(ev.x, ev.y, ev.amount);
|
||||
} else if (ev.type === "killed") {
|
||||
this.dungeonRenderer.spawnCorpse(ev.x, ev.y, ev.victimType || "rat");
|
||||
|
||||
// Try to drop loot from killed enemy
|
||||
if (ev.victimType && ev.victimType !== "player") {
|
||||
const loot = generateLoot(Math.random, ev.victimType, this.floorIndex);
|
||||
if (loot) {
|
||||
this.itemManager.spawnItem(loot, { x: ev.x, y: ev.y });
|
||||
this.dungeonRenderer.showFloatingText(ev.x, ev.y, `${loot.name}!`, "#ffd700");
|
||||
}
|
||||
}
|
||||
} else if (ev.type === "waited" && ev.actorId === this.playerId) {
|
||||
const player = this.world.actors.get(this.playerId) as CombatantActor;
|
||||
if (player) {
|
||||
|
||||
Reference in New Issue
Block a user