code:
window.getIntegrityInfo = function (integrity) {
	if (integrity >= 900)
		return [7, "green"];
	if (integrity >= 500)
		return [6, "teal"];
	if (integrity >= 200)
		return [5, "lblue"];
	if (integrity >= 100)
		return [4, "blue"];
	if (integrity >= 50)
		return [3, "purple"];
	if (integrity >= 20)
		return [2, "pink"];
	return [1, "red"];
}

window.getRevealInfo = function (reveal) {
	if (reveal >= 900)
		return [7, "red"];
	if (reveal >= 700)
		return [6, "pink"];
	if (reveal >= 500)
		return [5, "purple"];
	if (reveal >= 300)
		return [4, "blue"];
	if (reveal >= 200)
		return [3, "lblue"];
	if (reveal >= 100)
		return [2, "teal"];
	return [1, "green"];
}

window.getWarmthInfo = function (warmth) {
	if (warmth >= 75)
		return [5, "warm-4"];
	if (warmth >= 50)
		return [4, "warm-3"];
	if (warmth >= 25)
		return [3, "warm-2"];
	if (warmth >= 10)
		return [2, "warm-1"];
	return [1, "warm-0"];
}

// for outfits it adds the lower piece's warmth too
window.getTrueWarmth = function (item) {
	let warmth = item.warmth || 0;

	if (item.outfitPrimary) {
		// sum of warmth of every secondary piece
		// outfitPrimary looks like this {'lower': 'item_name', 'head': 'item_name'}
		warmth += Object.keys(item.outfitPrimary) // loop through secondary items list
		.filter(x => item.outfitPrimary[x] != "broken") // filter out broken pieces
		.map(x => setup.clothes[x].find(z => z.name == item.outfitPrimary[x] && z.modder === item.modder)) // find items in setup.clothes
		.reduce((sum, x) => sum + (x.warmth || 0), 0); // calculate sum of their warmth field
	}

	if (item.outfitSecondary) {
		if (item.outfitSecondary.length % 2 != 0)
			console.log("WARNING: " + item.name + " has bad .outfitSecondary data!");

		// outfitSecondary looks like this ['upper', 'item_name', 'head', 'item_name']
		item.outfitSecondary.forEach((x, i) => {
			if (i % 2 == 0 && item.outfitSecondary[i + 1] != "broken") {
				warmth += setup.clothes[x].find(z => z.name == item.outfitSecondary[i + 1] && z.modder === item.modder).warmth || 0;
			}
		});
	}

	return warmth;
}

window.clothingSlotToIconName = function (slotName, outfits) {
	switch (slotName) {
		case "over_upper":
			return "overoutfit";
		case "upper":
			return outfits ? "outfit" : "upper";
		case "under_upper":
			return outfits ? "underoutfit" : "underupper";
		case "under_lower":
			return "underlower";
		default:
			return slotName;
	}
}

// Make .divs-links clickable as if they're anchors
window.linkifyDivs = function (parentSelector = "") {
	$(document).ready(() => { $(parentSelector + " .div-link").click(function (e) { $(this).find('a').first().click(); }) });
	$(document).ready(() => { $(parentSelector + " .div-link a").click(function (e) { e.stopPropagation(); }) });
}

// Hook custom colour sliders and preset dropdowns
window.attachCustomColourHooks = function (slot = "") {
	$(() => {
		// throttling for smoother experience
		let updating = false;
		$('.custom-colour-sliders.primary input[type=range]').on('input change', ()=>{
			if (updating) return;
			updating = true;
			requestAnimationFrame(()=>{
				updating = false;
				updateCustomColour('primary', slot); updateMannequin(slot);
			})
		});
		$('.custom-colour-sliders.secondary input[type=range]').on('input change', ()=>{
			if (updating) return;
			updating = true;
			requestAnimationFrame(()=>{
				updating = false;
				updateCustomColour('secondary', slot); updateMannequin(slot);
			})
		});
		$('.custom-colour.primary > .custom-colour-presets > .presets-dropdown > select').on('change', () => { loadCustomColourPreset('primary'); new Wikifier(null, '<<updateclotheslist>>'); });
		$('.custom-colour.secondary > .custom-colour-presets > .presets-dropdown > select').on('change', () => { loadCustomColourPreset('secondary'); new Wikifier(null, '<<updateclotheslist>>'); });

		$('.custom-colour-sliders.primary > .colour-slider > div > input').on('input', (e) => { V.customColors.sepia.primary = 0; });
		$('.custom-colour-sliders.secondary > .colour-slider > div > input').on('input', (e) => { V.customColors.sepia.secondary = 0; });
	});
}

