javascript Copy
const MODULE_ID = "sc-item-rarity-colors" ;
const FILTER_IDS = {
glow: "scRarityGlow" ,
bevel: "scRarityBevel" ,
fog: "scRarityFog" ,
ray: "scRarityRay" ,
glowFx: "scRarityGlowFx"
};
if ( ! canvas?.tokens?.controlled?. length ) return ui.notifications. warn ( "Select at least one token." );
if ( ! globalThis.TokenMagic) return ui.notifications. error ( "TokenMagic not found." );
if ( ! game.modules. get ( MODULE_ID )?.active) return ui.notifications. error ( "SC Item Rarity Colors is not active." );
const controlledTokens = canvas.tokens.controlled;
const esc = ( s ) =>
String (s ?? "" ). replace ( / [&<>"'] / g , ( c ) => ({ "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ , "'" : "'" }[c]));
const hasSetting = ( k ) => game.settings.settings. has ( `${ MODULE_ID }.${ k }` );
const getSetting = ( k , fallback = null ) => ( hasSetting (k) ? game.settings. get ( MODULE_ID , k) : fallback);
const normalizeKey = ( v ) => String (v ?? "" ). trim (). toLowerCase ();
const cleanHex = ( v ) => {
const m = String (v ?? "" ). trim (). match ( / ^ #( [0-9a-f] {6}| [0-9a-f] {8} ) $ / i );
return m ? `#${ m [ 1 ]. slice ( 0 , 6 ). toUpperCase () }` : null ;
};
const hexToInt = ( h ) => Number. parseInt (h. slice ( 1 ), 16 );
function mixHex ( a , b , t = 0.5 ) {
const A = parseInt (a. slice ( 1 ), 16 );
const B = parseInt (b. slice ( 1 ), 16 );
const ar = ( A >> 16 ) & 255 , ag = ( A >> 8 ) & 255 , ab = A & 255 ;
const br = ( B >> 16 ) & 255 , bg = ( B >> 8 ) & 255 , bb = B & 255 ;
const rr = Math. round (ar + (br - ar) * t);
const rg = Math. round (ag + (bg - ag) * t);
const rb = Math. round (ab + (bb - ab) * t);
return `#${ (( rr << 16 ) | ( rg << 8 ) | rb ). toString ( 16 ). padStart ( 6 , "0" ). toUpperCase () }` ;
}
const lighten = ( h , amount = 0.35 ) => mixHex (h, "#FFFFFF" , amount);
const darken = ( h , amount = 0.45 ) => mixHex (h, "#000000" , amount);
function getRarityEntries () {
const out = new Map ();
// SC rarity list (includes custom rarities created in the module)
const listEnabled = getSetting ( "rarity-list-enabled" , true );
if (listEnabled) {
const cfg = getSetting ( "rarity-list-config" , {});
if (cfg && typeof cfg === "object" ) {
for ( const [ rawKey , rawVal ] of Object. entries (cfg)) {
const key = normalizeKey (rawVal?.key ?? rawKey);
if ( ! key || rawVal?.visible === false || out. has (key)) continue ;
out. set (key, { key, label: String (rawVal?.label ?? rawKey) });
}
}
}
// Fallback to system rarity names (if SC list is empty/missing entries)
const sys = CONFIG ?. DND5E ?.itemRarity ?? {};
if (sys && typeof sys === "object" ) {
for ( const [ k , v ] of Object. entries (sys)) {
const key = normalizeKey (k);
if ( ! key || out. has (key)) continue ;
out. set (key, { key, label: typeof v === "string" ? v : String (v?.label ?? k) });
}
}
return [ ... out. values ()];
}
function buildRarityData ( entry ) {
const primary = cleanHex ( getSetting ( `${ entry . key }-item-color` ));
const gradientOn = !! getSetting ( `${ entry . key }-gradient-option` , false );
const secondary = gradientOn ? cleanHex ( getSetting ( `${ entry . key }-secondary-item-color` )) : null ;
const glowOption = !! getSetting ( `${ entry . key }-glow-option` , false );
const colors = [primary, secondary]. filter (Boolean). filter (( c , i , arr ) => arr. indexOf (c) === i);
return { ... entry, colors, glowOption };
}
async function clearAllRarityFilters () {
for ( const t of controlledTokens) {
await TokenMagic. deleteFiltersOnSelected ();
}
}
async function applySingleGlow ( color1 ) {
const params = [{
filterType: "glow" ,
filterId: FILTER_IDS .glow,
color: hexToInt (color1),
animated: null
}];
for ( const t of controlledTokens) await TokenMagic. addUpdateFilters (t, params);
}
async function applyBevel ( color1 , color2 ) {
const params = [{
filterType: "bevel" ,
filterId: FILTER_IDS .bevel,
rotation: 0 ,
thickness: 5 ,
lightColor: hexToInt (color1),
lightAlpha: 0.8 ,
shadowColor: hexToInt (color2),
shadowAlpha: 0.5 ,
animated: {
rotation: {
active: true ,
clockWise: true ,
loopDuration: 1600 ,
animType: "syncRotation"
}
}
}];
for ( const t of controlledTokens) await TokenMagic. addUpdateFilters (t, params);
}
async function applyGlowEffect ( color1 , color2 = null ) {
const rayColor = color2 || lighten (color1, 0.35 );
const glowStart = darken (color1, 0.55 );
const glowEnd = color2 || lighten (color1, 0.45 );
const params = [
{
filterType: "fog" ,
filterId: FILTER_IDS .fog,
color: hexToInt (color1),
density: 0.20 ,
time: 0 ,
animated: {
time: {
active: true ,
speed: 1.2 ,
animType: "move"
}
}
},
{
filterType: "ray" ,
filterId: FILTER_IDS .ray,
time: 0 ,
color: hexToInt (rayColor),
alpha: 0.25 ,
divisor: 32 ,
anchorY: 0 ,
animated: {
time: {
active: true ,
speed: 0.0005 ,
animType: "move"
}
}
},
{
filterType: "glow" ,
filterId: FILTER_IDS .glowFx,
distance: 10 ,
outerStrength: 8 ,
innerStrength: 0 ,
color: hexToInt (glowStart),
quality: 0.5 ,
padding: 10 ,
animated: {
color: {
active: true ,
loopDuration: 3000 ,
animType: "colorOscillation" ,
val1: hexToInt (glowStart),
val2: hexToInt (glowEnd)
}
}
}
];
for ( const t of controlledTokens) await TokenMagic. addUpdateFilters (t, params);
}
const rarities = getRarityEntries (). map (buildRarityData);
if ( ! rarities. length ) return ui.notifications. warn ( "No rarities found." );
const optionsHtml = rarities
. map (( r ) => {
const c1 = r.colors[ 0 ] || "" ;
const c2 = r.colors[ 1 ] || "" ;
return `<option value="${ esc ( r . key ) }" data-c1="${ esc ( c1 ) }" data-c2="${ esc ( c2 ) }" data-glow="${ r . glowOption ? "1" : "0"}">${ esc ( r . label ) }</option>` ;
})
. join ( "" );
new Dialog ({
title: "Rarity Effect Picker" ,
content: `
<div style="display:flex; gap:10px; align-items:flex-end;">
<div style="flex:1;">
<label>Rarity</label>
<select name="rarity" style="width:100%;">${ optionsHtml }</select>
</div>
<div style="min-width:120px;">
<label>Colors</label>
<div data-role="preview" style="display:flex; gap:6px; min-height:28px; align-items:center;"></div>
</div>
</div>
<p data-role="mode" style="opacity:.85; margin:.5rem 0 0 0;"></p>
` ,
buttons: {
apply: {
label: "Apply" ,
callback : async ( html ) => {
const select = html[ 0 ]. querySelector ( 'select[name="rarity"]' );
const selected = rarities. find (( r ) => r.key === select?.value);
if ( ! selected) return ;
const c1 = selected.colors[ 0 ] || null ;
const c2 = selected.colors[ 1 ] || null ;
if ( ! c1) return ui.notifications. warn ( `"${ selected . label }" has no configured color.` );
await clearAllRarityFilters ();
if (selected.glowOption) {
await applyGlowEffect (c1, c2);
return ui.notifications. info ( `Applied Glow FX: ${ selected . label }` );
}
if (c2) {
await applyBevel (c1, c2);
return ui.notifications. info ( `Applied Bevel: ${ selected . label }` );
}
await applySingleGlow (c1);
ui.notifications. info ( `Applied Glow: ${ selected . label }` );
}
},
remove: {
label: "Remove" ,
callback : async () => {
await clearAllRarityFilters ();
ui.notifications. info ( "Rarity effects removed." );
}
},
cancel: { label: "Cancel" }
},
default: "apply" ,
render : ( html ) => {
const root = html[ 0 ];
const select = root. querySelector ( 'select[name="rarity"]' );
const preview = root. querySelector ( '[data-role="preview"]' );
const mode = root. querySelector ( '[data-role="mode"]' );
const paint = () => {
const opt = select?.selectedOptions?.[ 0 ];
if ( ! opt) return ;
const c1 = opt.dataset.c1 || null ;
const c2 = opt.dataset.c2 || null ;
const glow = opt.dataset.glow === "1" ;
if ( ! c1) {
preview.innerHTML = `<span style="opacity:.65;">No color</span>` ;
} else if (c2) {
preview.innerHTML = `
<span style="width:26px;height:26px;border:1px solid #666;border-radius:4px;background:${ c1 };display:inline-block;"></span>
<span style="width:26px;height:26px;border:1px solid #666;border-radius:4px;background:${ c2 };display:inline-block;"></span>
` ;
} else {
preview.innerHTML = `<span style="width:26px;height:26px;border:1px solid #666;border-radius:4px;background:${ c1 };display:inline-block;"></span>` ;
}
mode.textContent = glow
? "Mode: Glow FX (fog + ray + glow)"
: c2
? "Mode: Bevel (2 colors)"
: c1
? "Mode: Glow (1 color)"
: "Mode: None" ;
};
select?. addEventListener ( "change" , paint);
paint ();
}
}). render ( true );