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.
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.
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 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 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.
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.