From f88fecd6fd60fc7bcadc36a88f727a9ce6dfacef Mon Sep 17 00:00:00 2001
From: Kirsty <kirsty.degreesoflewdity@gmail.com>
Date: Sun, 12 Jan 2025 12:06:24 -0500
Subject: [PATCH] Combat hair gradients.

---
 .../05-renderer/18-combat-renderer.js         |  86 +++++++++++---
 .../05-renderer/21-npc-options.js             |   6 +
 .../05-renderer/21-player-options.js          |  14 +--
 game/04-Variables/colours.js                  | 110 +++++++++++++++++-
 types/npc.d.ts                                |   2 +-
 5 files changed, 195 insertions(+), 23 deletions(-)

diff --git a/game/03-JavaScript/05-renderer/18-combat-renderer.js b/game/03-JavaScript/05-renderer/18-combat-renderer.js
index 8f59b6b9bb..b5f153275a 100644
--- a/game/03-JavaScript/05-renderer/18-combat-renderer.js
+++ b/game/03-JavaScript/05-renderer/18-combat-renderer.js
@@ -384,7 +384,7 @@ class CombatRenderer {
 	 * @returns {Partial<CompositeLayerSpec>}
 	 */
 	static createHairColourGradient(hairPart, gradient, hairType, hairLength, prefilterName) {
-		const combatHair = CombatRenderer.getHairGradientType(hairType);
+		const combatHair = CombatRenderer.getHairGradientType(hairType, gradient);
 		const filterPrototypeLibrary = setup.colours.hairgradients_prototypes[hairPart][gradient.style];
 		const filterPrototype = filterPrototypeLibrary[combatHair] || filterPrototypeLibrary.all;
 		/** @type {Partial<CompositeLayerSpec>} */
@@ -656,14 +656,61 @@ class CombatRenderer {
 			return CombatRenderer.lookupColour(setup.colours.hair_map, V.haircolour, "hair", "hair_custom", "hair");
 		}
 		if (["wide flaps", "hime", "curtain", "mohawk"].includes(V.fringetype)) {
+			return this.getFringeFilter();
+		}
+		if (V.hairColourGradient.style === "split") {
+			const index = V.position === "missionary" ? 0 : 1;
+			return CombatRenderer.lookupColour(setup.colours.hair_map, V.hairColourGradient.colours[index], "hair", "hair_custom", "hair");
+		}
+		return CombatRenderer.createHairColourGradient(
+			"sides",
+			V.hairColourGradient,
+			CombatRenderer.getHairSideType(),
+			hairLengthStringToNumber(V.hairlengthstage),
+			"hair"
+		);
+	}
+
+	/** @returns {string} */
+	static getHairLength() {
+		if (["wide flaps", "hime", "curtain", "mohawk"].includes(V.fringetype)) {
+			return V.fringelengthstage;
+		}
+		return V.hairlengthstage;
+	}
+
+	/**
+	 * @param {TransformationParts} part
+	 * @returns {Partial<CompositeLayerSpec>}
+	 */
+	static getPartFilter(part) {
+		if (V.hairColourGradient.style === "split") {
+			if (["tail", "pubes"].includes(part) || (["wings", "ears"].includes(part) && CombatRenderer.getPosition(V.position) === "missionary")) {
+				return CombatRenderer.lookupColour(setup.colours.hair_map, V.hairColourGradient.colours[0], "hair", "hair_custom", "hair");
+			}
+			return CombatRenderer.lookupColour(setup.colours.hair_map, V.hairColourGradient.colours[1], "hair", "hair_custom", "hair");
+		}
+		if (V.hairColourGradient.style === "high-ombre") {
+			if (["tail", "pubes", "wings"].includes(part)) {
+				return CombatRenderer.lookupColour(setup.colours.hair_map, V.hairColourGradient.colours[0], "hair", "hair_custom", "hair");
+			}
+			return CombatRenderer.lookupColour(setup.colours.hair_map, V.hairColourGradient.colours[1], "hair", "hair_custom", "hair");
+		}
+		if (V.hairColourGradient.style === "low-ombre") {
+			if (["tail"].includes(part)) {
+				return CombatRenderer.lookupColour(setup.colours.hair_map, V.hairColourGradient.colours[0], "hair", "hair_custom", "hair");
+			}
 			return CombatRenderer.createHairColourGradient(
-				"fringe",
-				V.hairFringeColourGradient,
-				CombatRenderer.getHairFringeType(),
-				hairLengthStringToNumber(V.fringelengthstage),
+				"sides",
+				V.hairColourGradient,
+				CombatRenderer.getHairSideType(),
+				hairLengthStringToNumber(V.hairlengthstage),
 				"hair"
 			);
 		}
+		if (V.hairColourGradient.style === "face-frame") {
+			return CombatRenderer.lookupColour(setup.colours.hair_map, V.hairColourGradient.colours[1], "hair", "hair_custom", "hair");
+		}
 		return CombatRenderer.createHairColourGradient(
 			"sides",
 			V.hairColourGradient,
@@ -680,19 +727,26 @@ class CombatRenderer {
 		if (V.hairFringeColourStyle === "simple") {
 			return CombatRenderer.lookupColour(setup.colours.hair_map, V.hairfringecolour || V.haircolour, "hair_fringe", "hair_fringe_custom", "hair_fringe");
 		}
+		if (V.hairFringeColourGradient.style === "split" && this.getFringeType() !== "mohawk") {
+			const index = V.position === "missionary" ? 0 : 1;
+			return CombatRenderer.lookupColour(
+				setup.colours.hair_map,
+				V.hairFringeColourGradient.colours[index],
+				"hair_fringe",
+				"hair_fringe_custom",
+				"hair_fringe"
+			);
+		}
 		return CombatRenderer.createHairColourGradient(
 			"fringe",
 			V.hairFringeColourGradient || V.hairColourGradient,
 			CombatRenderer.getHairFringeType(),
 			hairLengthStringToNumber(V.fringelengthstage),
-			"fringe"
+			"hair"
 		);
 	}
 
 	static getFringeType() {
-		if (V.hairtype === "short") {
-			return "short";
-		}
 		if (V.fringetype === "wide flaps") {
 			return "wide-flaps";
 		}
@@ -711,17 +765,21 @@ class CombatRenderer {
 		if (V.hairtype === "layered bob") {
 			return "layered-bob";
 		}
+		if (["shaved", "short"].includes(V.hairtype) || (V.hairtype === "default" && V.hairlengthstage === "short")) {
+			return "short";
+		}
 		return "default";
 	}
 
 	/**
 	 * @param {string} hairType
+	 * @param {Gradient} gradient
 	 */
-	static getHairGradientType(hairType) {
+	static getHairGradientType(hairType, gradient) {
 		if (V.fringetype === "mohawk") {
 			return V.position === "missionary" ? "combatMohawk" : "combatMohawkDoggy";
 		}
-		return hairType;
+		return V.position === "missionary" ? "combatMissionary" : "combatDoggy";
 	}
 
 	/**
@@ -735,13 +793,13 @@ class CombatRenderer {
 			colour: { h: 0, s: 100, l: 30 },
 		};
 		if (transformation === "bird" && ["tail", "wings", "malar", "plumage", "pubes"].includes(part)) {
-			return CombatRenderer.getHairFilter();
+			return CombatRenderer.getPartFilter(part);
 		}
 		if (["cat", "wolf"].includes(transformation) && ["ears", "tail", "pubes", "pits"].includes(part)) {
-			return CombatRenderer.getHairFilter();
+			return CombatRenderer.getPartFilter(part);
 		}
 		if (transformation === "fox" && ["ears", "tail", "cheeks", "pubes"].includes(part)) {
-			return CombatRenderer.getHairFilter();
+			return CombatRenderer.getPartFilter(part);
 		}
 		// No filter possible as part(s) cannot be recoloured
 		if (
diff --git a/game/03-JavaScript/05-renderer/21-npc-options.js b/game/03-JavaScript/05-renderer/21-npc-options.js
index bd7c6763eb..656a2d1582 100644
--- a/game/03-JavaScript/05-renderer/21-npc-options.js
+++ b/game/03-JavaScript/05-renderer/21-npc-options.js
@@ -670,6 +670,12 @@ class NpcCombatMapper {
 						blendMode: "multiply",
 						desaturate: true,
 					};
+				case "dark red":
+					return {
+						blend: "#b50202",
+						blendMode: "multiply",
+						desaturate: true,
+					};
 			}
 		}
 		return NpcCombatMapper.getNpcSkinFilter(npc);
diff --git a/game/03-JavaScript/05-renderer/21-player-options.js b/game/03-JavaScript/05-renderer/21-player-options.js
index 0ca2b24601..70879732fc 100644
--- a/game/03-JavaScript/05-renderer/21-player-options.js
+++ b/game/03-JavaScript/05-renderer/21-player-options.js
@@ -1795,14 +1795,14 @@ class PlayerCombatMapper {
 		if (clothing.combat?.mainColour && !["primary", "secondary"].includes(clothing.combat?.mainColour)) {
 			options.filters[mainFilterKey] = PlayerCombatMapper.genFilterWithHex(clothing.combat.mainColour);
 		} else if (clothing.combat?.mainColour && clothing.combat?.mainColour === "secondary") {
-			const accColour = clothing.combat?.accColour || clothing.accessory_colour;
+			const accColour = clothing.accessory_colour === "original" ? 0 : clothing.combat?.accColour || clothing.accessory_colour;
 			const accDebugName = slot + " accessory";
 			const accCustomFilter = clothing.accessory_colourCustom;
 			options.filters[mainFilterKey] = accColour
 				? CombatRenderer.lookupColour(setup.colours.clothes_map, accColour, accDebugName, accCustomFilter, clothing.prefilter)
 				: Renderer.emptyLayerFilter();
 		} else {
-			const colour = clothing.colour;
+			const colour = clothing.colour === "original" ? 0 : clothing.colour;
 			const debugName = slot + " clothing";
 			const customFilter = clothing.colourCustom;
 			options.filters[mainFilterKey] = colour
@@ -1813,14 +1813,14 @@ class PlayerCombatMapper {
 		if (clothing.combat?.accColour && !["primary", "secondary"].includes(clothing.combat?.accColour)) {
 			options.filters[accFilterKey] = PlayerCombatMapper.genFilterWithHex(clothing.combat.accColour);
 		} else if (clothing.combat?.accColour && clothing.combat?.accColour === "primary") {
-			const colour = clothing.colour;
+			const colour = clothing.colour === "original" ? 0 : clothing.colour;
 			const debugName = slot + " clothing";
 			const customFilter = clothing.colourCustom;
 			options.filters[accFilterKey] = colour
 				? CombatRenderer.lookupColour(setup.colours.clothes_map, colour, debugName, customFilter, clothing.prefilter)
 				: Renderer.emptyLayerFilter();
 		} else {
-			const accColour = clothing.combat?.accColour || clothing.accessory_colour;
+			const accColour = clothing.accessory_colour === "original" ? 0 : clothing.combat?.accColour || clothing.accessory_colour;
 			const accDebugName = slot + " accessory";
 			const accCustomFilter = clothing.accessory_colourCustom;
 			options.filters[accFilterKey] = accColour
@@ -1833,7 +1833,7 @@ class PlayerCombatMapper {
 		} else if (clothing.combat?.mainColour && !clothing.combat?.sleeveColour) {
 			options.filters[sleeveFilterKey] = PlayerCombatMapper.genFilterWithHex(clothing.combat.mainColour);
 		} else {
-			const colour = clothing.colour;
+			const colour = clothing.colour === "original" ? 0 : clothing.colour;
 			const debugName = slot + " clothing";
 			const customFilter = clothing.colourCustom;
 			options.filters[sleeveFilterKey] = colour
@@ -1846,7 +1846,7 @@ class PlayerCombatMapper {
 		} else if (clothing.combat?.accColour && !clothing.combat?.sleeveAccColour) {
 			options.filters[sleeveAccFilterKey] = PlayerCombatMapper.genFilterWithHex(clothing.combat.accColour);
 		} else {
-			const accColour = clothing.combat?.accColour || clothing.accessory_colour;
+			const accColour = clothing.accessory_colour === "original" ? 0 : clothing.combat?.accColour || clothing.accessory_colour;
 			const accDebugName = slot + " accessory";
 			const accCustomFilter = clothing.accessory_colourCustom;
 			options.filters[sleeveAccFilterKey] = accColour
@@ -2521,7 +2521,7 @@ class PlayerCombatMapper {
 	static generateHairFilters(options) {
 		options.filters.hair = CombatRenderer.getHairFilter();
 		options.filters.fringe = CombatRenderer.getFringeFilter();
-		options.hairLength = V.hairlengthstage;
+		options.hairLength = CombatRenderer.getHairLength();
 		options.hairType = CombatRenderer.getFringeType();
 	}
 }
diff --git a/game/04-Variables/colours.js b/game/04-Variables/colours.js
index 8a1552fffb..b619a60ba3 100644
--- a/game/04-Variables/colours.js
+++ b/game/04-Variables/colours.js
@@ -177,7 +177,7 @@ setup.colours = {
 			blend: setup.colours.getSkinRgb(options, tan / 100),
 			blendMode: options.blendMode,
 			desaturate: options.desaturate,
-			...options.alpha && { alpha: options.alpha },
+			...(options.alpha && { alpha: options.alpha }),
 		};
 	},
 	getSkinRgb(type, tan) {
@@ -640,6 +640,24 @@ setup.colours.hairgradients_prototypes = {
 					[0.85, "rgba(0, 0, 0, 1)"],
 				],
 			},
+			combatDoggy: {
+				gradient: "linear",
+				values: [250, 440, 250, 0],
+				lengthFunctions: [(length, value) => value, (length, value) => value],
+				colors: [
+					[0.76, "rgba(0, 0, 0, 1)"],
+					[0.85, "rgba(0, 0, 0, 1)"],
+				],
+			},
+			combatMissionary: {
+				gradient: "linear",
+				values: [180, 245, 0, 250],
+				lengthFunctions: [(length, value) => value, (length, value) => value],
+				colors: [
+					[0.64, "rgba(0, 0, 0, 1)"],
+					[0.85, "rgba(0, 0, 0, 1)"],
+				],
+			},
 		},
 		"low-ombre": {
 			all: {
@@ -651,6 +669,24 @@ setup.colours.hairgradients_prototypes = {
 					[0.85, "rgba(0, 0, 0, 1)"],
 				],
 			},
+			combatDoggy: {
+				gradient: "linear",
+				values: [340, 180, 300, 0],
+				lengthFunctions: [(length, value) => value - length / 1000 / 2, (length, value) => value - length / 1000 / 2],
+				colors: [
+					[0.6, "rgba(0, 0, 0, 1)"],
+					[0.85, "rgba(0, 0, 0, 1)"],
+				],
+			},
+			combatMissionary: {
+				gradient: "linear",
+				values: [180, 350, 0, 350],
+				lengthFunctions: [(length, value) => value - length / 1000 / 2, (length, value) => value - length / 1000 / 2],
+				colors: [
+					[0.6, "rgba(0, 0, 0, 1)"],
+					[0.85, "rgba(0, 0, 0, 1)"],
+				],
+			},
 		},
 		split: {
 			parted: {
@@ -718,6 +754,24 @@ setup.colours.hairgradients_prototypes = {
 					[0.175, "rgba(0, 0, 0, 1)"],
 				],
 			},
+			combatDoggy: {
+				gradient: "radial",
+				values: [15, 183, 50, 150, 103, 350],
+				lengthFunctions: [(length, value) => value, (length, value) => value],
+				colors: [
+					[0.15, "rgba(0, 0, 0, 1)"],
+					[0.175, "rgba(0, 0, 0, 1)"],
+				],
+			},
+			combatMissionary: {
+				gradient: "radial",
+				values: [125, 103, 50, 150, 103, 350],
+				lengthFunctions: [(length, value) => value, (length, value) => value],
+				colors: [
+					[0.15, "rgba(0, 0, 0, 1)"],
+					[0.175, "rgba(0, 0, 0, 1)"],
+				],
+			},
 		},
 	},
 	sides: {
@@ -731,6 +785,24 @@ setup.colours.hairgradients_prototypes = {
 					[0.85, "rgba(0, 0, 0, 1)"],
 				],
 			},
+			combatDoggy: {
+				gradient: "linear",
+				values: [250, 440, 250, 0],
+				lengthFunctions: [(length, value) => value, (length, value) => value],
+				colors: [
+					[0.76, "rgba(0, 0, 0, 1)"],
+					[0.85, "rgba(0, 0, 0, 1)"],
+				],
+			},
+			combatMissionary: {
+				gradient: "linear",
+				values: [180, 245, 0, 250],
+				lengthFunctions: [(length, value) => value, (length, value) => value],
+				colors: [
+					[0.64, "rgba(0, 0, 0, 1)"],
+					[0.85, "rgba(0, 0, 0, 1)"],
+				],
+			},
 		},
 		"low-ombre": {
 			all: {
@@ -742,6 +814,24 @@ setup.colours.hairgradients_prototypes = {
 					[0.85, "rgba(0, 0, 0, 1)"],
 				],
 			},
+			combatDoggy: {
+				gradient: "linear",
+				values: [340, 180, 300, 0],
+				lengthFunctions: [(length, value) => value - length / 1000 / 2, (length, value) => value - length / 1000 / 2],
+				colors: [
+					[0.6, "rgba(0, 0, 0, 1)"],
+					[0.85, "rgba(0, 0, 0, 1)"],
+				],
+			},
+			combatMissionary: {
+				gradient: "linear",
+				values: [180, 350, 0, 350],
+				lengthFunctions: [(length, value) => value - length / 1000 / 2, (length, value) => value - length / 1000 / 2],
+				colors: [
+					[0.6, "rgba(0, 0, 0, 1)"],
+					[0.85, "rgba(0, 0, 0, 1)"],
+				],
+			},
 		},
 		split: {
 			all: {
@@ -764,6 +854,24 @@ setup.colours.hairgradients_prototypes = {
 					[0.0, "rgba(0, 0, 0, 1)"],
 				],
 			},
+			combatDoggy: {
+				gradient: "radial",
+				values: [15, 183, 50, 150, 103, 350],
+				lengthFunctions: [(length, value) => value, (length, value) => value],
+				colors: [
+					[0.15, "rgba(0, 0, 0, 1)"],
+					[0.175, "rgba(0, 0, 0, 1)"],
+				],
+			},
+			combatMissionary: {
+				gradient: "radial",
+				values: [125, 103, 50, 150, 103, 350],
+				lengthFunctions: [(length, value) => value, (length, value) => value],
+				colors: [
+					[0.15, "rgba(0, 0, 0, 1)"],
+					[0.175, "rgba(0, 0, 0, 1)"],
+				],
+			},
 		},
 	},
 };
diff --git a/types/npc.d.ts b/types/npc.d.ts
index 7d4c8a67e3..76be4381d1 100644
--- a/types/npc.d.ts
+++ b/types/npc.d.ts
@@ -150,7 +150,7 @@ declare global {
 
 		strapon?: {
 			state: "worn";
-			color: "black" | "red" | "pink" | "purple" | "fleshy" | "blue" | "green";
+			color: "black" | "red" | "pink" | "purple" | "fleshy" | "blue" | "green" | "dark red";
 			description: string;
 			size: number;
 		};
-- 
GitLab