From ff1129ddae1cef85a7bd3dbfcf5db82c45937d42 Mon Sep 17 00:00:00 2001
From: xao321 <xao321@hotmail.com>
Date: Sat, 11 May 2024 16:27:15 +0200
Subject: [PATCH] Added reflections for water, with distortion animation
 (currently for sea, docks, beach, lake) Added setting in options to disable
 reflections Changed where sun/moon enters/leaves canvas, slightly Reverted
 setting to disable location-image animations

---
 game/03-JavaScript/debug-menu.js              |  18 +-
 .../weather/00-docs/weather.g.ts              |  17 +-
 .../weather/01-setup/canvas-settings.js       |  12 +-
 .../weather/01-setup/location-images.js       | 178 ++++++-----
 .../weather/01-setup/weather-bindings.js      |   4 -
 .../03-canvas/01-src/00-main/00-sky-canvas.js |   5 +
 .../03-canvas/01-src/01-effect-manager.js     |   2 +-
 .../03-canvas/01-src/03-observables.js        |   1 +
 .../02-lib/00-effects/effects-location.js     | 284 ++++++++++++++++--
 .../02-lib/01-layers/layer-location.js        |  84 +++---
 game/04-Variables/variables-start.twee        |   1 +
 .../04-Variables/variables-versionUpdate.twee |   3 -
 game/base-system/overlays/options.twee        |  28 +-
 img/misc/locations/docks/reflective.png       | Bin 218 -> 195 bytes
 img/misc/locations/docks/water.png            | Bin 389 -> 401 bytes
 img/misc/locations/lake/water.png             | Bin 334 -> 309 bytes
 img/misc/locations/sea/base.png               | Bin 526 -> 0 bytes
 img/misc/locations/sea/reflective.png         | Bin 247 -> 120 bytes
 img/misc/locations/sea/water.png              | Bin 0 -> 242 bytes
 19 files changed, 451 insertions(+), 186 deletions(-)
 delete mode 100644 img/misc/locations/sea/base.png
 create mode 100644 img/misc/locations/sea/water.png

