@allemandi/gacha-engine
Version:
Practical, type-safe toolkit for simulating and understanding gacha rates and rate-ups.
2 lines (1 loc) • 4.52 kB
JavaScript
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).AllemandiGachaEngine={})}(this,function(t){"use strict";class e{static SCALE=1e6;static MAX_SAFE_SCALE=Math.floor(Number.MAX_SAFE_INTEGER/e.SCALE);mode;pools=[];rarityRatesScaled={};flatRateMap=new Map;dropRateCacheScaled=new Map;flatRateRateUpItems=[];constructor(t){if(this.mode=t.mode,"weighted"===t.mode){const e=t;this.pools=e.pools,this.rarityRatesScaled=this.scaleRarityRates(e.rarityRates),this.validateConfig(e.rarityRates)}else{if("flatRate"!==t.mode)throw new Error(`Unknown gacha mode: ${this.mode}`);{const e=t;this.pools=e.pools;for(const t of e.pools)for(const e of t.items){if(e.weight<0)throw new Error(`FlatRate item "${e.name}" must have non-negative weight`);this.flatRateMap.set(e.name,e.weight),e.rateUp&&this.flatRateRateUpItems.push(e.name)}const a=Array.from(this.flatRateMap.values()).reduce((t,e)=>t+e,0);if(Math.abs(a-1)>1e-6)throw new Error(`FlatRate item rates must sum to 1.0, but got ${a}`)}}}scaleRarityRates(t){const e={};for(const[a,r]of Object.entries(t)){if(r<0||r>1)throw new Error(`Rarity rate for "${a}" must be between 0 and 1, got ${r}`);e[a]=this.toScaled(r)}return e}toScaled(t){if(t>e.MAX_SAFE_SCALE/e.SCALE)throw new Error(`Probability ${t} too large for safe integer arithmetic`);return Math.round(t*e.SCALE)}fromScaled(t){return t/e.SCALE}validateConfig(t){const e=new Set(Object.keys(this.rarityRatesScaled)),a=new Set(this.pools.map(t=>t.rarity)),r=Array.from(a).filter(t=>!e.has(t));if(r.length>0)throw new Error(`Missing rarity rates for: ${r.join(", ")}`);const o=Object.values(t).reduce((t,e)=>t+e,0);if(Math.abs(o-1)>1e-10)throw new Error(`Rarity rates must sum to 1.0, got ${o}`);for(const t of this.pools){if(0===t.items.length)throw new Error(`Rarity "${t.rarity}" has no items`);if(t.items.reduce((t,e)=>t+e.weight,0)<=0)throw new Error(`Rarity "${t.rarity}" has zero total weight`);for(const e of t.items)if(e.weight<0)throw new Error(`Item "${e.name}" weight must be non-negative, got ${e.weight}`);if(!t.items.some(t=>t.weight>0))throw new Error(`Rarity "${t.rarity}" must have at least one item with positive weight`)}}getItemDropRate(t){if("flatRate"===this.mode)return this.flatRateMap.get(t)||0;if(this.dropRateCacheScaled.has(t))return this.fromScaled(this.dropRateCacheScaled.get(t));for(const a of this.pools){const r=a.items.find(e=>e.name===t);if(r){if(0===r.weight)return this.dropRateCacheScaled.set(t,0),0;const o=a.items.reduce((t,e)=>t+e.weight,0),i=this.rarityRatesScaled[a.rarity],s=this.toScaled(r.weight),n=this.toScaled(o),h=Math.round(s*i/e.SCALE),l=Math.round(h*e.SCALE/n);return this.dropRateCacheScaled.set(t,l),this.fromScaled(l)}}throw new Error(`Item "${t}" not found`)}getCumulativeProbabilityForItem(t,e){const a=this.getItemDropRate(t);return 0===a?0:a>=1?1:1-Math.pow(1-a,e)}getRollsForTargetProbability(t,e){if(e<=0)return 0;if(e>=1)return 1;const a=this.getItemDropRate(t);return a<=0?1/0:Math.ceil(Math.log(1-e)/Math.log(1-a))}getRateUpItems(){return"weighted"===this.mode?this.pools.flatMap(t=>t.items.filter(t=>t.rateUp).map(t=>t.name)):this.flatRateRateUpItems}getAllItemDropRates(){return"flatRate"===this.mode?Array.from(this.flatRateMap.entries()).map(([t,e])=>({name:t,dropRate:e,rarity:"flatRate"})):this.pools.flatMap(t=>t.items.map(e=>({name:e.name,dropRate:this.getItemDropRate(e.name),rarity:t.rarity})))}roll(t=1){const e=[];for(let a=0;a<t;a++)if("flatRate"===this.mode){const t=Math.random();let a=0;for(const[r,o]of this.flatRateMap.entries())if(a+=o,t<a){e.push(r);break}}else{const t=this.selectRarity(),a=this.pools.find(e=>e.rarity===t),r=this.selectItemFromPool(a);e.push(r.name)}return e}selectRarity(){const t=Math.floor(Math.random()*e.SCALE);let a=0;for(const[e,r]of Object.entries(this.rarityRatesScaled))if(a+=r,t<a)return e;return Object.keys(this.rarityRatesScaled)[0]}selectItemFromPool(t){const e=t.items.filter(t=>t.weight>0),a=e.map(t=>({...t,scaledWeight:this.toScaled(t.weight)})),r=a.reduce((t,e)=>t+e.scaledWeight,0),o=Math.floor(Math.random()*r);let i=0;for(const t of a)if(i+=t.scaledWeight,o<i)return{name:t.name,weight:t.weight};return e[0]}getDebugInfo(){const t={};for(const[e,a]of Object.entries(this.rarityRatesScaled))t[e]=this.fromScaled(a);return{scale:e.SCALE,rarityRatesScaled:{...this.rarityRatesScaled},rarityRatesFloat:t}}}t.GachaEngine=e});//# sourceMappingURL=index.umd.js.map