window.updateCustomColour = function(type, slot) {
	$('.colour-options-div.' + type + ' > .colour-button > .bg-custom').css('filter', getCustomColourStyle(type, true));
	let model = Renderer.locateModel("main", "shop");
	if (model) {
		let customColors = V.customColors;
		model.options.filters["worn_" + slot + (type === "primary" ? "_custom" : "_acc_custom")] =
			getCustomClothesColourCanvasFilter(customColors.color[type],
				customColors.saturation[type],
				customColors.brightness[type],
				customColors.contrast[type]);
	}
}

window.updateMannequin = function(slot = "") {
	new Wikifier(null, '<<updatemannequin "' + slot + '">>');
}

window.getCustomColourStyle = function (type, valueOnly = false) {
	if (type != 'primary' && type != 'secondary')
		return;
	return (valueOnly ? '' : 'filter: ') + 'hue-rotate(' + V.customColors.color[type] + 'deg) saturate(' + V.customColors.saturation[type] + ') brightness(' + V.customColors.brightness[type] + ') contrast(' + V.customColors.contrast[type] + ')' + (valueOnly ? '' : ';');
}

window.saveCustomColourPreset = function (slot = "primary") {
	let setName = prompt("Enter new colour preset name", "New preset");
	if (setName != null) {
		if (Object.keys(V.customColors.presets).includes(setName)) {
			alert('Preset "' + setName + '" already exists!');
			return;
		}

		V.customColors.presets[setName] = {
			ver: 2,
			color: V.customColors.color[slot],
			brightness: V.customColors.brightness[slot],
			saturation: V.customColors.saturation[slot],
			contrast: V.customColors.contrast[slot]
		};
	}
}

window.loadCustomColourPreset = function (slot = "primary") {
	let setName = T.preset_choice[slot];
	let preset = V.customColors.presets[setName];
	if (preset) {
		// new version of preset (has only one set of colour parameters and doesn't have sepia)
		if (preset.ver >= 2) {
			V.customColors.color[slot] = preset.color;
			V.customColors.brightness[slot] = preset.brightness;
			V.customColors.saturation[slot] = preset.saturation;
			V.customColors.contrast[slot] = preset.contrast;
			V.customColors.sepia[slot] = 0;
		}
		// legacy preset (has both primary and secondary colours information)
		else {
			V.customColors.color.primary = preset.color.primary;
			V.customColors.brightness.primary = preset.brightness.primary;
			V.customColors.saturation.primary = preset.saturation.primary;
			V.customColors.contrast.primary = preset.contrast.primary;
			V.customColors.sepia.primary = preset.sepia.primary;

			V.customColors.color.secondary = preset.color.secondary;
			V.customColors.brightness.secondary = preset.brightness.secondary;
			V.customColors.saturation.secondary = preset.saturation.secondary;
			V.customColors.contrast.secondary = preset.contrast.secondary;
			V.customColors.sepia.secondary = preset.sepia.secondary;
		}
	}
}

// adjusts available options for reveal dropdowns (makes sure upper bound is not below lower bound and vice versa)
window.getFilterRevealOptions = function (type) {
	let optionsFrom = { unassuming: 0, smart: 100, tasteful: 200, comfy: 300, seductive: 500, risqué: 700, lewd: 900 };
	let optionsTo = { unassuming: 100, smart: 200, tasteful: 300, comfy: 500, seductive: 700, risqué: 900, lewd: 9999 };

	if (type == 'from') {
		// this line removes values that are larger than reveal.to
		return Object.keys(optionsFrom).filter(x => optionsFrom[x] < V.shopClothingFilter.reveal.to).reduce((res, key) => (res[key] = optionsFrom[key], res), {})
	}
	else {
		// this line removes values that are smaller than reveal.from
		return Object.keys(optionsTo).filter(x => optionsTo[x] > V.shopClothingFilter.reveal.from).reduce((res, key) => (res[key] = optionsTo[key], res), {})
	}
}

// toggles checkboxes in filters menu
window.toggleAllTraitsFilter = function () {
	let chboxes = $('#filter-traits input:not(:checked)');
	if (chboxes.length > 0)
		chboxes.click();
	else
		$('#filter-traits input:checked').click();
}

// accepts a list of clothes, returns a filtered list of clothes
window.applyClothingShopFilters = function (items) {
	let f = V.shopClothingFilter;
	if (!f.active)
		return items;

	// (example) turns f.gender object {female: true, neutral: true, male: false} into ["f", "n"], ready to compare with gender in items
	let allowedGenders = Object.keys(f.gender).filter(x => f.gender[x]).map(x => x.first());

	return items.filter(x => allowedGenders.includes(x.gender)
		&& x.reveal >= f.reveal.from && x.reveal < f.reveal.to
		&& x.warmth >= f.warmth.from && x.warmth < f.warmth.to
		&& (f.traits.length == 0 || f.traits.includesAny(x.type))
	);
}

