Of course! This is a fantastic project. Here is a complete, self-contained HTML file that includes the CSS and JavaScript to do exactly what you want.
You can save this code as a single visualizer.html file and open it in your web browser. It will work entirely locally; no web server is needed.
- File Pickers: Allows you to upload your map image (
.png,.jpg) and your server log file. - Log Parser: Uses a Regular Expression to find all player position entries in the log.
- Canvas Drawing: Renders your map and draws the parsed path on top of it.
- Interactive Calibration: Includes input fields for you to tweak the
scaleandmap dimensionsin real-time to get a perfect alignment. - Clear Instructions: Guides you on how to find the correct calibration values.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Valheim Map Log Visualizer</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #1a1a1a;
color: #f0f0f0;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
margin: 0;
}
.container {
max-width: 1200px;
width: 100%;
}
h1, h2 {
text-align: center;
color: #ffc107;
}
.controls {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
background-color: #2a2a2a;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.control-group {
padding: 15px;
border: 1px solid #444;
border-radius: 5px;
margin: 10px;
}
.control-group h3 {
margin-top: 0;
border-bottom: 1px solid #ffc107;
padding-bottom: 5px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="file"], input[type="number"] {
padding: 8px;
border-radius: 4px;
border: 1px solid #555;
background-color: #333;
color: #f0f0f0;
margin-top: 5px;
}
#status {
text-align: center;
margin: 15px 0;
font-style: italic;
color: #aaa;
}
#mapCanvas {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
background-color: #222;
border: 2px dashed #555;
}
.instructions {
background-color: #2a2a2a;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #ffc107;
}
</style>
</head>
<body>
<div class="container">
<h1>Valheim Map Log Visualizer</h1>
<div class="instructions">
<h3>How to Use:</h3>
<ol>
<li><strong>Calibrate:</strong> Get your map image (often found in `C:\Users\[YourUser]\AppData\LocalLow\IronGate\Valheim\worlds_local\`). Open it in an image editor to find its dimensions (e.g., 2048x2048) and enter them below. The default scale is a good starting point.</li>
<li><strong>Upload Map:</strong> Click "Choose File" and select your map image.</li>
<li><strong>Upload Log:</strong> Select your server log file (e.g., `server_log.txt`). The path will be drawn automatically.</li>
<li><strong>Fine-Tune:</strong> Adjust the "World to Pixel Scale" value until the path lines up perfectly with roads or landmarks on your map.</li>
</ol>
</div>
<div class="controls">
<div class="control-group">
<h3>1. Calibration</h3>
<label for="mapWidth">Map Image Width (px):</label>
<input type="number" id="mapWidth" value="2048">
<label for="mapHeight" style="margin-top: 10px;">Map Image Height (px):</label>
<input type="number" id="mapHeight" value="2048">
<label for="scale" style="margin-top: 10px;">World to Pixel Scale:</label>
<input type="number" id="scale" value="0.098" step="0.0001">
</div>
<div class="control-group">
<h3>2. Upload Map</h3>
<label for="mapInput">Select Map Image (.png, .jpg):</label>
<input type="file" id="mapInput" accept="image/png, image/jpeg">
</div>
<div class="control-group">
<h3>3. Upload Log</h3>
<label for="logInput">Select Server Log File (.log, .txt):</label>
<input type="file" id="logInput" accept=".log, .txt">
</div>
</div>
<p id="status">Waiting for map and log files...</p>
<canvas id="mapCanvas"></canvas>
</div>
<script>
const mapInput = document.getElementById('mapInput');
const logInput = document.getElementById('logInput');
const mapWidthInput = document.getElementById('mapWidth');
const mapHeightInput = document.getElementById('mapHeight');
const scaleInput = document.getElementById('scale');
const canvas = document.getElementById('mapCanvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
let mapImage = null;
let pathCoordinates = [];
// --- Event Listeners ---
mapInput.addEventListener('change', handleMapUpload);
logInput.addEventListener('change', handleLogUpload);
[mapWidthInput, mapHeightInput, scaleInput].forEach(input => {
input.addEventListener('change', drawMapAndPath);
input.addEventListener('keyup', drawMapAndPath);
});
// --- File Handling ---
function handleMapUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
mapImage = new Image();
mapImage.onload = function() {
console.log(`Map loaded: ${mapImage.width}x${mapImage.height}`);
statusEl.textContent = 'Map loaded. Ready for log file.';
// Automatically update dimension inputs if they seem default
if (mapWidthInput.value === '2048' && mapHeightInput.value === '2048') {
mapWidthInput.value = mapImage.width;
mapHeightInput.value = mapImage.height;
}
drawMapAndPath();
}
mapImage.src = e.target.result;
}
reader.readAsDataURL(file);
}
function handleLogUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const logText = e.target.result;
console.log('Log file read, starting parse...');
pathCoordinates = parseValheimLog(logText);
statusEl.textContent = `Log parsed. Found ${pathCoordinates.length} position entries.`;
console.log(`Parsed ${pathCoordinates.length} coordinates.`);
drawMapAndPath();
}
reader.readAsText(file);
}
/**
* Parses the raw text from a Valheim log file to extract player positions.
* Looks for lines like: ZNet: Player 'PlayerName' pos=(x, y, z)
*/
function parseValheimLog(logText) {
// Regex to capture the X and Z coordinates from the position log entry.
// It captures the first and third numbers inside pos=(...)
const posRegex = /Player '.*?' pos=\(([^,]+), [^,]+, ([^)]+)\)/g;
const coordinates = [];
let match;
while ((match = posRegex.exec(logText)) !== null) {
const worldX = parseFloat(match[1]);
const worldZ = parseFloat(match[2]); // In Valheim, Z is the North/South axis
if (!isNaN(worldX) && !isNaN(worldZ)) {
coordinates.push({ x: worldX, z: worldZ });
}
}
return coordinates;
}
/**
* Converts Valheim world coordinates to pixel coordinates on the canvas.
*/
function worldToPixel(worldX, worldZ) {
const mapWidth = parseFloat(mapWidthInput.value);
const mapHeight = parseFloat(mapHeightInput.value);
const scale = parseFloat(scaleInput.value);
if (isNaN(mapWidth) || isNaN(mapHeight) || isNaN(scale)) {
return null;
}
const offsetX = mapWidth / 2;
const offsetY = mapHeight / 2;
// The formula to convert coordinates
const pixelX = (worldX * scale) + offsetX;
// The Z-axis needs to be inverted for the Y pixel coordinate
const pixelY = (worldZ * -scale) + offsetY;
return { x: pixelX, y: pixelY };
}
/**
* Clears the canvas and redraws the map image and the entire path.
*/
function drawMapAndPath() {
if (!mapImage) {
// Clear canvas if map is removed
ctx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
// Set canvas size to match the image for 1:1 pixel mapping
canvas.width = mapImage.width;
canvas.height = mapImage.height;
// Draw the map image
ctx.drawImage(mapImage, 0, 0);
if (pathCoordinates.length < 2) {
return; // Not enough points to draw a line
}
ctx.beginPath();
ctx.strokeStyle = 'rgba(255, 69, 0, 0.85)'; // A nice bright orange/red
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Move to the first point
const startPoint = worldToPixel(pathCoordinates[0].x, pathCoordinates[0].z);
if(startPoint) {
ctx.moveTo(startPoint.x, startPoint.y);
}
// Draw lines to all subsequent points
for (let i = 1; i < pathCoordinates.length; i++) {
const point = worldToPixel(pathCoordinates[i].x, pathCoordinates[i].z);
if(point) {
ctx.lineTo(point.x, point.y);
}
}
// Render the path
ctx.stroke();
console.log("Path redrawn.");
}
</script>
</body>
</html>