Dungeon Portfolio: Gamifying My Resume with Minecraft Dungeons Aesthetics
Portfolios are often static, linear lists of skills and projects. Passionate about web development and fascinated by the world of MinecraftŠ, I wanted to break this mold.
I developed Dungeon Portfolio, an interactive Single Page Application (SPA) inspired by the map selection screen of Minecraft DungeonsŠ. Instead of scrolling through a PDF, recruiters and visitors explore my professional journey as an adventurer exploring a dungeon map.
You can check out the live version here: dungeon.maxgrz.fr

The Mathematics of Coordinate Mapping: Projections vs Cartesian Planes
Repurposing a geographical mapping tool like Leaflet for a 2D game map requires understanding the mathematics of cartographic projections:
- The Spherical Distortion Problem: Standard map libraries default to the EPSG:3857 (Web Mercator) projection. This system assumes the world is a sphere and uses logarithmic scaling for latitude to maintain local angles (conformal projection). If you load a flat 2D game image under this projection, coordinates distort exponentially as you move away from the equator, causing your game map to warp and bend.
- Euclidean Grid System (CRS.Simple): To keep the game map flat and proportioned, we switch to
L.CRS.Simple. This system establishes a flat 2D Cartesian grid where the distance between two points follows the standard Euclidean metric: $$d = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$$ InCRS.Simple, one map unit projects directly to one image pixel. - Axis Inversion: Leaflet is hardcoded to use
[latitude, longitude]coordinates which maps mathematically to[y, x]. When coding game nodes, developers must be careful to invert standard Cartesian $(x, y)$ coordinates to $(y, x)$ coordinate pairs in Leaflet to place markers accurately on the visual islands.
The Concept: UI/UX Gamification
The goal was to bridge the gap between game UI design and web standards. The application needed to feel like a native game menu while remaining a responsive web app.
- The Map: Acts as the primary navigation interface. Unlike a standard navbar, spatial positioning determines the hierarchy of information.
- The Nodes: Each âmissionâ point (e.g., Paladium Peaks, Rusty Rocks) represents a specific career chapter or technical skill set.
- The Inventory System: Clicking a node triggers a modal sidebar, simulating an inventory inspection screen for detailed reading.
Technical Architecture & Stack
To support high interactivity and maintain type safety, I selected a robust stack centered around the Vue 3 ecosystem.
Core Framework
- Nuxt 3: Chosen for its robust directory-based routing and auto-imports. The project leverages the Composition API (
<script setup lang="ts">) for better logic reuse and cleaner component architecture. - TypeScript: Used strictly throughout the project to ensure type safety, particularly for the custom Leaflet interfaces and data models for âMissionsâ and âExperiencesâ.
The Mapping Engine
- Leaflet & Vue-Leaflet: While Leaflet is traditionally used for geographical data (OpenStreetMap), it is an industry standard for âRaster Mapsâ (game maps) when combined with simple coordinate systems.
- Custom Composables: The logic is decoupled from the view using Nuxt composables:
useCoordinate: Manages the playerâs current X/Y position on the map.useSwitchMap: Handles state transitions between different dungeon maps (e.g., Overworld vs. The End).useContactForm: Manages the visibility state of the overlay contact modal.
Infrastructure
- Azure Blob Storage: Used for storing static assets like images and videos.
- Cloudflare CDN: Provides global content delivery network for faster load times.
Deep Dive: Hacking Leaflet with CRS.Simple
The most challenging technical aspect of this project was adapting Leaflet-a library designed for spherical geographical maps (using Latitude/Longitude)-to work with a flat, pixel-based game map.
The Problem: Earth is Round, Dungeons are Flat
Standard maps use the EPSG:3857 projection (Web Mercator). If you try to place a game map image on a standard Leaflet instance, the coordinates will be interpreted as degrees of latitude and longitude, causing the image to warp or vanish at high zoom levels.
The Solution: L.CRS.Simple
To solve this, I utilized L.CRS.Simple, a Coordinate Reference System that represents a square grid. In this system, one map unit equals one pixel. This allows us to map (x, y) pixel coordinates from the original Photoshop/Figma design directly to the interactive map.
Implementation in Nuxt
In index.vue, I calculate the bounds dynamically based on the imageâs aspect ratio. This ensures the user cannot pan âinto the voidâ outside the game map.
// index.vue logic
const width = ref(390);
const height = ref(200);
// Defining the "world" boundaries based on image dimensions
// Note: Leaflet uses [y, x] (Lat, Lng) order, not [x, y]
const bounds = computed(() =>
[
[0, 0], // Bottom-Left
[height.value, width.value], // Top-Right
] as L.LatLngBoundsLiteral
);
The Map component configuration is crucial. I strictly control the zoom levels (min-zoom="1", max-zoom="3") to mimic the restricted camera movement of an RPG UI, rather than the infinite zoom of Google Maps.
<LMap
ref="map"
:center="[height / 2, width / 2]"
:max-bounds="bounds"
:min-zoom="1"
:max-zoom="3"
:options="{
attributionControl: false,
zoomControl: false // Hiding default controls for game feel
}"
:zoom="2"
crs="Simple"
style="background-color: #e7d6c2; z-index: 4"
@ready="mapInitialized"
>
<LImageOverlay :bounds="bounds" url="/maps/map.jpg"/>
</LMap>
Asset Loading Strategy
High-resolution fantasy maps are heavy assets. To prevent the âtexture poppingâ effect often seen in web games, I implemented a dedicated loading state manager.
Instead of relying on browser defaults, I created a loading reactive state that interpolates a progress bar. The map is only interactive once the mapInitialized event fires, ensuring a smooth entry into the experience.
const loading = ref({state: true, percentage: 0});
const updateLoadingProgress = async (increment: number) => {
// Custom interval logic to smooth out the loading bar animation
// simulating a real game loading screen
let currentIncrement = 0;
const intervalId = setInterval(() => {
// ... increment logic ...
}, 80);
};
const mapInitialized = async () => {
// Triggered by @ready on LMap component
await updateLoadingProgress(40);
};
Part 3: Interactive Markers & Game Logic
Biomes as Navigation Nodes
In Minecraft Dungeons, players select missions from a map. I replicated this by treating every major section of my resume as a âBiomeâ located at specific coordinates.
Instead of a standard v-for loop, I manually placed markers to match the artistic design of the background map. This required precise coordinate mapping to ensure the âMissionsâ aligned perfectly with the visual islands on the underlying image.
Scaling Icons with Math
Leaflet markers usually have fixed pixel sizes. However, in a zoomed-in game map, icons need to feel integrated. I used dynamic calculation for icon sizes directly in the template to maintain the pixel-art aspect ratio while fitting the map scale.
<LMarker :lat-lng="[63, 205]" @click="markerOnClick($event, 'RUSTY ROCKS')">
<LIcon
:icon-size="[844/8, 460/8]"
icon-url="/markers/RUSTY ROCKS.png"
/>
</LMarker>
When a user clicks a marker, it triggers the markerOnClick function, which utilizes the useSidebar composable. This abstracts the state management, allowing the sidebar to open with the correct context (Title, Data) without cluttering the map component logic.
async function markerOnClick(e: any, title: string) {
// Opens the "Inventory" (Sidebar) with specific mission data
openSidebar(title, e.latlng.lat, e.latlng.lng);
}
The âKey Golemâ Interaction
One of my favorite details is the Contact button. Instead of a boring âContact Meâ link, I used the Key Golem mob from the game. This required handling state-based animations within the Leaflet marker system.
The Golem has two states:
- Sleeping: The default static or low-energy state.
- Awake: triggered on hover, showing an active animation.
I implemented this using Vueâs reactivity system to swap the asset URL dynamically based on mouse events.
// Managing hover state
const hoverMeVisible = ref(false);
function hoverMe(value: boolean) {
hoverMeVisible.value = value;
}
In the template, the :icon-url prop binds to a ternary operator. This creates a seamless transition between the âSleepingâ GIF and the âActiveâ GIF, making the map feel alive.
<LMarker
:lat-lng="[30, 350]"
@click="switchContact"
@mouseout="hoverMe(false)"
@mouseover="hoverMe(true)">
<LIcon
:icon-size="[100, 100]"
:icon-url="hoverMeVisible
? '/mobs/KeyGolemWithStars.gif'
: '/mobs/KeyGolem_Diamond_Sleeping.gif'"
/>
</LMarker>
Navigation Logic (The âYou Are Hereâ Marker)
To ground the user, I added a dynamic âYOU ARE HEREâ flag. This marker is bound to the coordinate reactive object from my useCoordinate composable. This allows the marker to potentially move or update if I decide to add âwalkingâ mechanics in the future, separating the playerâs position from the mapâs static assets.
<LMarker :lat-lng="[coordinate.latitude, coordinate.longitude]">
<LIcon :icon-size="[755/7, 272/7]" icon-url="/markers/YOU ARE HERE.png"/>
</LMarker>
Part 4: Scalability, Future Updates & Conclusion
The âEnd Gameâ: Scalability & Future Plans
One of the strengths of this architecture is its scalability. Because the map logic is decoupled from the content, adding new âbiomesâ or even entirely new dimensions is straightforward.
In the codebase, I have already prepared the structure for a second map: The End.
<LMap v-else ref="map2" ... >
<LImageOverlay :bounds="..." url="/maps/the end.png"/>
<LMarker :lat-lng="..." @click="markerOnClick($event, 'INITIATE ISLAND')">
<LIcon ... icon-url="/markers/INITIATE ISLAND.png"/>
</LMarker>
</LMap>
By leveraging a simple v-if/v-else toggle driven by a useSwitchMap composable, the application can support multiple distinct environments-allowing my portfolio to grow as my career expands into new territories (literally and metaphorically).
Conclusion
Building Dungeon Portfolio was more than just a design exercise; it was a challenge in repurposing strict geographical tools for creative expression.
By combining Nuxt 3âs performance, TypeScriptâs reliability, and Leafletâs flexibility, I transformed a static CV into an immersive exploration game. It proves that technical portfolios donât have to be boring to be professional-they just need good loot.
Links & Resources
- Live Experience: dungeon.maxgrz.fr
- Source Code: GitHub Repository
Feel free to fork the project to create your own map, or drop a star â on the repo if you enjoyed the journey !
Enjoyed this article ? Let's connect !
I'm Maximilien, a full-stack developer and MSc AI student specializing in on-device AI (Rust/NPU) and clean-core development (SAP BTP/CAP). Let's connect on LinkedIn or collaborate on GitHub !