diff --git a/game/03-JavaScript/debug-menu.js b/game/03-JavaScript/debug-menu.js
index a0e246ddcd..48715a6fff 100644
--- a/game/03-JavaScript/debug-menu.js
+++ b/game/03-JavaScript/debug-menu.js
@@ -12,18 +12,8 @@ setup.debugMenu = {
 setup.debugMenu.eventList = {
 	Main: [
 		{
-			link: [`Freeze stats`, stayOnPassageFn],
-			widgets: [`<<set $statFreeze to true>>`],
-			condition() {
-				return !V.statFreeze;
-			},
-		},
-		{
-			link: [`Unfreeze stats`, stayOnPassageFn],
-			widgets: [`<<set $statFreeze to false>>`],
-			condition() {
-				return V.statFreeze;
-			},
+			link: [`Home`, `Bedroom`],
+			widgets: [`<<endcombat>>`],
 		},
 		{
 			link: [`Pass 1 minute`, stayOnPassageFn],
@@ -65,10 +55,6 @@ setup.debugMenu.eventList = {
 			link: [`Pass 24 hours`, stayOnPassageFn],
 			widgets: [`<<pass 24 hours>>`],
 		},
-		{
-			link: [`Home`, `Bedroom`],
-			widgets: [`<<endcombat>>`],
-		},
 		{
 			link: [`Wardrobe`, `Wardrobe`],
 			widgets: [``],
diff --git a/game/03-JavaScript/weather/00-docs/weather.g.ts b/game/03-JavaScript/weather/00-docs/weather.g.ts
index fc19a7d519..7eafcf826e 100644
--- a/game/03-JavaScript/weather/00-docs/weather.g.ts
+++ b/game/03-JavaScript/weather/00-docs/weather.g.ts
@@ -1,8 +1,9 @@
 interface ImageLocation {
     folder: string;
-    base: ImageSetting | { [key: string]: ImageSetting };
+    base?: ImageSetting | { [key: string]: ImageSetting };
     emissive?: EmissiveSetting | { [key: string]: EmissiveSetting };
-    reflective?: ReflectiveSetting };
+    reflective?: ReflectiveSetting;
+    layerTop?: ImageSetting | { [key: string]: ImageSetting };
 }
 
 interface AnimationSetting {
@@ -34,14 +35,20 @@ interface EmissiveSetting {
 }
 
 interface ReflectiveSetting {
+	/**
+     * The primary mask setting used to define the basic masking properties of a reflection.
+     * This should be the first property defined for clarity when setting up reflective properties.
+     */
     mask: MaskSetting;
-    [key: string]: ImageSetting;
+    [key: string]: ImageSetting | any;
 }
 
 interface MaskSetting {
     image: string;
-    alpha?: number;
-	horizon?: number;
+    alpha?: number | (() => number);
+	horizon?: number | (() => number);
+	waveShiftFactor?: number | (() => number);
+	animationCondition?: boolean | (() => boolean);
 }
 
 declare global {
diff --git a/game/03-JavaScript/weather/01-setup/canvas-settings.js b/game/03-JavaScript/weather/01-setup/canvas-settings.js
index b9d9f89c0a..eff6abc00f 100644
--- a/game/03-JavaScript/weather/01-setup/canvas-settings.js
+++ b/game/03-JavaScript/weather/01-setup/canvas-settings.js
@@ -23,8 +23,8 @@ setup.SkySettings = {
 				riseTime: 7,
 				setTime: 19,
 				path: {
-					startX: -14, // Start offscreen
-					endX: 64 + 14, // End offscreen
+					startX: -7,
+					endX: 64 + 7,
 					peakY: 70,
 					horizon: 162,
 				},
@@ -35,8 +35,8 @@ setup.SkySettings = {
 				riseTime: 19,
 				setTime: 7,
 				path: {
-					startX: -12, // Start offscreen
-					endX: 64 + 12, // End offscreen
+					startX: -6,
+					endX: 64 + 6,
 					peakY: 50, // The y value of the top of the orbit arc
 					horizon: 162, // The y value of the horizon
 				},
@@ -45,8 +45,8 @@ setup.SkySettings = {
 				riseTime: 19,
 				setTime: 7,
 				path: {
-					startX: -12, // Start offscreen
-					endX: 64 + 12, // End offscreen
+					startX: -6,
+					endX: 64 + 6,
 					peakY: 90,
 					horizon: 162,
 				},
diff --git a/game/03-JavaScript/weather/01-setup/location-images.js b/game/03-JavaScript/weather/01-setup/location-images.js
index e48ae870c6..38384868be 100644
--- a/game/03-JavaScript/weather/01-setup/location-images.js
+++ b/game/03-JavaScript/weather/01-setup/location-images.js
@@ -202,9 +202,17 @@ setup.LocationImages = {
 				condition: () => Weather.isSnow,
 				image: "snow.png",
 			},
-			water: {
+		},
+		reflective: {
+			mask: {
+				image: "reflective.png",
+				alpha: 0.7,
+			},
+			overlay: {
 				image: "water.png",
 			},
+		},
+		layerTop: {
 			tree: {
 				image: "tree.png",
 				animation: {
@@ -212,13 +220,7 @@ setup.LocationImages = {
 					cycleDelay: () => 2500,
 				},
 			},
-		},
-		reflective: {
-			image: "reflective.png",
-			backgroundOnly: true,
-			animation: "tree",
-			blur: 0,
-		},
+		}
 	},
 	bog: {
 		folder: "bog",
@@ -314,9 +316,9 @@ setup.LocationImages = {
 			color: "#deae66",
 			strength: 2,
 		},
-		reflective: {
-			image: "reflective.png",
-		},
+		// reflective: {
+		// 	image: "reflective.png",
+		// },
 	},
 	cafe_construction: {
 		folder: "cafe_construction",
@@ -336,9 +338,9 @@ setup.LocationImages = {
 			color: "#deae66",
 			strength: 2,
 		},
-		reflective: {
-			image: "reflective.png",
-		},
+		// reflective: {
+		// 	image: "reflective.png",
+		// },
 	},
 	cafe_renovated: {
 		folder: "cafe_renovated",
@@ -352,9 +354,9 @@ setup.LocationImages = {
 				image: "snow.png",
 			},
 		},
-		reflective: {
-			image: "reflective.png",
-		},
+		// reflective: {
+		// 	image: "reflective.png",
+		// },
 	},
 	canal: {
 		folder: "canal",
@@ -384,9 +386,9 @@ setup.LocationImages = {
 				},
 			},
 		},
-		reflective: {
-			image: "reflective.png",
-		},
+		// reflective: {
+		// 	image: "reflective.png",
+		// },
 	},
 	churchyard: {
 		folder: "churchyard",
@@ -495,9 +497,6 @@ setup.LocationImages = {
 				condition: () => Weather.isSnow,
 				image: "snow.png",
 			},
-			water: {
-				image: "water.png",
-			},
 			boat: {
 				// Not at same time as cruiser
 				waitForAnimation: "cruiser",
@@ -521,8 +520,14 @@ setup.LocationImages = {
 			},
 		},
 		reflective: {
-			image: "reflective.png",
-			alpha: 0.6,
+			mask: {
+				image: "reflective.png",
+				alpha: 0.2,
+				waveShiftFactor: 0.004, // default is 0.006
+			},
+			overlay: {
+				image: "water.png",
+			},
 		},
 	},
 	drain: {
@@ -547,10 +552,18 @@ setup.LocationImages = {
 				},
 			},
 		},
-		reflective: {
-			image: "reflective.png",
-			alpha: 0.6,
-		},
+		// reflective: {
+		// 	mask: {
+		// 		image: "reflective.png",
+		// 	},
+		// 	water: {
+		// 		image: "water.png",
+		// 		animation: {
+		// 			frameDelay: 1000,
+		// 			cycleDelay: () => 2000,
+		// 		},
+		// 	},
+		// },
 	},
 	estate: {
 		folder: "estate",
@@ -804,7 +817,7 @@ setup.LocationImages = {
 				image: "snow.png",
 			},
 			smoke: {
-				condition: () => !Time.bloodMoon,
+				condition: () => !Weather.bloodMoon,
 				image: "smoke.png",
 				animation: {
 					frameDelay: 200,
@@ -868,21 +881,19 @@ setup.LocationImages = {
 			default: {
 				image: "base.png",
 			},
-			water: {
-				image: "water.png",
-				animation: {
-					frameDelay: 500,
-					cycleDelay: () => 500,
-				},
-			},
-		},
-		reflective: {
-			image: "reflective.png",
-			animation: "water",
-			horizon: 122,
-			alpha: 0.25,
-			blur: 0.8,
 		},
+		// reflective: {
+		// 	mask: {
+		// 		image: "reflective.png",
+		// 	},
+		// 	overlay: {
+		// 		image: "water.png",
+		// 		animation: {
+		// 			frameDelay: 500,
+		// 			cycleDelay: () => 500,
+		// 		},
+		// 	},
+		// },
 	},
 	kylar_manor: {
 		folder: "kylar_manor",
@@ -915,18 +926,6 @@ setup.LocationImages = {
 				condition: () => Weather.isSnow,
 				image: "snow.png",
 			},
-			water: {
-				condition: () => !Weather.isFrozen("lake"),
-				image: "water.png",
-				animation: {
-					frameDelay: 1000,
-					cycleDelay: () => 0,
-				},
-			},
-			ice: {
-				condition: () => Weather.isFrozen("lake"),
-				image: "ice.png",
-			},
 			deer: {
 				condition: () => !Weather.isSnow && Time.dayState === "dawn",
 				image: "deer.png",
@@ -976,8 +975,25 @@ setup.LocationImages = {
 			},
 		},
 		reflective: {
-			image: "reflective.png",
-			horizon: 156,
+			mask: {
+				image: "reflective.png",
+				alpha: () => (!Weather.isFrozen("lake") ? 0.8 : 0.35),
+				horizon: 18,
+				waveShiftFactor: 0.009,
+				animationCondition: () => !Weather.isFrozen("lake"),
+			},
+			water: {
+				condition: () => !Weather.isFrozen("lake"),
+				image: "water.png",
+				animation: {
+					frameDelay: 1050,
+					cycleDelay: () => 0,
+				},
+			},
+			ice: {
+				condition: () => Weather.isFrozen("lake"),
+				image: "ice.png",
+			},
 		},
 	},
 	lake_ruin: {
@@ -1032,17 +1048,17 @@ setup.LocationImages = {
 				color: "#e63e3e",
 			},
 		},
-		reflective: {
-			default: {
-				condition: () => !Weather.bloodMoon,
-				image: "reflective.png",
-			},
-			bloodMoon: {
-				condition: () => Weather.bloodMoon,
-				image: "reflective_blood.png",
-			},
-			horizon: 112,
-		},
+		// reflective: {
+		// 	default: {
+		// 		condition: () => !Weather.bloodMoon,
+		// 		image: "reflective.png",
+		// 	},
+		// 	bloodMoon: {
+		// 		condition: () => Weather.bloodMoon,
+		// 		image: "reflective_blood.png",
+		// 	},
+		// 	horizon: 112,
+		// },
 	},
 	landfill: {
 		folder: "landfill",
@@ -1127,10 +1143,10 @@ setup.LocationImages = {
 				},
 			},
 		},
-		reflective: {
-			image: "reflective.png",
-			horizon: 112,
-		},
+		// reflective: {
+		// 	image: "reflective.png",
+		// 	horizon: 112,
+		// },
 	},
 	museum: {
 		folder: "museum",
@@ -1511,16 +1527,14 @@ setup.LocationImages = {
 	},
 	sea: {
 		folder: "sea",
-		base: {
-			image: "base.png",
-			animation: {
-				frameDelay: 300,
-				cycleDelay: () => 0,
-			},
-		},
 		reflective: {
-			image: "reflective.png",
-			alpha: 0.8,
+			mask: {
+				image: "reflective.png",
+				alpha: 0.7,
+			},
+			overlay: {
+				image: "water.png",
+			},
 		},
 	},
 	pirate_ship: {
diff --git a/game/03-JavaScript/weather/01-setup/weather-bindings.js b/game/03-JavaScript/weather/01-setup/weather-bindings.js
index 5341ce0ff1..db775e8127 100644
--- a/game/03-JavaScript/weather/01-setup/weather-bindings.js
+++ b/game/03-JavaScript/weather/01-setup/weather-bindings.js
@@ -48,10 +48,6 @@ setup.WeatherBindings = {
 		variable: () => Weather.lightsOn,
 		layers: ["location"],
 	},
-	locationAnimations: {
-		variable: () => V.options.locationAnimations,
-		layers: ["location"],
-	},
 	fireOn: {
 		variable: () => {
 			if (V.bird.firepit === undefined) return null;
diff --git a/game/03-JavaScript/weather/03-canvas/01-src/00-main/00-sky-canvas.js b/game/03-JavaScript/weather/03-canvas/01-src/00-main/00-sky-canvas.js
index 723cf8708b..15798af1fa 100644
--- a/game/03-JavaScript/weather/03-canvas/01-src/00-main/00-sky-canvas.js
+++ b/game/03-JavaScript/weather/03-canvas/01-src/00-main/00-sky-canvas.js
@@ -19,6 +19,11 @@ Weather.Sky = (() => {
 			this.element.height = height;
 		}
 
+		reset() {
+			this.clear();
+			this.ctx.reset();
+		}
+
 		/* Aliases */
 		clear() {
 			this.ctx.clearRect(0, 0, this.element.width, this.element.height);
diff --git a/game/03-JavaScript/weather/03-canvas/01-src/01-effect-manager.js b/game/03-JavaScript/weather/03-canvas/01-src/01-effect-manager.js
index f0e9a1278e..21fc0c047c 100644
--- a/game/03-JavaScript/weather/03-canvas/01-src/01-effect-manager.js
+++ b/game/03-JavaScript/weather/03-canvas/01-src/01-effect-manager.js
@@ -11,7 +11,7 @@ Weather.Sky.Effects = (() => {
 		params = { ...optionalParams, ...params };
 
 		if (_effects.has(params.name)) {
-			Weather.Sky.errors.add("Effects", `Effect with name '${params.name}', already exists, and will be overwritten.`);
+			console.error("Effects", `Effect with name '${params.name}', already exists, and will be overwritten.`);
 		}
 
 		const { name, ...context } = params;
diff --git a/game/03-JavaScript/weather/03-canvas/01-src/03-observables.js b/game/03-JavaScript/weather/03-canvas/01-src/03-observables.js
index bb61afcc13..9ed92d7685 100644
--- a/game/03-JavaScript/weather/03-canvas/01-src/03-observables.js
+++ b/game/03-JavaScript/weather/03-canvas/01-src/03-observables.js
@@ -81,6 +81,7 @@ Weather.Observables = (() => {
 	});
 
 	return {
+		objects: observables,
 		checkForUpdate: setBindings,
 	};
 })();
diff --git a/game/03-JavaScript/weather/03-canvas/02-lib/00-effects/effects-location.js b/game/03-JavaScript/weather/03-canvas/02-lib/00-effects/effects-location.js
index df5ad753f2..9a63941068 100644
--- a/game/03-JavaScript/weather/03-canvas/02-lib/00-effects/effects-location.js
+++ b/game/03-JavaScript/weather/03-canvas/02-lib/00-effects/effects-location.js
@@ -20,13 +20,106 @@ Weather.Sky.Effects.create({
 				},
 			},
 		},
+		{
+			effect: "colorOverlay",
+			drawCondition: () => !Weather.Sky.skyDisabled,
+			params: {
+				color: {
+					nightDark: "#00001ceb",
+					nightBright: "#0d0d26bf",
+					day: "#00000000",
+					dawnDusk: "#4f3605a5",
+					bloodMoon: "#380101bf",
+				},
+			},
+			bindings: {
+				sunFactor() {
+					return Weather.Sky.orbitals.sun.factor;
+				},
+				moonFactor() {
+					return Weather.Sky.moonBrightnessFactor;
+				},
+				bloodMoon() {
+					return Weather.bloodMoon;
+				},
+			},
+		},
 	],
 	init() {
 		this.animationFrame = 0;
 	},
 	draw() {
+		// Add the overlay to the effect itself
 		this.effects[0].draw();
+		this.effects[1].draw();
 		this.canvas.drawImage(this.effects[0].canvas.element);
+		this.canvas.ctx.globalCompositeOperation = "source-atop";
+		this.canvas.drawImage(this.effects[1].canvas.element);
+		
+	},
+});
+
+// Fallback only if reflections are disabled
+Weather.Sky.Effects.create({
+	name: "locationWater",
+	effects: [
+		{
+			effect: "locationImageAnimation",
+			bindings: {
+				location() {
+					return this.location;
+				},
+				key() {
+					return this.key;
+				},
+				parentLayer() {
+					return this.parentLayer;
+				},
+				fullPath() {
+					return `${this.path}/` + (this.location.folder ? `${this.location.folder}/` : "");
+				},
+			},
+		},
+		{
+			effect: "colorOverlay",
+			drawCondition: () => !Weather.Sky.skyDisabled,
+			params: {
+				color: {
+					nightDark: "#00001ceb",
+					nightBright: "#0d0d26bf",
+					day: "#00000000",
+					dawnDusk: "#4f3605a5",
+					bloodMoon: "#380101bf",
+				},
+			},
+			bindings: {
+				sunFactor() {
+					return Weather.Sky.orbitals.sun.factor;
+				},
+				moonFactor() {
+					return Weather.Sky.moonBrightnessFactor;
+				},
+				bloodMoon() {
+					return Weather.bloodMoon;
+				},
+			},
+		},
+	],
+	init() {
+		this.animationFrame = 0;
+	},
+	draw() {
+		// Ignore mask
+		this.effects[0].draw({ start: key => {
+			if (this.key === "reflective" && key === "mask") {
+				return false;
+			}
+			return true;
+		}});
+		this.effects[1].draw();
+		this.canvas.drawImage(this.effects[0].canvas.element);
+		this.canvas.ctx.globalCompositeOperation = "source-atop";
+		this.canvas.drawImage(this.effects[1].canvas.element);
 		
 	},
 });
@@ -58,7 +151,8 @@ Weather.Sky.Effects.create({
 		},
 	],
 	draw() {
-		this.effects[0].draw((drawCanvas, obj) => {
+		// Set the blur
+		this.effects[0].draw({ start: (key, obj, drawCanvas) => {
 			const glowSize = obj.size ?? this.defaultSize;
 			const glowColor = obj.color ?? this.defaultColor;
 			const glowAlpha = obj.alpha ?? this.defaultAlpha;
@@ -66,8 +160,8 @@ Weather.Sky.Effects.create({
 			drawCanvas.ctx.shadowBlur = glowSize;
 			drawCanvas.ctx.filter = `blur(0.5px) drop-shadow(0px 0px ${glowSize}px ${glowColor})`;
 			drawCanvas.ctx.globalAlpha = glowAlpha;
-			return drawCanvas;
-		});
+			return true;
+		}});
 		this.canvas.drawImage(this.effects[0].canvas.element);
 	},
 });
@@ -75,10 +169,14 @@ Weather.Sky.Effects.create({
 Weather.Sky.Effects.create({
 	name: "locationReflective",
 	defaultParameters: {
-		horizon: 112,
-		blur: 0.6,
-		reflectionAlpha: 0.8,
-		contrast: 0.9,
+		defaultHorizon: 40,
+		defaultBlur: 0.7,
+		defaultAlpha: 0.7,
+		defaultContrast: 0.9,
+		minAmplitude: 1,
+		maxAmplitude: 6,
+		waveFrequency: 10,
+		defaultWaveShiftFactor: 0.006,
 	},
 	effects: [
 		{
@@ -92,27 +190,167 @@ Weather.Sky.Effects.create({
 				},
 				parentLayer() {
 					return this.parentLayer;
-				}
+				},
+				fullPath() {
+					return `${this.path}/` + (this.location.folder ? `${this.location.folder}/` : "");
+				},
+			},
+		},
+		{
+			effect: "colorOverlay",
+			params: {
+				color: {
+					nightDark: "#00001ceb",
+					nightBright: "#0d0d26bf",
+					day: "#00000000",
+					dawnDusk: "#4f3605a5",
+					bloodMoon: "#380101bf",
+				},
+			},
+			bindings: {
+				sunFactor() {
+					return Weather.Sky.orbitals.sun.factor;
+				},
+				moonFactor() {
+					return Weather.Sky.moonBrightnessFactor;
+				},
+				bloodMoon() {
+					return Weather.bloodMoon;
+				},
 			},
 		},
 	],
 	init() {
+		if (this.location?.[this.key] === undefined) return;
+		const obj = this.location[this.key].mask;
+		if (obj === undefined) throw new Error("Property 'mask' is not defined in reflective property.")
+
 		this.reflectionCanvas = new Weather.Sky.Canvas();
 		this.locationCanvas = new Weather.Sky.Canvas();
+		this.distortionMask = new Weather.Sky.Canvas();
+		this.distortionCanvas = new Weather.Sky.Canvas(this.canvas.element.width, this.canvas.element.height);
+
+		this.horizon = resolveValue(obj.horizon, this.defaultHorizon) * setup.SkySettings.scale;
+		this.overlayAlpha = resolveValue(obj.alpha, this.defaultAlpha);
+		this.blur = resolveValue(obj.blur, this.defaultBlur);
+		this.contrast = resolveValue(obj.contrast, this.defaultContrast);
+		this.waveShiftFactor = resolveValue(obj.waveShiftFactor, this.defaultWaveShiftFactor);
+		this.animationCondition = resolveValue(obj.animationCondition, true);
+
+		// Precalculate the sine curve for multiple frames
+		this.sineFrames = [];
+		const numFrames = 24;
 		
+		// Perform a full cycle to look fluent
+    	const phaseShiftPerFrame = (2 * Math.PI) / numFrames;
+
+		this.startY = 2 + this.locationCanvas.element.height - ((this.locationCanvas.element.height - this.horizon) * setup.SkySettings.scale);
+		for (let frame = 0; frame < numFrames; frame++) {
+			const sines = [];
+			for (let y = 0; y < this.distortionCanvas.element.height; y++) {
+				// Linear interpolation for amplitude based on y position
+				const amplitude = this.minAmplitude + (this.maxAmplitude - this.minAmplitude) * (y / this.distortionCanvas.element.height);
+				const basePhase = (y + this.startY) * this.waveFrequency;
+				const animPhase = basePhase + frame * phaseShiftPerFrame;
+				const totalSineValue = Math.sin(basePhase) * amplitude + Math.sin(animPhase) * amplitude * 0.5;
+				sines.push(totalSineValue);
+			}
+			this.sineFrames.push(sines);
+		}
+
+		// Set up animation properties
+		const animationOptions = {
+			image: this.distortionCanvas.element,
+			canvas: this.canvas,
+			numFrames,
+			frameDelay: 150, // milliseconds per frame
+			cycleDelay: 0, // No delay between cycles
+			startDelay: 0,
+			currentFrame: 0,
+			alwaysDisplay: true,
+			condition: () => typeof this.animationCondition === "function" ? this.animationCondition() : this.animationCondition,
+		};
+
+		// Only try to draw overlay if there is one
+		this.drawOverlay = this.effects[0].animations.size > 0 && [...this.effects[0].animations.keys()].some(key => key !== "mask");
+
+		this.animation = new Weather.Sky.Animation(animationOptions);
+		this.parentLayer.animationGroup.add("distortionAnimation", this.animation);
+		this.animation.enable();
 	},
-	draw(canvas, layerCanvas) {
-		// Temporarily disable reflections until next update
+	draw(canvas, locationLayer) {
+		this.reflectionCanvas.reset();
+		this.locationCanvas.reset();
+		this.distortionMask.reset();
+		this.distortionCanvas.reset();
+
+		// Draw the background canvas, then flip it upside down, and only draw it on top of the reflection map
+		this.locationCanvas.drawImage(canvas.element);
+		this.locationCanvas.drawImage(locationLayer.element);
+		this.reflectionCanvas.ctx.save();
+		this.reflectionCanvas.ctx.filter = `blur(${this.blur}px) contrast(${this.contrast})`;
+		this.reflectionCanvas.ctx.globalCompositeOperation = 'source-over';
+		this.reflectionCanvas.ctx.scale(1, -1);
+		this.reflectionCanvas.ctx.drawImage(
+			this.locationCanvas.element,
+			0,
+			canvas.element.height - (this.horizon * 2),
+			this.reflectionCanvas.element.width,
+			this.horizon,
+			0,
+			-canvas.element.height,
+			this.reflectionCanvas.element.width,
+			this.horizon
+		);
+		this.reflectionCanvas.ctx.restore();
+
+		// Save the mask separately from the other water sprites
+		this.effects[0].draw({ end: (key, obj, drawCanvas) => {
+			if (key === "mask") {
+				this.distortionMask.drawImage(drawCanvas.element);
+				drawCanvas.clear();
+			}
+			return true;
+		}});
+
+		// Overlay sky only on the water effects
+		if (this.drawOverlay) {
+			this.effects[1].draw();
+			this.effects[0].canvas.ctx.globalCompositeOperation = "source-atop";
+			this.effects[0].canvas.drawImage(this.effects[1].canvas.element);
+			
+			// Add the rest of the water effects here
+			this.reflectionCanvas.ctx.globalAlpha = 1 - this.overlayAlpha;
+			this.reflectionCanvas.drawImage(this.effects[0].canvas.element);
+		}
+
+		// Draw the reflection below the distortion first, in case of transparent pixels
+		this.distortionCanvas.drawImage(this.reflectionCanvas.element);
+
+		// Loop through the predetermined distortion frames for a simplified ripple effect
+		const currentFrameIndex = this.animation.currentFrame;
+		const sines = this.sineFrames[currentFrameIndex];
+		for (let y = 0; y < this.distortionCanvas.element.height; y++) {
+			const shiftX = sines[y] * this.waveShiftFactor;
+			this.distortionCanvas.ctx.setTransform(1, 0, shiftX, 1, 0, 0);
+			this.distortionCanvas.ctx.drawImage(this.reflectionCanvas.element, 0, y + this.startY, this.distortionCanvas.element.width, 1, 0, y + this.startY, this.distortionCanvas.element.width, 1);
+		}
+		this.distortionCanvas.ctx.setTransform(1, 0, 0, 1, 0, 0);
 		
+		// Remove any distortions that are outside of the water mask
+		this.distortionCanvas.ctx.globalCompositeOperation = 'destination-in';
+		this.distortionCanvas.drawImage(this.distortionMask.element);
 
+		// Draw the final frame
+		this.canvas.drawImage(this.distortionCanvas.element);
 	},
 });
 
 Weather.Sky.Effects.create({
 	name: "locationImageAnimation",
-	// Make it asyncronous to wait for the image to load before animating without slowing down the main flow
 	async init() {		
 		const loadImage = async (key, obj) => {
+			// Make it asyncronous to wait for the image to load before animating without slowing down the main flow
 			return new Promise((resolve, reject) => {
 				let image = new Image();
 				const imagePath = typeof obj === "object" && obj.image ? obj.image : this.obj;
@@ -120,13 +358,14 @@ Weather.Sky.Effects.create({
 				const handleLoadedImage = () => {
 					if (typeof obj !== "object") obj = {};
 					
-					if (V.options.locationAnimations && obj.animation) {
+					// If it's an animation, add it to the animation group
+					if (obj.animation) {
 						const animationOptions = {
 							image,
 							canvas: this.canvas,
 							alwaysDisplay: obj.alwaysDisplay,
 							waitForAnimation: obj.waitForAnimation,
-							frameDelay: obj.animation.frameDelay, // Will never be lower than the layer updateRate
+							frameDelay: obj.animation.frameDelay,
 							cycleDelay: obj.animation.cycleDelay,
 							startDelay: obj.animation.startDelay,
 							startY: this.canvas.element.height - image.height,
@@ -142,7 +381,7 @@ Weather.Sky.Effects.create({
 						obj.animation.enable();
 						this.parentLayer.animationGroup.add(key, obj.animation);
 					}
-					else {
+					else { // If it's a static image
 						obj.image = image;
 					}
 					this.animations.set(key, obj);
@@ -188,19 +427,20 @@ Weather.Sky.Effects.create({
 		}
 	},
 
-	draw(onDraw) {
-		for (const obj of this.animations.values()) {
+	draw(onDraw = {}) {
+		this.canvas.ctx.reset();
+		for (const [key, obj] of this.animations.entries()) {
 			// Preprocess based on effect
-			if (onDraw) {
-				onDraw(this.canvas, obj);
+			if (onDraw.start) {
+				if (!onDraw.start(key, obj, this.canvas)) continue;
 			}
 			
 			// Check conditions
-			if (V.options.locationAnimations && obj.condition && !obj.condition(this.parentLayer.animationGroup)) {
+			if (obj.condition && !obj.condition(this.parentLayer.animationGroup)) {
 				continue;
 			}
 
-			if (V.options.locationAnimations && obj.animation) { // If animation, let the animation object draw the right frame
+			if (obj.animation) { // If animation, let the animation object draw the right frame
 				obj.animation.draw();
 			} else { // If static image
 				const yPosition = this.canvas.element.height - obj.image.height;
@@ -211,6 +451,10 @@ Weather.Sky.Effects.create({
 				
 				this.canvas.ctx.drawImage(obj.image, frameX, 0, this.canvas.element.width, this.canvas.element.height, 0, yPosition, this.canvas.element.width, this.canvas.element.height);
 			}
+
+			if (onDraw.end) {
+				onDraw.end(key, obj, this.canvas);
+			}
 		}
 	},
 });
diff --git a/game/03-JavaScript/weather/03-canvas/02-lib/01-layers/layer-location.js b/game/03-JavaScript/weather/03-canvas/02-lib/01-layers/layer-location.js
index 5c0fb42bd9..d3806f1c58 100644
--- a/game/03-JavaScript/weather/03-canvas/02-lib/01-layers/layer-location.js
+++ b/game/03-JavaScript/weather/03-canvas/02-lib/01-layers/layer-location.js
@@ -26,34 +26,46 @@ Weather.Sky.Layers.add({
 			},
 		},
 		{
-			effect: "colorOverlay",
-			compositeOperation: "source-atop",
-			drawCondition: () => !Weather.Sky.skyDisabled,
+			effect: "locationEmissive",
+			drawCondition: () => {
+				return setup.LocationImages[setup.Locations.get()].emissive;
+			},
 			params: {
-				color: {
-					nightDark: "#00001ceb",
-					nightBright: "#0d0d26bf",
-					day: "#00000000",
-					dawnDusk: "#4f3605a5",
-					bloodMoon: "#380101bf",
-				},
+				path: "img/misc/locations",
 			},
 			bindings: {
-				sunFactor() {
-					return Weather.Sky.orbitals.sun.factor;
+				location() {
+					const location = setup.Locations.get();
+					return setup.LocationImages[location];
 				},
-				moonFactor() {
-					return Weather.Sky.moonBrightnessFactor;
+				key() {
+					return "emissive";
 				},
-				bloodMoon() {
-					return Weather.bloodMoon;
+			},
+		},
+		{
+			effect: "locationReflective",
+			drawCondition: () => {
+				return V.options.reflections && !!setup.LocationImages[setup.Locations.get()].reflective;
+			},
+			params: {
+				path: "img/misc/locations",
+			},
+			bindings: {
+				location() {
+					const location = setup.Locations.get();
+					return setup.LocationImages[location];
+				},
+				key() {
+					return "reflective";
 				},
 			},
 		},
+		// Fallback only if reflections are disabled
 		{
-			effect: "locationEmissive",
+			effect: "locationWater",
 			drawCondition: () => {
-				return setup.LocationImages[setup.Locations.get()].emissive;
+				return !V.options.reflections && !!setup.LocationImages[setup.Locations.get()].reflective;
 			},
 			params: {
 				path: "img/misc/locations",
@@ -64,28 +76,26 @@ Weather.Sky.Layers.add({
 					return setup.LocationImages[location];
 				},
 				key() {
-					return "emissive";
+					return "reflective";
+				},
+			},
+		},
+		// Draw on top
+		{
+			effect: "locationImage",
+			params: {
+				path: "img/misc/locations",
+			},
+			bindings: {
+				location() {
+					const location = setup.Locations.get();
+					return setup.LocationImages[location];
+				},
+				key() {
+					return "layerTop";
 				},
 			},
 		},
-		// {
-		// 	effect: "locationReflective",
-		// 	drawCondition: () => {
-		// 		return false;//!!setup.LocationImages[setup.Locations.get()].reflective;
-		// 	},
-		// 	params: {
-		// 		path: "img/misc/locations",
-		// 	},
-		// 	bindings: {
-		// 		location() {
-		// 			const location = setup.Locations.get();
-		// 			return setup.LocationImages[location];
-		// 		},
-		// 		key() {
-		// 			return "reflective";
-		// 		},
-		// 	},
-		// },
 	],
 });
 
diff --git a/game/04-Variables/variables-start.twee b/game/04-Variables/variables-start.twee
index 891f1f30e6..448208b83c 100644
--- a/game/04-Variables/variables-start.twee
+++ b/game/04-Variables/variables-start.twee
@@ -75,6 +75,7 @@
 		sidebarFontSize: _globalThemeDefaults.sidebarFontSize,
 		font: _globalThemeDefaults.font,
 		weatherUpdate: true,
+		reflections: true,
 		fahrenheit: false,
 	}>>
 	<<setFont>>
diff --git a/game/04-Variables/variables-versionUpdate.twee b/game/04-Variables/variables-versionUpdate.twee
index b63c03b700..141955bd0b 100644
--- a/game/04-Variables/variables-versionUpdate.twee
+++ b/game/04-Variables/variables-versionUpdate.twee
@@ -5200,9 +5200,6 @@
 	<<if $options.clothingCaption is undefined>>
 		<<set $options.clothingCaption to true>>
 	<</if>>
-	<<if $options.locationAnimations is undefined>>
-		<<set $options.locationAnimations to true>>	
-	<</if>>
 	
 	<!-- Remove before final push, only for active testers -->
 	<<if $weatherObj.ice is undefined>>
diff --git a/game/base-system/overlays/options.twee b/game/base-system/overlays/options.twee
index e7119290c7..5f3e7d8ff4 100644
--- a/game/base-system/overlays/options.twee
+++ b/game/base-system/overlays/options.twee
@@ -400,15 +400,6 @@ IMPORTANT:
 <<widget "optionsperformance">>
 	<<setupOptions>>
 	<<if StartConfig.enableImages is true>>
-		<span class="gold">Experimental features</span>
-		<br>
-		<div class="description">Close the options menu for the change to apply.</div>
-		<div>
-			<div class="settingsToggle">
-				<label data-target="options.images" data-disabledif="V.options.images===0"><<checkbox "$options.weatherUpdate" false true autocheck>> New weather renderer for sidebar</label>
-			</div>
-		</div>
-		<br><br><br>
 		<span class="gold">Images</span>
 		<br>
 		<div>
@@ -444,13 +435,26 @@ IMPORTANT:
 			<div style="clear:both;">/*Keep at end of toggles*/</div>
 		</div>
 		<br>
-		<span class="gold">Animations</span>
+		<span class="gold">Sidebar</span>
+		<br>
+		<div class="description">Close the options menu for the change to apply.</div>
 		<div>
 			<div class="settingsToggle">
-				<label data-target="options.images" data-disabledif="V.options.images===0"><<checkbox "$options.sidebarAnimations" false true autocheck>> Sidebar character animations</label>
+				<label data-target="options.images" data-disabledif="V.options.images===0"><<checkbox "$options.weatherUpdate" false true autocheck>> Use new weather renderer for sidebar</label>
 			</div>
+		</div>
+		<div>
+			<div class="settingsToggle">
+				<label data-target='["options.images", "weatherupdate"]' data-disabledif="V.options.images===0||V.options.weatherUpdate===false"><<checkbox "$options.reflections" false true autocheck>> Render water reflections
+					<span class="linkBlue" tooltip="Could cause performance issues on old devices.">(?)</span>
+				</label>
+			</div>
+		</div>
+		<br><br><br>
+		<span class="gold">Animations</span>
+		<div>
 			<div class="settingsToggle">
-				<label data-target="options.images" data-disabledif="V.options.images===0"><<checkbox "$options.locationAnimations" false true autocheck>> Location image animations</label>
+				<label data-target="options.images" data-disabledif="V.options.images===0"><<checkbox "$options.sidebarAnimations" false true autocheck>> Sidebar character animations</label>
 			</div>
 			<div class="settingsToggle">
 				<label data-target='["options.images", "sidebaranimations"]' data-disabledif="V.options.images===0||V.options.sidebarAnimations===false"><<checkbox "$options.blinkingEnabled" false true autocheck>> Animate eyes blinking</label>
diff --git a/img/misc/locations/docks/reflective.png b/img/misc/locations/docks/reflective.png
index 872f769ec69a877a19410c241e92f6353ac57a7b..0b4149537b6fcd2e23fafcfce80874699a4800bf 100644
GIT binary patch
delta 154
zcmcb`c$jg5VLeZPPlzi61H*q9usC470?2163GxeOIQ{znzK6Fa0{NkyE{-7<y>HJ1
zavf0MVZNZY_y64R#9d`a8y6+ZcXN2fD}Q=!%ovd8#k@tiiaUnc@d4+GXH7v>rV)&f
z@AQBECM0lFPT+{lha?7;GdxR}BOc#>{(q@VkMITNYoEe;{{wAc@O1TaS?83{1OVRk
BLz4gi

delta 177
zcmX@ic#Cm@VLg9<Plzi61H*qL(C>6F11Q8=666=mP_pm!|H?Y`Q$T?XPZ!4!i{7_=
zC%FzN@VIO~^S{1WSUXqjrK84|3+j%B`iouc=R3OWk($0>n}@cdW8_T34aYQ87(Zxz
z);!20(s|=s<{kby(c)?Ob58O5To5pH^wgTMz458M)0xQ^l+9RkI3wRNz2$me%%F0b
VRY2EUuN7!1gQu&X%Q~loCIGX;N<RPq

diff --git a/img/misc/locations/docks/water.png b/img/misc/locations/docks/water.png
index ddc75ec34c17380007fb071db77cac65a19a443e..58fd3a7ed80c566fb87283f53ae1aa5ef885553e 100644
GIT binary patch
delta 361
zcmV-v0ha!S1CaxeF@H-*L_t(og=1hC1*2dTjDk@x(g9=tvp4lHPN9@YBQl>FpioMr
z5uZHEqWiBH&Rc6W8Xmn?57Pi+Qv%LgYvHvR?f?b`28Q(V`ubB)nt_3VVb{rPSQQJH
zYr)wcPF};{qpKmx0Uu6YW7s9G%W&UXtMNPo1A}7UntEakLVuTs@sUH57{CZqY_@47
z0|NsC!zsUTxNk@W0_Ivo2R#h1z%1J(t_u;tVmY>;#wJG&U?IjbY#{<uM{eAsI6%N$
z3$JDPEI=2Jv2gjn<is9Ek{m$D7avYuBRXA3^K%euH^KqP7Lfw5I(R2ZMqs7^QUEpw
ztBP}z6b($2IAQ=BXf+6JASD{8?f@8wv2giMj5>;<02fdd=O$V$(IIfZD5UXzQ3z3`
z-0hcN85kId3k9MaK&-{+fS9yElQgi(J)r@{CN?dMf&l{no%?zHdv~H900000NkvXX
Hu0mjfG#`<A

delta 349
zcmV-j0iynq1BC;SF@HZvL_t(og=1hC1*2dTjDk@x(g9=tvp4@?oI)uP6h1XTp_B+C
zUI!Q+z4rgSwHBOaU|?V{JbH~{N5IS?1Xvgt7#Pya>;IpE(hLj?47*NV!>U-oTno<r
zaPk@kA6*SGfKjn;&3^_428R1ZA&d+R3=A+nsRqFSdT0`K0DmmF%{HxMU|?WiIOP`(
z_YJ8)z+8*ypoak#m}R@fbs-{HEXNkq*yP9oEW}ubEkt1I$c=jx2MCyJ;k68(1?b{2
z7B2snoY+HjG_W9Bip!D@C$ABmE~NQ6h_xHx0A!0u0azWplO!WB(*P*|n}b!wxk-u!
zCQ2ND4YV4Bl2#NARCfRj#8|ixRh&~41-O8!I5*L1i4KALMInr^;+hb+{qidV0|RlP
vK$HWBwHO@`lNM-_23ENz(8AbJKpOx6^R0EqA}}Bw00000NkvXXu0mjf-A##p

diff --git a/img/misc/locations/lake/water.png b/img/misc/locations/lake/water.png
index bde749d6623a092f12a14f647bae8a627397e53d..fa7c28ff4319a37b312a9065b82632fca2628ef0 100644
GIT binary patch
delta 160
zcmV;R0AK&k0<{8=oqvN#L_t(oh3$|_3cw%?M00~0I`=<oO*DR}U5Q=I1108VASglz
zA%qY@2q6aU3|i#?|9E(<uw$j&P<x{&Z#vZ@p51TcMO()+Is)x>iOapbynQNsre?pJ
zm$7|0DSyp6cEU@Q$QCf|bIGO<Yo7G6K&%%`pLo*80)w$t5g9}2w0{6%s6fj+|KR`t
O0000<MNUMnLSTZUD@D}+

delta 185
zcmV;q07n0{0?q=EoqwE3L_t(oh3%113dA4`L~{cTJNG~9n&@gu)fR;Png=cFn-NhF
zLI@#*5JCts@F}2=9N<45K33RgL;FK*j;egoX&%4n@CSX-zTzE?K!=^;@~kg6pBwGW
z_g3(}$10Ft_2sgjoILBD>2*M=Ly<s{HI^-ee##I?Pl4GEyE49vTS*tfEgkB8pxr<a
nc2YZP(*9J7z<A0{;Hm!wfU`i_fr*9Z00000NkvXXu0mjf0vA^U

diff --git a/img/misc/locations/sea/base.png b/img/misc/locations/sea/base.png
deleted file mode 100644
index 1f4234d2e589296a31c13d9ce0263ae5a19f496b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 526
zcmV+p0`dKcP)<h;3K|Lk000e1NJLTq00961001Be0{{R3%6C@Q0000IP)t-s0000P
zlC(6n)hNv0itF)-;r+}eVrBpU00wkYPE!E?|NsC0dU@df0004{Nkl<ZXo2mP*^a{?
z3`N0A{{NS&ge2H_11ya+Q|?pSW3G+MmeSVN*4Eb6*4Eb6)>o>WI({x4&xiV@>G}CU
z90z!OG=avW`3?J&Zvhl6$h~Mll=FZ&KAm5)Pb8}Lg_}2i2CVae1SXH>7wmBZP?JQD
z#^n@fyn`M4HUF9VT0r7a;^4+@>~YsV=<o9`fUJP4IY&Jq1S5EHl*{XMoCosv^Lrc6
z4o10$?c`kJLkSEX&9B+hEr3r-d7q*Hhr8pYfC;`*pI~1b0QB1AcL=>#=XtRcxICI)
zw?``gD!IFKZk4`D$eHY<o)?RAdzj*$|44np{8Af(#cH$g-{aCX$b+cOf8{B^YeU^8
zH_zHOD-MTZC-C@y{|J}uliD9vquF68;1~IN{nrZMy&+fX0B60&ukQ1VmkkEF=O3w`
z9)gg&W_A5@IAovG|6-m;>XUfVkB8^gJkRgOtILlJU*(4XO#KNhC_&G93vp2K7?0*R
z=cl{_@MsN>at*g5z~<5X_WbBAK-+b+w-g{piPwOx-;8$#(_HuW>eJ`<0a|1$BiNZY
Qh5!Hn07*qoM6N<$g7CNo@c;k-

diff --git a/img/misc/locations/sea/reflective.png b/img/misc/locations/sea/reflective.png
index aa9c933c0a92ebf7a93dad303dacadd28d32c5d9..c6b1ef982c7bf81d84994d7f20f4bf13d04730f7 100644
GIT binary patch
delta 90
zcmey)STR8*gfqY=#Fc@8;XfD@S1e5ia+phk{DK*_{`WgpxDLpb_H=O!vFJ@skYH_Q
oGZ13n@nJczqA8H|N+<(^8W+pM^|{mffXWy=UHx3vIVCg!0OeaAt^fc4

delta 218
zcmV<0044u;_W_V3e*tq+M?wIu&K&6g000tDOjJbx000@1v^2HVD9qoA>+y-<{mdp}
zW&i*H26R$RQvm<}|NsAbdEov4004nWL_t(IjqQ*@62Kq`18G|R|K*OLLR9WOG^ffe
z+o;H&&*W~=$D-rI4;Hz4fIA0cJ^_pGA&)YUBsc<{;xiedRIpCR1fn=f!*Eay2N3sO
z9YX5}ORF*gexu@=JLHsv`&>iH{cPBG`vhxAtTF?tdO&obe=u3R1D6bhb=uzY1+%0D
UXUMdPYXATM07*qoM6N<$g6U*nJOBUy

diff --git a/img/misc/locations/sea/water.png b/img/misc/locations/sea/water.png
new file mode 100644
index 0000000000000000000000000000000000000000..8513c6f3814bfcbb2216ff526119f164ed5e9cdb
GIT binary patch
literal 242
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|L<4+6T!FND
zaff2x8l9D=O}1Xmzw;jm^i~GU07|fz1o;IsfCT>kUs}KHH&Cd;)5S5wqWA3tN1+A-
z4(5x`{^ysuENGeb@zudQx72PooLGPUMa9H9Y6&R^-ycrUdvZL>Lm-Fu*&}1Yc^kAO
zR;s+=-ola|z^nPORZ-;lk~<R(KJ?8p48P3Lw0XPflC$ZDW^I+eGHHc@Y01}1Gw$@y
jJuA24$rb~@@=C_@AJ|Hr*cD2FZeZ|q^>bP0l+XkK>i}fk

literal 0
HcmV?d00001

-- 
GitLab