window.getWarmthScaleData = function(newWarmth) {
	let maxWarmth = Math.max(260, V.warmth * 1.04);
	if (newWarmth)
		maxWarmth = Math.max(maxWarmth, newWarmth * 1.04);
	let chill = V.chill;
	let cold = chill - 90;
	let warm = chill * 1.3 + 70;
	let hot = chill * 1.3 + 150;
	let minW = Math.min(V.warmth, newWarmth);
	let maxW = Math.max(V.warmth, newWarmth);

	return {
		cold: cold / maxWarmth * 100 + '%',
		chill: (chill - Math.max(cold, 0)) / maxWarmth * 100 + '%',
		ok: (Math.min(warm, maxWarmth) - chill) / maxWarmth * 100 + '%',
		warm: (Math.min(hot, maxWarmth) - Math.min(warm, maxWarmth)) / maxWarmth * 100 + '%',
		hot: (maxWarmth - hot) / maxWarmth * 100 + '%',
		nowarm: warm > maxWarmth ? 'nowarm' : '',
		nohot: hot > maxWarmth ? 'nohot' : '',
		nocold: cold < 0 ? 'nocold' : '',
		player: V.warmth / maxWarmth * 100 + '%',
		playerNew: (V.warmth > newWarmth ? minW : maxW) / maxWarmth * 100 + '%',
		diffUpDown: (V.warmth > newWarmth ? 'down' : 'up'),
		diffStart: minW / maxWarmth * 100 + '%',
		diffWidth: (maxW - minW) / maxWarmth * 100 + '%'
	};
}

window.getWarmthWithOtherClothing = function(slot, clothingId) {
	let newClothing = setup.clothes[slot][clothingId];
	let worn = V.worn;

	let newWarmth = V.warmth + getTrueWarmth(newClothing);

	// subtract warmth of all clothes that would be taken off
	if (newClothing.outfitPrimary) {
		//newWarmth -= Object.keys(newClothing.outfitPrimary).reduce((sum, x) => sum + (worn[x].warmth || 0), 0);

		// compile a list of all primary clothes to be removed. It implies that item may have only one primary piece
		let clothesToRemove = [slot, ...Object.keys(newClothing.outfitPrimary)].map(x => (worn[x].outfitSecondary && worn[x].outfitSecondary[1] != "broken") ? setup.clothes[worn[x].outfitSecondary[0]].find(z => z.name == worn[x].outfitSecondary[1]) : worn[x])
		let removedClothes = new Set();

		clothesToRemove.forEach(x => {
			if (!removedClothes.has(x.name)) {
				newWarmth -= getTrueWarmth(x);
				removedClothes.add(x.name);
			}
		});
	}
	else
		newWarmth -= worn[slot].warmth;

	return newWarmth;
}


code:
function clamp255 (val) { return val > 255 ? 255 : val < 0 ? 0 : val }

// apply___Filter functions are ported from WebKit implementation of CSS filters
// https://github.com/WebKit/webkit/blob/main/Source/WebCore/platform/graphics/texmap/TextureMapperShaderProgram.cpp

function applySepiaFilter(colour, amount) {
	amount = 1.0 - amount;
	return {
		r: clamp255((0.393 + 0.607 * amount) * colour.r + (0.769 - 0.769 * amount) * colour.g + (0.189 - 0.189 * amount) * colour.b),
		g: clamp255((0.349 - 0.349 * amount) * colour.r + (0.686 + 0.314 * amount) * colour.g + (0.168 - 0.168 * amount) * colour.b),
		b: clamp255((0.272 - 0.272 * amount) * colour.r + (0.534 - 0.534 * amount) * colour.g + (0.131 + 0.869 * amount) * colour.b),
	};
}

function applySaturateFilter(colour, amount) {
	return {
		r: clamp255((0.213 + 0.787 * amount) * colour.r + (0.715 - 0.715 * amount) * colour.g + (0.072 - 0.072 * amount) * colour.b),
		g: clamp255((0.213 - 0.213 * amount) * colour.r + (0.715 + 0.285 * amount) * colour.g + (0.072 - 0.072 * amount) * colour.b),
		b: clamp255((0.213 - 0.213 * amount) * colour.r + (0.715 - 0.715 * amount) * colour.g + (0.072 + 0.928 * amount) * colour.b)
	};
}

