big cleanup, more config & get args for setting some config opts; improve debug logging

master
Ondřej Hruška 5 years ago
parent c4e86ef1a4
commit 8ec8ac6818
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 330
      script.js
  2. 59
      style.css

@ -1,5 +1,3 @@
!(function () {
class Svg { class Svg {
/** /**
* Build a node from XML * Build a node from XML
@ -122,8 +120,10 @@
} }
this.onOrbClick = (index, orb) => { this.onOrbClick = (index, orb) => {
// placeholder
}; };
this.onTileClick = (index) => { this.onTileClick = (index) => {
// placeholder
}; };
this.initOrb(); this.initOrb();
@ -190,7 +190,7 @@
let scaleX = w / (1066 + pad * 2); let scaleX = w / (1066 + pad * 2);
let scaleY = h / (926 + pad * 2); let scaleY = h / (926 + pad * 2);
let scale = Math.min(scaleX, scaleY); let scale = Math.min(scaleX, scaleY);
this.$root.setAttribute('transform', `translate(${w / 2},${h / 2}) scale(${scale})`) this.$root.setAttribute('transform', `translate(${w / 2},${h / 2}) scale(${scale})`);
this.rescaleTimeout = null; this.rescaleTimeout = null;
} }
@ -363,15 +363,13 @@
r="50" cy="5" cx="0" r="50" cy="5" cx="0"
fill="black" fill="black"
class="orb-shadow" class="orb-shadow"
filter="url('#filterDropshadow')"
opacity="1" opacity="1"
fill-opacity="0.7" /> fill-opacity="0.7" />
<circle <circle
r="55" cy="2" cx="0" r="55" cy="0" cx="0"
fill="white" fill="white"
class="orb-glow" class="orb-glow"
opacity="0" opacity="0" />
filter="url('#filterDropshadow')" />
<circle <circle
r="49" cy="0" cx="0" r="49" cy="0" cx="0"
fill="#9F9F9F" fill="#9F9F9F"
@ -550,7 +548,6 @@
water: '#0fdac3', water: '#0fdac3',
earth: '#99ff11', earth: '#99ff11',
// TODO metals need color adjust + ideally should have different visuals
mercury: '#f2e7b4', mercury: '#f2e7b4',
lead: '#728686', lead: '#728686',
tin: '#c5be9b', tin: '#c5be9b',
@ -588,7 +585,7 @@
*/ */
removeAllOrbs() { removeAllOrbs() {
Object.keys(this.grid).forEach((n) => { Object.keys(this.grid).forEach((n) => {
this.removeOrbByIndex(n); this.removeOrbByIndex(+n);
}) })
} }
@ -615,10 +612,13 @@
* @param {Number|null} seed * @param {Number|null} seed
*/ */
constructor(seed = null) { constructor(seed = null) {
this.seed = null;
if (seed === null) { if (seed === null) {
seed = +new Date; seed = +new Date;
} }
this.seed(seed);
this.setSeed(+seed);
} }
/** /**
@ -626,7 +626,9 @@
* *
* @param {Number} seed * @param {Number} seed
*/ */
seed(seed) { setSeed(seed) {
seed = +seed;
this.seed = seed;
this.state = seed; this.state = seed;
} }
@ -656,15 +658,17 @@
class SettingsStorage { class SettingsStorage {
constructor() { constructor() {
// this object is never overwritten, references are stable // this object is never overwritten, references are stable
this.settings = { this.defaults = {
debug: false, log: 'info',
version: 1, version: 1,
allowTemplateAugmenting: true, allowTemplateAugmenting: false,
retryTemplate: 30, retryTemplate: 30,
attemptTemplates: 50, attemptTemplates: 50,
animations: true, svgAnimations: false,
svgBlur: false,
disabledEffect: true, disabledEffect: true,
}; };
this.settings = Object.assign({}, this.defaults);
} }
load() { load() {
@ -693,7 +697,58 @@
} }
save() { save() {
localStorage.setItem('sigmar_settings', JSON.stringify(this.settings)); let changed = Object.entries(this.settings).reduce((acu, [k, v]) => {
if (this.defaults[k] !== v) {
acu[k] = v;
}
return acu;
}, {});
localStorage.setItem('sigmar_settings', JSON.stringify(changed));
}
}
class Nav {
/**
* Replace URL in the address bar
* @param new_url
*/
static replaceUrl(new_url) {
history.replaceState(null, null, new_url);
}
/**
* Set URL args (GET).
*
* @param args - arguments to set, or delete (when the value is null)
*/
static setUrlArgs(args) {
let url = new URL(location.href);
let query = new URLSearchParams(url.search);
for (let [k, v] of Object.entries(args)) {
if (v === null) {
query.delete(k);
} else {
query.set(k, v);
}
}
url.search = query.toString();
history.replaceState(null, null, url.href);
}
/**
* Get URL arguments
*
* @return {object}
*/
static getUrlArgs() {
let url = new URL(location.href);
let query = new URLSearchParams(url.search);
let params = {};
for (const [key, value] of query) {
params[key] = value;
}
return params;
} }
} }
@ -701,19 +756,65 @@
/** /**
* Init the game * Init the game
*/ */
constructor(seed = null) { constructor() {
this.LOGLEVELS = ['error', 'warn', 'info', 'debug', 'trace'];
this.settingsStore = new SettingsStorage(); this.settingsStore = new SettingsStorage();
this.cfg = this.settingsStore.load(); this.cfg = this.settingsStore.load();
this.debug("Game settings:", this.cfg); this.applyLogFilter();
// TODO take seed from hash this.get_opts = {
url_seed: true,
template: null,
template_flip: null,
};
let args = Object.assign({
seed: null,
debug: null,
trace: null,
log: null,
pretty: null,
rnd: null,
template: null,
}, Nav.getUrlArgs());
this.board = new Board(); this.board = new Board();
if (seed === null) { this.rng = new Rng();
seed = +new Date();
// Debug can be toggled via the debug=0/1 GET arg
if (args.debug !== null) {
this.setCfg({log: (!!+args.debug) ? 'debug' : 'info'});
}
if (args.trace !== null) {
this.setCfg({log: (!!+args.trace) ? 'trace' : 'debug'});
}
if (args.log !== null) {
this.setCfg({log: args.log});
}
if (args.rnd !== null) {
this.get_opts.url_seed = !!!+args.rnd;
}
if (args.template !== null) {
let tpl = args.template;
this.get_opts.template = tpl;
this.get_opts.template_flip = false;
if (tpl.endsWith('_flip')) {
this.get_opts.template_flip = true;
this.get_opts.template = tpl.substring(0, tpl.length - '_flip'.length);
}
}
// Toggle GPU intensive effects via the pretty=0/1 GET arg
if (args.pretty !== null) {
let pretty = !!+args.pretty;
this.setCfg({
svgAnimations: pretty,
svgBlur: pretty,
});
} }
this.rng = new Rng(seed); this.info("Game settings:", this.cfg);
this.layoutTemplates = { this.layoutTemplates = {
// templates apparently all have 55 items // templates apparently all have 55 items
@ -747,28 +848,44 @@
this.applySettings(); this.applySettings();
setTimeout(() => this.newGame(), 100); // Defer start to give browser time to render the background
setTimeout(() => {
this.newGame(args.seed)
}, 50);
}
applyLogFilter() {
let index = this.LOGLEVELS.indexOf(this.cfg.log);
for (let level of this.LOGLEVELS) {
this['logging_' + level] = index >= this.LOGLEVELS.indexOf(level);
}
}
trace(...args) {
if (this.logging_trace) console.debug(...args);
} }
debug(...args) { debug(...args) {
if (this.cfg.debug) console.log(...args); if (this.logging_debug) console.log(...args);
} }
info(...args) { info(...args) {
console.info(...args); if (this.logging_info) console.info(...args);
} }
warn(...args) { warn(...args) {
console.warn(...args); if (this.logging_warn) console.warn(...args);
} }
error(...args) { error(...args) {
console.error(...args); if (this.logging_error) console.error(...args);
} }
applySettings() { applySettings() {
this.board.$svg.classList.toggle('cfg-no-anim', !this.cfg.animations); this.board.$svg.classList.toggle('cfg-no-anim', !this.cfg.svgAnimations);
this.board.$svg.classList.toggle('cfg-no-fade-disabled', !this.cfg.disabledEffect); this.board.$svg.classList.toggle('cfg-no-fade-disabled', !this.cfg.disabledEffect);
this.board.$svg.classList.toggle('cfg-no-blur', !this.cfg.svgBlur);
this.applyLogFilter();
} }
setCfg(update) { setCfg(update) {
@ -790,9 +907,9 @@
showTemplate(name, flip = false) { showTemplate(name, flip = false) {
this.board.removeAllOrbs(); this.board.removeAllOrbs();
this.getTemplate(name, flip).forEach((n) => { for (let n of this.getTemplate(name, flip).positions) {
this.board.placeOrbByIndex(n, 'lead'); this.board.placeOrbByIndex(n, 'lead');
}) }
} }
/** /**
@ -800,9 +917,9 @@
*/ */
showRandomTemplate() { showRandomTemplate() {
this.board.removeAllOrbs(); this.board.removeAllOrbs();
this.getRandomTemplate().forEach((n) => { for (let n of this.getRandomTemplate().positions) {
this.board.placeOrbByIndex(n, 'lead'); this.board.placeOrbByIndex(n, 'lead');
}) }
} }
/** /**
@ -810,22 +927,27 @@
* *
* @param {String} name * @param {String} name
* @param {boolean} flipped * @param {boolean} flipped
* @returns {number[]} * @returns {{name: String, flipped: Boolean, positions: Number[]}}
*/ */
getTemplate(name, flipped) { getTemplate(name, flipped) {
let tpl = this.layoutTemplates[name].slice(0); // this slice takes a copy so the array is not corrupted by later manipulations let positions = this.layoutTemplates[name].slice(0); // this slice takes a copy so the array is not corrupted by later manipulations
if (flipped) { if (flipped) {
tpl = this.flipTemplate(tpl); positions = this.flipTemplate(positions);
} }
return tpl; return {
basename: name,
name: name+(flipped?'_flip':''),
flipped,
positions,
};
} }
/** /**
* Get a random and randomly flipped template * Get a random and randomly flipped template
* *
* @return Number[] - template indices; this is a clone, free to modify * @returns {{name: String, flipped: Boolean, positions: Number[]}}
*/ */
getRandomTemplate() { getRandomTemplate() {
let names = Object.keys(this.layoutTemplates); let names = Object.keys(this.layoutTemplates);
@ -834,12 +956,10 @@
let tpl = this.getTemplate(name, flipped); let tpl = this.getTemplate(name, flipped);
// 60 (center) must be included to place gold // 60 (center) must be included to place gold
if (!tpl.includes(60)) { if (!tpl.positions.includes(60)) {
throw Error(`Template "${name}", flip=${+flipped}, lacks 60.`); throw Error(`Template "${name}", flip=${+flipped}, lacks 60.`);
} }
console.info(`Selected board layout template "${name}", flipped=${+flipped}`);
return tpl; return tpl;
} }
@ -936,6 +1056,8 @@
} }
placeOrbs(template) { placeOrbs(template) {
let tilesAdded = 0;
this.board.removeAllOrbs(); this.board.removeAllOrbs();
let allowedTable = []; let allowedTable = [];
@ -961,7 +1083,7 @@
} }
const place = (n, symbol) => { const place = (n, symbol) => {
this.debug(`Place ${n} <- ${symbol}`); this.trace(`Place ${n} <- ${symbol}`);
if (!allowedTable[n]) throw Error(`Position ${n} not allowed by template`); if (!allowedTable[n]) throw Error(`Position ${n} not allowed by template`);
if (this.board.grid[n]) throw Error(`Position ${n} is occupied by ${this.board.grid[n]}`); if (this.board.grid[n]) throw Error(`Position ${n} is occupied by ${this.board.grid[n]}`);
// we use a hack to speed up generation here - SVG is not altered until we have a solution // we use a hack to speed up generation here - SVG is not altered until we have a solution
@ -969,14 +1091,13 @@
}; };
const unplace = (n) => { const unplace = (n) => {
this.debug(`Unplace ${n}`); this.trace(`Unplace ${n}`);
this.board.grid[n] = null; this.board.grid[n] = null;
}; };
const findAvailableIndexWithNeighbours = (count, except = null) => { const findAvailableIndexWithNeighbours = (count, except = null) => {
let candidates = []; let candidates = [];
for (let i = 0; i < template.length; i++) { for (let n of template) {
const n = template[i];
if (except && except.includes(n)) continue; if (except && except.includes(n)) continue;
if (!this.board.grid[n]) { if (!this.board.grid[n]) {
@ -1022,6 +1143,7 @@
outsideTemplate.splice(outsideTemplate.indexOf(toAdd), 1); outsideTemplate.splice(outsideTemplate.indexOf(toAdd), 1);
template.push(toAdd); template.push(toAdd);
tilesAdded++;
this.warn(`Adding extra tile to template: ${toAdd}`); this.warn(`Adding extra tile to template: ${toAdd}`);
return toAdd; return toAdd;
@ -1035,13 +1157,16 @@
const toPlace = this.buildPlacementList(); const toPlace = this.buildPlacementList();
let solution = []; let solution = [
['gold', 60],
];
while (toPlace.length > 0) { while (toPlace.length > 0) {
this.debug('placing a pair.'); this.trace('placing a pair.');
let symbol1 = toPlace.pop(); let symbol1 = toPlace.pop();
let index1 = findAvailableIndex(); let index1 = findAvailableIndex();
place(index1, symbol1); place(index1, symbol1);
solution.push([symbol1, index1]);
let symbol2 = toPlace.pop(); let symbol2 = toPlace.pop();
let index2 = findAvailableIndex(); let index2 = findAvailableIndex();
@ -1055,12 +1180,13 @@
let suc = false; let suc = false;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
this.debug(`try #${i + 1}`); this.trace(`try #${i + 1}`);
let index = findAvailableIndex(except); let index = findAvailableIndex(except);
place(index, symbol2); place(index, symbol2);
if (this.isAvailable(index1)) { if (this.isAvailable(index1)) {
suc = true; suc = true;
index2 = index;
break; break;
} else { } else {
unplace(index); unplace(index);
@ -1073,7 +1199,7 @@
} }
} }
solution.push([symbol1, index1]); // index2 is updated in the fixing loop
solution.push([symbol2, index2]); solution.push([symbol2, index2]);
} }
@ -1088,6 +1214,11 @@
}); });
this.debug('Solution:', solution); this.debug('Solution:', solution);
return {
solution,
tilesAdded: tilesAdded,
};
} }
/** /**
@ -1113,14 +1244,7 @@
* @return {number} * @return {number}
*/ */
countOrbs() { countOrbs() {
let n = 0; return this.board.grid.reduce((acu, x) => acu + (x !== null), 0);
// todo use reduce
this.board.grid.forEach((x) => {
if (x !== null) {
n++;
}
});
return n;
} }
/** /**
@ -1162,7 +1286,7 @@
while (true) { while (true) {
const n = this.rng.nextInt(toPlace.length - 1); const n = this.rng.nextInt(toPlace.length - 1);
if (toPlace[n][1] !== 'salt') { if (toPlace[n][1] !== 'salt') {
this.debug(`Pairing ${toPlace[n][1]} with salt.`); this.trace(`Pairing ${toPlace[n][1]} with salt.`);
newSaltedPairs.push([toPlace[n][1], 'salt']); newSaltedPairs.push([toPlace[n][1], 'salt']);
toPlace[n][1] = 'salt'; toPlace[n][1] = 'salt';
break; break;
@ -1204,12 +1328,14 @@
mPos.push(x) mPos.push(x)
} }
mPos.sort((a, b) => a - b); mPos.sort((a, b) => a - b);
this.debug('Metal positions ', mPos); this.trace('Metal positions ', mPos);
// inject them into the array // inject them into the array
metals.forEach((pair, i) => { metals.forEach((pair, i) => {
toPlace.splice(mPos[i] + i, 0, pair); toPlace.splice(mPos[i] + i, 0, pair);
}); });
this.debug('Placement order (last first):', toPlace);
return toPlace.reduce((a, c) => { return toPlace.reduce((a, c) => {
a.push(c[0]); a.push(c[0]);
a.push(c[1]); a.push(c[1]);
@ -1267,35 +1393,48 @@
* @param n * @param n
* @param orb * @param orb
*/ */
ingameBoardClick(n, orb) { inGameBoardClick(n, orb) {
if (!this.isAvailableAtPlaytime(n)) return; this.debug(`Clicked orb ${n}: ${orb.symbol}`);
if (!this.isAvailableAtPlaytime(n)) {
this.debug(`Orb is blocked`);
return;
}
let wantRefresh = false; let wantRefresh = false;
if (orb.symbol === 'gold') { if (orb.symbol === 'gold') {
// gold has no pairing // gold has no pairing
this.debug(`Removing gold.`);
this.board.removeOrbByIndex(n); this.board.removeOrbByIndex(n);
this.selectedOrb = null; this.selectedOrb = null;
wantRefresh = true; wantRefresh = true;
} else if (this.selectedOrb === null) { } else if (this.selectedOrb === null) {
this.debug(`Select orb`);
// first selection // first selection
this.selectedOrb = {n, orb}; this.selectedOrb = {n, orb};
orb.node.classList.add('selected'); orb.node.classList.add('selected');
} else { } else {
if (this.selectedOrb.n === n) { if (this.selectedOrb.n === n) {
this.debug(`Unselect orb`);
// orb clicked twice // orb clicked twice
orb.node.classList.remove('selected'); orb.node.classList.remove('selected');
this.selectedOrb = null; this.selectedOrb = null;
} else { } else {
this.debug(`Second selection, try to match`);
// second orb in a pair // second orb in a pair
const otherSymbol = this.selectedOrb.orb.symbol; const otherSymbol = this.selectedOrb.orb.symbol;
if (this.getPairSymbols(orb.symbol).includes(otherSymbol)) { if (this.getPairSymbols(orb.symbol).includes(otherSymbol)) {
this.debug(`Match confirmed, removing ${this.selectedOrb.n} (${this.selectedOrb.orb.symbol} + ${n} (${orb.symbol}`);
// compatible pair clicked // compatible pair clicked
this.board.removeOrbByIndex(n); this.board.removeOrbByIndex(n);
this.board.removeOrbByIndex(this.selectedOrb.n); this.board.removeOrbByIndex(this.selectedOrb.n);
if ([orb.symbol, otherSymbol].includes(this.nextMetal)) { if ([orb.symbol, otherSymbol].includes(this.nextMetal)) {
this.debug("Advance metal transmutation sequence.");
this.advanceMetal(); this.advanceMetal();
} }
@ -1303,6 +1442,8 @@
wantRefresh = true; wantRefresh = true;
} else { } else {
this.debug("No match, start new pair selection.");
// Bad selection, select it as the first orb. // Bad selection, select it as the first orb.
this.selectedOrb.orb.node.classList.remove('selected'); this.selectedOrb.orb.node.classList.remove('selected');
this.selectedOrb = {n, orb}; this.selectedOrb = {n, orb};
@ -1320,7 +1461,20 @@
} }
} }
newGame() { newGame(seed) {
if (seed !== null) {
this.rng.setSeed(seed);
}
this.info("RNG seed is: " + this.rng.seed);
if (this.get_opts.url_seed) {
// Place seed in the navbar for bookmarking / sharing
Nav.setUrlArgs({
seed: this.rng.seed,
});
}
this.board.onTileClick = (n) => { this.board.onTileClick = (n) => {
this.debug(n, gridIndexToXy(n)); this.debug(n, gridIndexToXy(n));
}; };
@ -1328,32 +1482,42 @@
this.selectedOrb = null; this.selectedOrb = null;
let self = this; let self = this;
this.board.onOrbClick = (n, orb) => self.ingameBoardClick(n, orb); this.board.onOrbClick = (n, orb) => self.inGameBoardClick(n, orb);
// retry loop, should not be needed if everything is correct // retry loop, should not be needed if everything is correct
let suc = false; let suc = false;
let numretries = 0; let retry_count = 0;
const alertOnError = false; let board_info = null;
for (let i = 0; i < this.cfg.attemptTemplates && !suc; i++) { for (let n_tpl = 0; n_tpl < this.cfg.attemptTemplates && !suc; n_tpl++) {
this.debug('RNG seed is: ' + this.rng.state); this.debug('RNG seed is: ' + this.rng.state);
const template = this.getRandomTemplate();
for (let j = 0; j < this.cfg.retryTemplate; j++) { let template;
if (n_tpl === 0 && this.get_opts.template !== null) {
template = this.getTemplate(this.get_opts.template, this.get_opts.template_flip);
} else {
template = this.getRandomTemplate();
}
this.info(`Selected board layout template "${template.name}"`);
for (let n_solution = 0; n_solution < this.cfg.retryTemplate; n_solution++) {
try { try {
this.placeOrbs(template.slice(0)); // clone board_info = this.placeOrbs(template.positions.slice(0)); // clone
board_info.template = template;
suc = true; suc = true;
break; break;
} catch (e) { } catch (e) {
if (alertOnError) alert('welp'); retry_count++;
numretries++; if (this.logging_trace) {
if (this.cfg.debug) {
this.error(e); this.error(e);
} else { } else {
this.warn(e.message); this.warn(e.message);
} }
} }
} }
if (!suc) { if (!suc) {
this.warn("Exhausted all retries for the template, getting a new one"); this.warn(`Exhausted all retries for the template "${template.name}", getting a new one`);
} }
} }
@ -1362,9 +1526,26 @@
this.updateOrbDisabledStatus(); this.updateOrbDisabledStatus();
if (!suc) { if (!suc) {
alert(`Sorry, could not find a valid board setup after ${numretries} retries.`); alert(`Sorry, could not find a valid board setup after ${retry_count} retries.`);
} else { } else {
this.info(`Found valid solution (with ${numretries} retries)`); this.info(
`Board set up with ${retry_count} retries:\n` +
`teplate "${board_info.template.name}"` +
(this.cfg.allowTemplateAugmenting ? ` with ${board_info.tilesAdded} extra tiles` : ''));
this.info('Reference solution:\n ' + board_info.solution.reduce((s, entry, i) => {
s += `${entry[0]} ${entry[1]}`;
if (i % 2 === 1) {
s += "\n ";
} else {
if (entry[0] !== 'gold') {
s += " + ";
}
}
return s;
}, ''));
} }
} }
@ -1398,4 +1579,3 @@
window.game = new Game(); window.game = new Game();
})();

@ -1,4 +1,4 @@
*,*:before,*:after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
} }
@ -22,6 +22,7 @@ html,body {
opacity: 0.6; opacity: 0.6;
} }
/* Highlighting */
.highlight-salt .symbol-salt .orb-fill, .highlight-salt .symbol-salt .orb-fill,
.highlight-air .symbol-air .orb-fill, .highlight-air .symbol-air .orb-fill,
.highlight-fire .symbol-fire .orb-fill, .highlight-fire .symbol-fire .orb-fill,
@ -40,33 +41,33 @@ html,body {
stroke-width: 7px; stroke-width: 7px;
} }
.orb.disabled .orb-glow, /* Orb is clickable */
.orb.disabled .orb-shadow,
.orb.disabled:hover .orb-glow,
.orb.disabled:hover .orb-shadow {
opacity: 0;
}
.orb { .orb {
cursor: pointer; cursor: pointer;
} }
/* Disabled effect */
.orb.disabled { .orb.disabled {
cursor: default; cursor: default;
opacity: 0.6; opacity: 0.6;
} }
.cfg-no-fade-disabled .orb.disabled { /* Disabled orb has no glow or shadow */
opacity: 1; .orb.disabled .orb-glow,
.orb.disabled .orb-shadow,
.orb.disabled:hover .orb-glow,
.orb.disabled:hover .orb-shadow {
opacity: 0;
} }
.orb-glow, /* Blur effect */
.orb-shadow { .orb-shadow, .orb-glow {
transition: opacity linear 0.1s; filter: url('#filterDropshadow');
} }
.cfg-no-anim * { /* Hover and select effects */
transition: none !important; .orb-glow, .orb-shadow {
transition: opacity linear 0.1s;
} }
.orb.selected .orb-glow, .orb.selected .orb-glow,
@ -78,3 +79,31 @@ html,body {
.orb:hover .orb-shadow { .orb:hover .orb-shadow {
opacity: 0; opacity: 0;
} }
/* No-anim version applies animations instantly */
.cfg-no-anim * {
transition: none !important;
}
/* No-blur version uses white ring around selected orbs, and has no shadow */
.cfg-no-blur .orb-shadow,
.cfg-no-blur .orb-glow {
filter: none !important;
opacity: 0 !important;
}
.cfg-no-blur .orb.selected .orb-fill,
.cfg-no-blur .orb:hover .orb-fill {
stroke: white;
stroke-width: 10px;
paint-order: stroke;
}
.cfg-no-blur .orb.disabled .orb-fill {
stroke: transparent !important;
}
/* No-disabled-fade version has all orbs at full opacity */
.cfg-no-fade-disabled .orb.disabled {
opacity: 1;
}

Loading…
Cancel
Save