diff --git a/buildtools/ant_modules/dist.xml b/buildtools/ant_modules/dist.xml
index 1803f5f043a..eeb5987e893 100644
--- a/buildtools/ant_modules/dist.xml
+++ b/buildtools/ant_modules/dist.xml
@@ -132,6 +132,10 @@
+
+
+
+
diff --git a/src/js/stendhal/Client.ts b/src/js/stendhal/Client.ts
index 6f911211867..3654a45cbc3 100644
--- a/src/js/stendhal/Client.ts
+++ b/src/js/stendhal/Client.ts
@@ -485,7 +485,9 @@ export class Client {
const musicVolume = parseFloat(zoneinfo["music_volume"]);
stendhal.ui.soundMan.playSingleGlobalizedMusic(zoneinfo["music"],
!Number.isNaN(musicVolume) ? musicVolume : 1.0);
-
+ // parallax background
+ stendhal.data.map.parallax.setImage(zoneinfo["parallax"]);
+ // zone coloring
if (zoneinfo["color"]) {
const hsl = Color.hexToHSL(Color.numToHex(Number(zoneinfo["color"])));
// workaround until able to get right saturation level from color methods
diff --git a/src/js/stendhal/data/Map.ts b/src/js/stendhal/data/Map.ts
index 2e493b768dd..951ca6a81ae 100644
--- a/src/js/stendhal/data/Map.ts
+++ b/src/js/stendhal/data/Map.ts
@@ -16,6 +16,7 @@ import { Paths } from "./Paths";
import { LandscapeRenderingStrategy, CombinedTilesetRenderingStrategy } from "../landscape/LandscapeRenderingStrategy";
import { IndividualTilesetRenderingStrategy } from "../landscape/IndividualTilesetRenderingStrategy";
+import { ParallaxBackground } from "../landscape/ParallaxBackground";
export class Map {
@@ -63,6 +64,8 @@ export class Map {
Paths.tileset + "/item/blood/small_stains"
];
+ private parallax: ParallaxBackground;
+
/** Singleton instance. */
private static instance: Map;
@@ -86,6 +89,7 @@ export class Map {
} else {
this.strategy = new CombinedTilesetRenderingStrategy();
}
+ this.parallax = ParallaxBackground.get();
}
/**
diff --git a/src/js/stendhal/data/Paths.ts b/src/js/stendhal/data/Paths.ts
index 15931181eb9..28eeb9aec65 100644
--- a/src/js/stendhal/data/Paths.ts
+++ b/src/js/stendhal/data/Paths.ts
@@ -23,6 +23,7 @@ export class Paths {
public static readonly weather = Paths.sprites + "/weather";
public static readonly achievements = Paths.sprites + "/achievements";
public static readonly tileset = Paths.data + "/maps/tileset";
+ public static readonly parallax = Paths.data + "/maps/parallax";
public static readonly ws = Paths.extractPath("data-ws");
/**
diff --git a/src/js/stendhal/landscape/ParallaxBackground.ts b/src/js/stendhal/landscape/ParallaxBackground.ts
new file mode 100644
index 00000000000..766badd8cab
--- /dev/null
+++ b/src/js/stendhal/landscape/ParallaxBackground.ts
@@ -0,0 +1,77 @@
+/***************************************************************************
+ * Copyright © 2024 - Faiumoni e. V. *
+ ***************************************************************************
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU Affero General Public License as *
+ * published by the Free Software Foundation; either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+import { singletons } from "../SingletonRepo";
+
+
+export class ParallaxBackground {
+
+ /** Tiled image to be drawn. */
+ private image?: HTMLImageElement;
+
+ /** Singleton instance. */
+ private static instance: ParallaxBackground;
+
+
+ /**
+ * Retrieves singleton instance.
+ */
+ static get(): ParallaxBackground {
+ if (!ParallaxBackground.instance) {
+ ParallaxBackground.instance = new ParallaxBackground();
+ }
+ return ParallaxBackground.instance;
+ }
+
+ /**
+ * Hidden singleton constructor.
+ */
+ private constructor() {
+ // do nothing
+ }
+
+ /**
+ * Sets image to be drawn.
+ *
+ * @param name {string}
+ * Relative path (exluding .png filename suffix) to image inside "data/maps/parallax" directory
+ * or `undefined` to unset.
+ */
+ public setImage(name?: string) {
+ if (!name) {
+ this.image = undefined;
+ return;
+ }
+ const fullPath = singletons.getPaths().parallax + "/" + name + ".png";
+ this.image = singletons.getSpriteStore().get(fullPath);
+ }
+
+ draw(ctx: CanvasRenderingContext2D, offsetX: number, offsetY: number) {
+ if (!this.image || !this.image.height) {
+ return;
+ }
+ // FIXME: jumps when shifting tiles
+ const tilesX = Math.max(Math.ceil(ctx.canvas.width / this.image.width) + 1, 1);
+ const tilesY = Math.max(Math.ceil(ctx.canvas.height / this.image.height) + 1, 1);
+ const clipLeft = (offsetX / 2) % ctx.canvas.width;
+ const clipTop = (offsetY / 2) % ctx.canvas.height;
+ for (let ix = 0; ix < tilesX; ix++) {
+ for (let iy = 0; iy < tilesY; iy++) {
+ ctx.drawImage(this.image,
+ 0, 0, this.image.width, this.image.height,
+ (ix*this.image.width)+offsetX-clipLeft,
+ (iy*this.image.height)+offsetY-clipTop,
+ this.image.width, this.image.height);
+ }
+ }
+ }
+}
diff --git a/src/js/stendhal/ui/ViewPort.ts b/src/js/stendhal/ui/ViewPort.ts
index 440a770ad9d..3f00a7aff68 100644
--- a/src/js/stendhal/ui/ViewPort.ts
+++ b/src/js/stendhal/ui/ViewPort.ts
@@ -149,6 +149,7 @@ export class ViewPort {
// FIXME: filter should not be applied to "blend" layers
this.applyFilter();
+ stendhal.data.map.parallax.draw(this.ctx, this.offsetX, this.offsetY);
stendhal.data.map.strategy.render(this.ctx.canvas, this, tileOffsetX, tileOffsetY, this.targetTileWidth, this.targetTileHeight);
this.weatherRenderer.draw(this.ctx);