function applyHueRotateFilter(colour, amount) {
	let c = Math.cos(amount * Math.PI / 180.0);
	let s = Math.sin(amount * Math.PI / 180.0);
	return {
		r: clamp255(colour.r * (0.213 + c * 0.787 - s * 0.213) + colour.g * (0.715 - c * 0.715 - s * 0.715) + colour.b * (0.072 - c * 0.072 + s * 0.928)),
		g: clamp255(colour.r * (0.213 - c * 0.213 + s * 0.143) + colour.g * (0.715 + c * 0.285 + s * 0.140) + colour.b * (0.072 - c * 0.072 - s * 0.283)),
		b: clamp255(colour.r * (0.213 - c * 0.213 - s * 0.787) +  colour.g * (0.715 - c * 0.715 + s * 0.715) + colour.b * (0.072 + c * 0.928 + s * 0.072)),
	};
}

function applyBrightnessFilter(colour, amount) {
	return { r: colour.r * amount, g: colour.g * amount, b: colour.b * amount };
}

function contrast(n, amount) { return clamp255((n - 128) * amount + 128); }
function applyContrastFilter(colour, amount) {
	return {
		r: contrast(colour.r, amount),
		g: contrast(colour.g, amount),
		b: contrast(colour.b, amount)
	};
}

window.getCustomColourRGB = function (hue, saturation, brightness, contrast, sepia = 0) {
	let col = { r: 255, g: 0, b: 0 };
	col = applyHueRotateFilter(col, hue);
	col = applySaturateFilter(col, saturation);
	col = applyBrightnessFilter(col, brightness);
	col = applyContrastFilter(col, contrast);
	col = applySepiaFilter(col, sepia);
	return {r: clamp255(col.r), g: clamp255(col.g), b: clamp255(col.b)};
}
window.getCustomClothesColourCanvasFilter = function(hue, saturation, brightness, contrast, sepia = 0) {
	if (arguments.length === 1 && typeof arguments[0] === 'string') {
		let match = parseCSSFilter(arguments[0]);
		if (!match) return clone(setup.colours.clothes_default);
		hue = +match[1];
		saturation = +match[2];
		brightness = +match[3];
		contrast = +match[4];
		sepia = +match[5];
	}
	let filterBrightness = 0.0;
	if (brightness >= 1.0) {
		// Slider brightness is 0..4, we consider 0..1 for colour spec
		// and everything above as extra brightness adjustment
		// In new renderer it's a shift, not multiplier, so we scale it from x1..x4 to +0..+0.21
		filterBrightness += (brightness-1)*0.07;
	}
	return Renderer.mergeLayerData({
		blend: tinycolor(getCustomColourRGB(hue, saturation, brightness, contrast, sepia)).toHexString(),
		contrast: contrast,
		brightness: filterBrightness
	}, setup.colours.clothes_default);
}

// It's a copy of this function from dolls/battleTestControls/controls.js
window.rgbToHsv = function (colour) {
	let rabs, gabs, babs, rr, gg, bb, h, s, v, diff, diffc, percentRoundFn;
	rabs = colour.r / 255;
	gabs = colour.g / 255;
	babs = colour.b / 255;
	v = Math.max(rabs, gabs, babs),
	diff = v - Math.min(rabs, gabs, babs);
	diffc = c => (v - c) / 6 / diff + 1 / 2;
	percentRoundFn = num => Math.round(num * 100) / 100;
	if (diff == 0) {
		h = s = 0;
	} else {
		s = diff / v;
		rr = diffc(rabs);
		gg = diffc(gabs);
		bb = diffc(babs);

		if (rabs === v) {
			h = bb - gg;
		} else if (gabs === v) {
			h = (1 / 3) + rr - bb;
		} else if (babs === v) {
			h = (2 / 3) + gg - rr;
		}
		if (h < 0) {
			h += 1;
		}else if (h > 1) {
			h -= 1;
		}
	}
	return {
		h: Math.round(h * 360),
		s: percentRoundFn(s * 100),
		v: percentRoundFn(v * 100)
	};
}

