Home

How did I use Leaflet JS in this project?

A quick explanation

Leaflet is an open-source JavaScript library used for mobile friendly inetactive maps. Typically it is used with OpenStreetMap but I modified it to display a single image for the map.
There are a numeber of ways to make maps using SVGs and other means (demonstrated here in this Bloodborne project by previous DIGIT students!) but I wanted you to be able to zoom and look at the details of the map within the game to show off how beautiful the map is and where exactly all the points are.

What I modified

The main thing I modified is how it displays a single image for the maps. They way I did this was replacing the part of the code,

L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap'
}).addTo(map);

with this

var imageUrl = 'images/eldenMap.png';
                    var imageBounds = [[0, 0], [600, 800]];
                    L.imageOverlay(imageUrl, imageBounds).addTo(map);
                    map.fitBounds(imageBounds);

which allowed for there to be a single image that wasn't tiled.

The custom markers

For the markers on the map I had to make each icon type separately and add them to a layer group (this was for hiding and showing each category of marker, I will explain this later). The code for this looked as so,

var IconIcon = L.Icon.extend({
                        options: {
                            iconSize: [50, 50],
                            iconAnchor: [23, 40],
                            popupAnchor: [2, -30]
                        }
                    });
                    var remIcon = new IconIcon({
                        iconUrl: 'images/blue.png'
                    });
                    var towerIcon = new IconIcon({
                        iconUrl: 'images/brown.png'
                    });
                    var npcIcon = new IconIcon({
                        iconUrl: 'images/red.png'
                    });
                    var towerLayer = L.layerGroup().addTo(map);
                    var remLayer = L.layerGroup().addTo(map);
                    var npcLayer = L.layerGroup().addTo(map);
                    L.icon = function (options) {
                        return new L.Icon(options);
                    };

That first section, the icon options, is just defining how big the icons will be on the map and where it is anchored within its image limits.
The three vars are there to establish each kind of marker, as you can see they are an NPC, tower, and rememberance markers.
The layer group section of that code is for the hide/show option I added to the legend, which is useful for when it gets too cluttered on the map. I will show how this specifically works in the next code explanation.

The popups

The popups weren't hard to add to the map itself, it just consisted of a lot of copy and pasting information.

L.marker([261.2656, 278.2192], {
                        icon: towerIcon
                    }).addTo(towerLayer).bindTooltip("Divine Tower of Liurnia", {
                        direction: 'top', permanent: false, offset: [2, -30]
                    }).bindPopup(`<div style="width:300px;">
                            <h3>Divine Tower of Liurnia</h3>
                            <iframe src="results.html?id=liurnia" width="100%" height="250"
                                style="border:1;"> </iframe>
                        </div>
                    `, { maxWidth: 320 });

You can see in the example above that there is a set of coordinates, this is the point at which the marker is placed. To find this I added a tempory coords control, once all the markers are added I plan to remove this as it doesn't corralate to the in game coords.

var CoordsControl = L.Control.extend({
                        onAdd: function (map) {
                            var container = L.DomUtil.create('div', 'leaflet-control-mouseposition');
                            map.on('mousemove', function (e) {
                                container.innerHTML = 'Lat: ' + e.latlng.lat.toFixed(4) + ', Lng: ' + e.latlng.lng.toFixed(4);
                            });
                            return container;
                        },
                        onRemove: function (map) {
                        }
                    });

                    map.addControl(new CoordsControl({
                        position: 'bottomleft'
                    }));

Within the bind.popup element there is an iFrame, this iFrame pulls from an HTML that isnt accessible from the website itself. This HTML contains information for the popup, such as information about the item, location, or NPC.
In the case of the NPC, there is another thing it grabs for the random dialogue. The dialogue comes from a JSON file I sourced from the Elden Ring Text Explorer which is randomly selected using this code:

let melinaLines = [];

            fetch("EldenDialogue.json").then(res => res.json()).then(data => {

                const npc = data.dialog["100"];

                let rawLines = npc.info_en.split("<br/><br/>");

                melinaLines = rawLines.map(line => line.replace(/\[\d+\]/, "").trim()).filter(line => line.length > 0);
            });

            function getMelinaDialogue() {

                if (melinaLines.length === 0) {
                    document.getElementById("dialogueMelina").innerHTML = "Loading dialogue...";
                    return;
                }

                const line = melinaLines[Math.floor(Math.random() * melinaLines.length)];

                document.getElementById("dialogueMelina").innerHTML = line;
            }

The function const.npc = data.dialog["100"]; uses the number, in this case 100, to identify the exact set of dialogue for the character. Like in the example above, 100 is used for the character Melina, but Varré uses the number 301.
There is also the function let rawLines = npc.info_en.split("<br/><br/>"); which seperates each string of dialogue when it encounters a set of breaks in the JSON.

The legend

The legend was a later addition to the map, specifically after I started to add a lot of points. The colors help distinguish them enough, but after a lot of overlap, I decided to add the show/hide feature.
The code for the legend looks like:

var legend = L.control({ position: "bottomleft" });

                    legend.onAdd = function () {
                        var div = L.DomUtil.create("div", "info legend");

                        div.innerHTML += "<h4>Important Items and Places</h4>";
                        div.innerHTML += '<button onclick="toggleTowers()" id="button"><img src="images/brown.png" width="20"/> Divine Towers<br/></button>';
                        div.innerHTML += '<button onclick="toggleRemembrances()" id="button"><img src="images/blue.png" width="20"/> Remembrances<br/></button>';
                        div.innerHTML += '<button onclick="toggleNpcs()" id="button"><img src="images/red.png" width="20"/> Main NPCs<br/></button>';

                        return div;
                    };

                    legend.addTo(map);
                    let towersVisible = true;
                    let remembrancesVisible = true;
                    let npcsVisible = true;

                    function toggleTowers() {
                        towersVisible = !towersVisible;

                        if (towersVisible) {
                            towerLayer.addTo(map);
                        } else {
                            map.removeLayer(towerLayer);
                        }
                    }

                    function toggleRemembrances() {
                        remembrancesVisible = !remembrancesVisible;

                        if (remembrancesVisible) {
                            remLayer.addTo(map);
                        } else {
                            map.removeLayer(remLayer);
                        }
                    }

                    function toggleNpcs() {
                        npcsVisible = !npcsVisible;

                        if (npcsVisible) {
                            npcLayer.addTo(map);
                        } else {
                            map.removeLayer(npcLayer);
                        }
                    }

You can see in this code snippet that the functions for the toggles are listed, this was the reason for the layer groups, as the function toggle only worked if they were grouped.
There are also the buttons which are used to collapse each category witthin the legend. These are made with an inner HTML element, which allows that HTML display to work with the JS fuction.

CSS changes

One of the other things I modified from the original Leaflet library was the css of the map. The popups of the markers were very plain at first, but I wanted them to be in the same golden color scheme so I took the CSS from the original file, and styled it to fit what I wanted.

.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
    font-family: "Agmena" !important;
    font-size: 16px !important;
    background: linear-gradient(180deg, #885f22, #312207) !important;
    color: #e6d8a8 !important;
    border: 3px solid #8b7500 !important;
    box-shadow: 0 0 20px rgba(212, 175, 55, 0.5) !important;
}

.leaflet-container a.leaflet-popup-close-button {
    color: #c6b477 !important;
}

.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
    color: #e6d8a8 !important;
}

The !important at the end of the CSS properties overrides the original CSS of Leaflet. This allowed me to not mess with the actual file and store all my CSS in one file.