Files
diablo2d/js/game/loot.js
T
deepseek fc1fa2d51e Diablo2D — Shadows of Tristram: complete browser ARPG
- Isometric canvas renderer (depth-sorted, FOV/fog, additive lighting)
- 3 classes x 20 skills, 4 acts x 4 floors + boss lairs, torment I-X
- Diablo-style loot: rarities, affix tiers, 14 legendaries, vendor, stash
- Rogue camp with 6 NPCs: Charsi/Akara/Kashya/Cain/Gheed/storage
- NPC quest chain (accept -> hunt -> turn in) with rewards & gating
- Procedural WebAudio SFX + generative music, EN/VI localization
- Saves, settings, waypoints, hardcore mode, PWA manifest
- 93-assertion headless suite + browser E2E via CDP
2026-08-23 06:59:36 +00:00

150 lines
5.4 KiB
JavaScript

/* ============================================================
* Diablo2D — loot.js : drops, gold, pickups
* ============================================================ */
'use strict';
window.D2 = window.D2 || {};
(function (D2) {
const rnd = Math.random;
function goldFindMult(player) {
return 1 + ((player.stats && player.stats.goldFind) || 0) / 100;
}
function magicFindOf(player) {
return (player.stats && player.stats.magicFind) || 0;
}
/** main drop routine when a monster dies */
function dropFor(game, m) {
const p = game.player;
const DC = D2.BAL.dropChances;
const x = m.x, y = m.y;
/* --- gold --- */
let goldChance = DC.gold + (m.isElite ? 0.4 : 0) + (m.isBoss ? 1 : 0);
if (m.isBoss) goldChance = 1;
if (rnd() < goldChance) {
let amount = D2.BAL.goldDropBase(m.level) * goldFindMult(p);
if (m.isElite) amount *= 2.6;
if (m.isBoss) amount *= 7;
amount = Math.max(1, Math.round(amount));
const piles = m.isBoss ? 4 : amount > 40 ? 2 : 1;
for (let i = 0; i < piles; i++) {
game.pickups.push(new D2.entities.Pickup('gold',
x + (rnd() - .5) * 0.8, y + (rnd() - .5) * 0.8,
{ amount: Math.max(1, Math.round(amount / piles)) }));
}
}
/* --- potions --- */
const potChance = (DC.potionHp + DC.potionMp) * (m.isElite ? 1.8 : 1);
if (rnd() < potChance || m.isBoss) {
const type = rnd() < 0.62 ? 'hp' : 'mp';
game.pickups.push(new D2.entities.Pickup('potion',
x + (rnd() - .5) * .6, y + (rnd() - .5) * .6, { potionType: type }));
}
if (m.isBoss) {
game.pickups.push(new D2.entities.Pickup('potion', x + .5, y, { potionType: 'hp' }));
game.pickups.push(new D2.entities.Pickup('potion', x - .5, y, { potionType: 'mp' }));
}
/* --- gear --- */
let gearChance = DC.gear + (m.isElite ? DC.gearEliteBonus : 0);
if (m.isBoss) gearChance = DC.gearBoss;
const rolls = m.isBoss ? 4 : 1;
for (let i = 0; i < rolls; i++) {
if (rnd() >= gearChance && !(m.isBoss && i === 0)) continue;
const opts = {
magicFind: magicFindOf(p),
elite: m.isElite,
boss: m.isBoss,
slot: rnd() < 0.55 ? undefined :
D2.Items.SLOTS[(rnd() * D2.Items.SLOTS.length) | 0],
};
if (m.isBoss && i === 0) {
/* guaranteed flagship drop: legendary 40%, else rare */
opts.rarity = rnd() < 0.4 ? 'legendary' : 'rare';
}
const item = D2.Items.rollItem(Math.max(1, m.level), opts);
if (item) {
game.pickups.push(new D2.entities.Pickup('item',
x + (rnd() - .5) * 1.2, y + (rnd() - .5) * 1.2, { item }));
if (item.rarity === 'legendary') {
game.sfx('legendary');
game.toast('✦ ' + item.name, 'gold');
game.addLight(x, y, 3, '#ef8f34', 0.9, 2.5);
} else if (item.rarity === 'rare') {
game.sfx('pickup');
}
}
}
}
/** barrels / urns / chests */
function rollPropLoot(game, x, y, propType) {
if (propType === 'shrine' || String(propType).startsWith('npc_')) return;
if (propType === 'chest') {
/* chests are generous */
const n = 1 + (rnd() < 0.4 ? 1 : 0);
for (let i = 0; i < n; i++) {
const item = D2.Items.rollItem(Math.max(1, game.monsterLevel()),
{ magicFind: magicFindOf(game.player) + 15 });
if (item) game.pickups.push(new D2.entities.Pickup('item', x + (rnd() - .5), y + (rnd() - .5) * .6, { item }));
}
const g = Math.round(D2.BAL.goldDropBase(game.monsterLevel()) * 2 * goldFindMult(game.player));
game.pickups.push(new D2.entities.Pickup('gold', x, y + .3, { amount: g }));
game.sfx('coin');
return;
}
/* barrels & urns */
if (rnd() < 0.45) {
const amount = Math.max(1, Math.round(D2.BAL.goldDropBase(Math.max(1, game.monsterLevel() - 2)) * 0.5 * goldFindMult(game.player)));
game.pickups.push(new D2.entities.Pickup('gold', x, y, { amount }));
} else if (rnd() < 0.14) {
game.pickups.push(new D2.entities.Pickup('potion', x, y, {
potionType: rnd() < 0.65 ? 'hp' : 'mp'
}));
} else if (rnd() < 0.05) {
const item = D2.Items.rollItem(Math.max(1, game.monsterLevel() - 1));
if (item) game.pickups.push(new D2.entities.Pickup('item', x, y, { item }));
}
}
/** collect a pickup; returns true if consumed */
function collect(game, pk) {
const p = game.player;
switch (pk.kind) {
case 'gold': {
p.gold += pk.amount;
game.sfx('coin', { vol: .35 });
return true;
}
case 'potion': {
if (p.potions[pk.potionType] >= D2.BAL.potionStackMax) return false;
p.potions[pk.potionType]++;
game.sfx('pickup', { vol: .3 });
return true;
}
case 'item': {
if (p.inventory.length >= 40) {
game.toast(D2.i18n.t('msg.inventory_full'), 'bad');
return false;
}
p.inventory.push(pk.item);
game.sfx(pk.item.rarity === 'legendary' ? 'legendary' : 'pickup', { vol: .45 });
if (pk.item.rarity === 'magic' || pk.item.rarity === 'rare' || pk.item.rarity === 'legendary') {
game.toast(pk.item.name, pk.item.rarity === 'legendary' ? 'gold' : '');
}
if (D2.ui) D2.ui.refreshInventory();
return true;
}
}
return false;
}
D2.loot = { dropFor, rollPropLoot, collect };
})(window.D2);