// return [ match, hue, saturation, brightness, contrast, sepia ]
window.parseCSSFilter = function(filter) {
	// extract values from filter string
	let result = filter.match(/(?:filter: hue-rotate\((-?\d{1,3})(?:deg\))? *(?:saturate\()([\d\.]+)\))? *(?:brightness\(([\d\.]+)\))? *(?:contrast\(([\d\.]+)\))? *(?:sepia\(([\d\.]+)\))?/);
	if (result[1] === undefined) result[1] = 0;
	if (result[2] === undefined) result[2] = 1;
	if (result[3] === undefined) result[3] = 1;
	if (result[4] === undefined) result[4] = 1;
	if (result[5] === undefined) result[5] = 0;
	return result;
}
window.getCustomColourName = function (hue, saturation, brightness, contrast) {
	if (arguments.length == 1 && typeof(arguments[0]) == "string") {
		let match = parseCSSFilter(arguments[0]);
		if (!match)
			return "custom";

		hue = match[1];
		saturation = match[2];
		brightness = match[3];
		contrast = match[4];
	}

	let rgb = getCustomColourRGB(hue, saturation, brightness, contrast);
	T.customRGB = `rgb(${Math.round(rgb.r)},${Math.round(rgb.g)},${Math.round(rgb.b)});`;
	let hsv = rgbToHsv(rgb);

	// very specific colours
	if (hsv.h >= 6 && hsv.h <= 11 && hsv.s > 60 && hsv.s < 80 && hsv.v > 85)
		return "coral";
	else if (hsv.h >= 20 && hsv.h <= 45 && hsv.s > 25 && hsv.s < 45 && hsv.v > 90)
		return "sand";

	// general desaturated colours
	else if (hsv.v <= 30 && hsv.s <= 20)
		return "black";
	else if (hsv.s < 10 && hsv.v > 90)
		return "white";
	else if (hsv.s < 12) {
		if (hsv.v >= 70)
			return "light grey";
		else if (hsv.v >= 50)
			return "grey";
		else
			return "dark grey";
	}
	// full rainbow action
	else {
		let main, aux;

		if (hsv.h <= 8 || hsv.h >= 345)
			main = "red";
		else if (hsv.h <= 44)
			main = "orange";
		else if (hsv.h <= 55)
			main = "yellow";
		else if (hsv.h <= 66)
			main = "chartreuse";
		else if (hsv.h <= 74)
			main = "lime";
		else if (hsv.h <= 150)
			main = "green";
		else if (hsv.h <= 165)
			main = "aquamarine";
		else if (hsv.h <= 180)
			main = "cyan";
		else if (hsv.h <= 215)
			main = "blue";
		else if (hsv.h <= 245)
			main = "indigo";
		else if (hsv.h <= 260)
			main = "violet";
		else if (hsv.h <= 285)
			main = "purple";
		else if (hsv.h <= 315)
			main = "magenta";
		else
			main = "rose";

		if (hsv.s < 60 && hsv.v > 80)
			aux = "light";
		else if (hsv.v <= 50)
			aux = "dark";
		else if (hsv.v > 50 && (hsv.h <= 100 && hsv.h >= 330 && hsv.s < 40
				|| hsv.h > 100 && hsv.h < 110 && hsv.s < 30
				|| hsv.h >= 100 && hsv.h < 150 && hsv.s < 20
				|| hsv.h >= 150 && hsv.h < 200 && hsv.s < 25
				|| hsv.h >= 200 && hsv.h < 330 && hsv.s < 30))
			aux = "greyish";

		if (main == "orange" && (hsv.s < 70 || hsv.v < 70))
			main = "brown";
		else if (main == "yellow" && hsv.v < 90)
			main = "brown";

		let colour = aux ? aux + " " + main : main;

		if (main == "brown" && (aux == "greyish" || aux == "light" || hsv.s < 25 && hsv.v < 70))
			colour = "beige";
		else if (colour == "greyish green" || (aux != "light" && main == "chartreuse"))
			colour = "olive";
		else if (aux == "light" && (main == "red" || main == "rose" || main == "magenta"))
			colour = "pink";
		else if (colour == "dark indigo")
			colour = "navy";
		else if (colour == "dark cyan")
			colour = "teal";

		return colour;
	}
}

repo_name: Area/degrees

license: mit


code:
/**
 * Adds the macros <<condition>> and <<compute>>.
 *
 * <<condition>> is a combination of <<widget>> and <<if>>. You define it like a
 * <<widget>>, but its body is a condition like in <<if>>. Example:
 *
 * <<condition "afternoon">>
 *   $hour gt 14 and $hour lt 18
 * <</condition>>
 *
 * Later you can use your new condition like this:
 *
 * <<afternoon>>
 *   It is time for tea!
 * <</afternoon>>
 *
 * The condition tag optionally takes more parameters:
 *
 * <<afternoon and not $minute is 0>>
 *   v- those are the same -^
 * <<afternoon not $minute is 0>>
 *
 * <<afternoon not>> would invert the condition.
 *
 * The condition can also be used in other code, there the prefix "C." is needed:
 *
 * <<if C.afternoon>>more tea<</if>>
 *
 *
 * <<compute>> works nearly the same, but it doesn't create a macro, only the
 * "C." value. It can also be used for non-condition definitions, e.g.:
 *
 * <<compute "time">>
 *   ($hour / 24 * 10) + ":" + ($minute / 60 * 100)
 * <</time>>
 *
 * For hardcore recolutionists it is <<= C.time >> o'decimal'clock.
 *
 */

Macro.add(['condition', 'compute'], {
	tags : null,
	handler() {
		if (this.args.length === 0) {
			return this.error('no condition name specified');
		} if (this.args.length > 1) {
			return this.error('too many parameters specified');
		}

		const widgetName = this.args[0];

		if (window.C.hasOwnProperty(widgetName)) {
			return this.error(`cannot clobber existing definition "${widgetName}"`);
		}

		Object.defineProperty(window.C, widgetName, {
			get: Function('return (' + Scripting.parse(this.payload[0].contents) + ')')
		});

		if (this.payload[0].name === 'condition') {
			if (Macro.has(widgetName)) {
				return this.error(`cannot clobber existing macro "${widgetName}"`);
			}

			Macro.add(widgetName, {
				isWidget : true,
				skipArgs : true,
				tags     : null,
				condend  : /[|&!<=>]\s*$/,
				condstart: /^\s*(\|\||&&)(.+)$/i,
				handler  : (function (widgetName) {
					return function () {
						try {
							let result;
							if (this.args.full.length > 0) {
								let prefix = this.args.full;
								let match = this.self.condstart.exec(prefix);
								if (match) {
									prefix = match[2] + match[1];
								} else if (!this.self.condend.test(prefix)) {
									prefix += '&&';
								}
								result = Scripting.evalJavaScript(prefix + 'C.' + widgetName);
							} else {
								result = C[widgetName];
							}
							if (!!result) {
								new Wikifier(this.output, this.payload[0].contents);
							}
						} catch (ex) {
							return this.error(`bad conditional expression in <<condition "${widgetName}">>: ${typeof ex === 'object' ? ex.message : ex}`);
						}
					}
				})(widgetName)
			});
		}
	}
});


code:
function eleprop(e, k, v) {
	if (k === '$oncreate') {
		v.call(e, e);
	} else if (k.indexOf('on') === 0) {
		e.addEventListener(k.slice(2), v.bind(e));
	} else if (v !== undefined) {
		if (k in e) {
			e[k] = v;
		} else {
			e.setAttribute(k, v);
		}
	}
	return e;
}

function eleprops(e, props) {
	if (!props) return e;
	for (let kv of Object.entries(props)) {
		eleprop(e, kv[0], kv[1]);
	}
	return e;
}

function elechild(e, c) {
	if (typeof c === 'string') {
		if (c) {
			e.appendChild(document.createTextNode(c));
		}
	} else if (Array.isArray(c)) {
		elechildren(e, c);
	} else {
		e.appendChild(c);
	}
	return e;
}

function elechildren(e, children) {
	if (arguments.length > 2) {
		elechildren(e, Array.from(arguments).slice(1));
	} else if (typeof children === 'string' || children instanceof Node) {
		elechild(e, children);
	} else if (children) {
		for (let c of children) {
			elechild(e, c);
		}
	}
	return e;
}

/**
 * Create HTML Element with properties, attributes, events, and children.
 * Similar to Vue h() function
 *
 * @param {string} tag Element tag name
 * @param [props] Object with properties, attributes, and events to set
 * @param {(string|Node|Array)} [children] Child elements and texts to add, either array or a single child
 * @return {HTMLElement} Created element
 * @example
 * element("span", {
 *     class: "red",
 *     onhover: function() { this.className = ""; }
 * }, [
 *  "Click the ",
 *  element("a", { href:"/link" }, "Link")
 * ])
 */
function element(tag, props, children) {
	if (children === undefined && (typeof props === 'string' || Array.isArray(props) || props instanceof Node)) {
		children = props;
		props = null;
	}
	let e = document.createElement(tag);
	elechildren(e, children);
	eleprops(e, props);
	return e;
}

function elecustomprops(e, props, customProps) {
	for (let kv of Object.entries(props)) {
		let k = kv[0], v = kv[1];
		if (k in customProps) {
			customProps[k](e, v)
		} else {
			eleprop(e, k, v)
		}
	}
}

function customElement(tag, baseProps, props, children, customProps) {
	let e = element(tag, baseProps, children);
	elecustomprops(e, props, customProps);
	return e;
}

/**
 * Extra props:
 * - set(newValue:(string|number)) - input listener
 */
function eInput(props) {
	return customElement("input", {type: "text"}, props, null, {
		set(e, set) {
			set = set.bind(e);
			e.addEventListener('input', () => {
				let value = e.value;
				if (e.type === 'number' || e.type === 'range') {
					value = parseFloat(value);
					if (!isFinite(value)) return;
				}
				set(value);
			})
		}
	})
}

/**
 * Extra props:
 * - value:boolean - same as 'checked'
 * - set(newValue:boolean) - change listener
 */
function eCheckbox(props) {
	let checkbox = customElement("input", {type: "checkbox"}, props, null, {
		label() {
		},
		value(e, value) {
			e.checked = !!value;
		},
		set(e, set) {
			set = set.bind(e);
			e.addEventListener("change", () => {
				set(e.checked);
			})
		}
	});
	if (props && 'label' in props) {
		return element('label', [
			props.label,
			checkbox
		])
	}
	return checkbox
}

/**
 * Extra props:
 * - items:({value:string, text:string}] | string)[] - options
 * - value:string - selected item value
 * - set(newValue:string) - change listener
 */
function eSelect(props) {
	return customElement("select", null, props, null,
		{
			items(e, items) {
				for (let item of items) {
					if (typeof item === 'string') item = {value: item, text: item};
					e.appendChild(element("option", {
						value: item.value,
						selected: item.value == e.value
					}, item.text));
				}
			},
			set(e, set) {
				set = set.bind(e);
				e.addEventListener("change", () => set(e.value));
			}
		})
}


code:
/*
 * Usage:
 *
 * <<doubleslider VAR1 VAR2 VAR3 LEFTCOLOR MIDDLECOLOR RIGHTCOLOR>>
 *
 * The vars should be the variable name WITHOUT the "$", while the colors can be any css-compatible color string, like "#00FF00" or "rgb(0,255,0)".
 *
 * Additional macros, or additional arguments, can be implemented to allow for different skins or different presentation styles.
 *
 * The current "doubleslider" macro creates a range from 0 to 100, with two sliders and three labels. The player moves the sliders to set the values of three variables,
 * which will add up to a total of 100%. The three variables are visually represented as the spaces between the sliders.
 */
Macro.add("doubleslider", {
	handler: function () {
		let [var1, var2, var3, leftcolor, middlecolor, rightcolor] = this.args;
		if (this.args.length % 3 != 0) throw new Error("Bad doubleslider args "+JSON.stringify(this.args));
		if (this.args.length === 3){ //defaults
			leftcolor = "#fa0000";
			middlecolor = "#ab00fa";
			rightcolor = "#006cfa";
		}
		function updateValues(data, where){
			// console.log("updateValues from " + where);

			var bar2 = data.slider.find('.irs-bar2');
			if (bar2.length === 0) bar2 = $('<span>').addClass('irs-bar2').appendTo(data.slider);
			bar2.css('width',''+(data.from/(data.max-data.min))*100+'%');

			var middleLabel = data.slider.find('.irs-single');
			var rightLabel = data.slider.find('.irs-to');
			var leftLabel = data.slider.find('.irs-from');

			//label visibility mod
			leftLabel.css('visibility', "visible");
			if (data.from <= 0) leftLabel.css('visibility', "hidden");

			middleLabel.css('visibility', "visible");
			if (data.to - data.from <= 0) middleLabel.css('visibility', "hidden");

			rightLabel.css('visibility', "visible");
			if (100 - data.to <= 0) rightLabel.css('visibility', "hidden");

			//set the label content
			middleLabel.html(V[var2]+"%");
			rightLabel.html(V[var3]+"%");

			//set the label positions to be centered on the bars
			let a = (data.max-data.min);
			var midbar = data.slider.find('.irs-bar');

			leftLabel.css("left", "calc(" + (data.from/a*50) + "% - " + (leftLabel.width()/2) + "px)");
			middleLabel.css("left", "calc(" + ((data.from/a*100) + (midbar.width()/8)) + "% - " + (middleLabel.width()/2) + "px)");
			rightLabel.css("left", "calc(" + (data.to + (100-data.to)/2) + "% - " + (rightLabel.width()/2) + "px)");
		}

		let input = $("<input>") /* create new element and wrap in jQuery */
			.appendTo(this.output) /* 'output' is document fragment to be appended to current passage */
			.ionRangeSlider({
				type: "double",
				skin: "roundprobability",
				drag_interval: true,
				hide_min_max: true,
				postfix: "%",
				min: 0, max: 100,
				from: V[var1], to: (100-V[var3]),
				onStart(data){
					//let uniqueId = data.slider[0].classList[2];	//ensures that multiple sliders don't interfere with each other.

					updateValues(data, "onStart");

					data.slider.find('.irs-handle.from').css('--color', leftcolor);
					data.slider.find('.irs-from').css('--color', leftcolor);
					data.slider.find(".irs-bar2").css('--color', leftcolor);

					data.slider.find('.irs-handle.single').css('--color', middlecolor);
					data.slider.find('.irs-single').css('--color', middlecolor);
					data.slider.find(".irs-bar").css('--color', middlecolor);

					data.slider.find('.irs-handle.to').css('--color', rightcolor);
					data.slider.find('.irs-to').css('--color', rightcolor);
					data.slider.find(".irs-line").css('--color', rightcolor);
				},
				onChange(data) {
					//set the variables
					V[var1] = data.from;
					V[var2] = data.to - data.from;
					V[var3] = 100 - data.to;
					//update the page to tell the user what the variables are
					jQuery('#numberslider-value-'+var1).text(V[var1]);
					jQuery('#numberslider-value-'+var2).text(V[var2]);
					jQuery('#numberslider-value-'+var3).text(V[var3]);

					// updateValues(data, "onChange");
				}
			})
		let slider = input.data('ionRangeSlider');
		let oldDrawHandles = slider.drawHandles;
		slider.drawHandles = function() {
			oldDrawHandles.call(this);
			updateValues(this.result, "drawHandles");
		}
		slider.callOnChange();
	}
});


code:
/*
 * Usage:
 *
 * 1. Registering events as widgets
 * <<widget EVENT_NAME>>
 *   (Event content)
 * <</widget>>
 *
 * 2. Calling events
 *
 * <<cleareventpool>>
 *
 * <<addevent WIDGET_NAME [WEIGHT]>>
 * <<addevent WIDGET_NAME [WEIGHT]>>
 * <<addinlineevent [NAME_FOR_DEBUGGING [WEIGHT]]>>
 *   Content for an event you don't want to define as a widget
 * <</addinlineevent>>
 *
 * <<runeventpool>> - will pick a weighted random event and execute its content.
 *
 * Weight is a relative probability, so event with weight=2 would appear twice as often as event with weight=1.
 * Default weight is 1.
 */

/**
 * Return a weighted random item, that is, probability of each item is proportional to their weight,
 * probability P(event_i) = weight_i / sum_of_weights
 *
 * items - array of objects with optional 'weight' key - a number or a no-arg function returning number.
 * Default 1.
 * Weight of Infinity means "return this item, ignore others"
 */
 function rollWeightedRandomFromArray(items) {
	if (!Array.isArray(items)) throw new Error("Not an array: " + items);
	// convert items to array of {weight:number, item:original_item} and filter out bad elements
	items = items.map(function (el) {
		if (!el || typeof el !== 'object') return null;
		var w = el.weight;
		if (typeof w === 'function') w = w();
		if (typeof w === 'string') w = parseFloat(w);
		if (typeof w !== 'number') w = 1;
		return { weight: w, item: el };
	}).filter(function (el) {
		if (!el || !(el.weight > 0)) {
			if (StartConfig.debug) {
				console.debug("Filtered out ",el);
			}
			return false;
		}
		return true;
	});
	if (StartConfig.debug) {
		console.debug("Picking from random pool",items);
	}
	if (items.length == 0) return null; // Or could throw an exception (no items with positive weight)
	var sum = 0;
	for (var i = 0; i < items.length; i++) {
		if (!isFinite(items[i].weight)) {
			if (StartConfig.debug) {
				console.debug("Returning infinite-weighted",items[i].item);
			}
			return items[i].item;
		}
		sum += items[i].weight;
	}
	var roll = randomFloat(sum), roll0 = roll;
	for (i = 0; i < items.length; i++) {
		roll -= items[i].weight;
		if (roll <= 0) {
			if (StartConfig.debug) {
				console.debug("Roll = ",roll0,"sum = ",sum,"returning ",items[i].item);
			}
			return items[i].item;
		}
	}
	// Should never happen
	console.warn('Weighted random math went wrong', roll0, sum, items);
	return items[0].item;
}
window.rollWeightedRandomFromArray = rollWeightedRandomFromArray;

Macro.add("cleareventpool", {
	skipArgs: true,
	handler: function () {
		T.eventpool = [];
	}
});

Macro.add("addinlineevent", {
	tags: null,
	handler: function () {
		T.eventpool.push({
			name: this.args[0] || "",
			weight: this.args.length == 2 ? +this.args[1] : 1.0,
			content: this.payload[0].contents
		});
	}
});

Macro.add("addevent", {
	handler: function () {
		var widget = this.args[0];
		if (typeof widget !== 'string' || !widget || this.args.length > 2) throw new Error("Bad addevent args "+JSON.stringify(this.args));
		T.eventpool.push({
			name: widget,
			content: "<<"+widget+">>",
			weight: this.args.length == 2 ? +this.args[1] : 1.0
		});
	}
});

Macro.add("runeventpool", {
	skipArgs: true,
	handler: function () {
		let pick = T.eventpool.find(e => e.name === V.eventPoolOverride);
		if (pick) {
			delete V.eventPoolOverride;
		} else if (T.eventpool.includes(V.eventPoolOverride)) {
			pick = V.eventPoolOverride;
			delete V.eventPoolOverride;
		} else {
			pick = rollWeightedRandomFromArray(T.eventpool);
		}
		if (!pick) throw new Error("Event pool is empty");
		// Jimmy: For tracking where in the code you may be.
		// E.G: ['eventAmbient', >>'autumn_anystreet_2'<<, 'generate1']
		DOL.Stack.push(pick.name);
		jQuery(this.output).wiki(pick.content);
		DOL.Stack.pop();
	}
});