Merge branch 'master' of github.com-personal:Azgaar/Fantasy-Map-Generator into burg-groups

This commit is contained in:
Azgaar 2025-10-30 15:29:46 +01:00
commit 8e13a3a0de
8 changed files with 211 additions and 32 deletions

89
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,89 @@
# Fantasy Map Generator
Azgaar's Fantasy Map Generator is a client-side JavaScript web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Working Effectively
- **CRITICAL**: This is a static web application - NO build process needed. No npm install, no compilation, no bundling.
- Run the application using HTTP server (required - cannot run with file:// protocol):
- `python3 -m http.server 8000` - takes 2-3 seconds to start
- Access at: `http://localhost:8000`
## Validation
- Always manually validate any changes by:
1. Starting the HTTP server (NEVER CANCEL - wait for full startup)
2. Navigate to the application in browser
3. Click the "►" button to open the menu and generate a new map
4. **CRITICAL VALIDATION**: Verify the map generates with countries, cities, roads, and geographic features
5. Test UI interaction: click "Layers" button, verify layer controls work
6. Test regeneration: click "New Map!" button, verify new map generates correctly
- **Known Issues**: Google Analytics and font loading errors are normal (blocked external resources)
## Repository Structure
### Core Files
- `index.html` - Main application entry point
- `main.js` - Core application logic
- `versioning.js` - Version management and update handling
### Key Directories
- `modules/` - core functionality modules:
- `modules/ui/` - UI components (editors, tools, style management)
- `modules/dynamic/` - runtime modules (export, installation)
- `modules/renderers/` - drawing and rendering logic
- `utils/` - utility libraries (math, arrays, strings, etc.)
- `styles/` - visual style presets (JSON files)
- `libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
- `images/` - backgrounds, UI elements
- `charges/` - heraldic symbols and coat of arms elements
- `config/` - Heightmap templates and configurations
- `heightmaps/` - Terrain generation data
## Common Tasks
### Making Code Changes
1. Edit JavaScript files directly (no compilation needed)
2. Refresh browser to see changes immediately
3. **ALWAYS test map generation** after making changes
4. Update version in `versioning.js` for all changes
5. Update file hashes in `index.html` for changed files (format: `file.js?v=1.108.1`)
### Debugging Map Generation
- Open browser developer tools console
- Look for timing logs, e.g. "TOTAL: ~0.76s"
- Map generation logs show each step (heightmap, rivers, states, etc.)
- Error messages will indicate specific generation failures
### Testing Different Map Types
- Use "New Map!" button for quick regeneration
- Access "Layers" menu to change map visualization
- Available presets: Political, Cultural, Religions, Biomes, Heightmap, Physical, Military
## Troubleshooting
### Application Won't Load
- Ensure using HTTP server (not file://)
- Check console for JavaScript errors
- Verify all files are present in repository
### Map Generation Fails
- Check browser console for error messages
- Look for specific module failures in generation logs
- Try refreshing page and generating new map
### Performance Issues
- Map generation should complete in ~1 second for standard configurations
- If slower, check browser console for errors
Remember: This is a sophisticated client-side application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. Always validate that your changes preserve the core map generation functionality.

View file

@ -5009,11 +5009,16 @@
</label>
<label for="aiGeneratorKey"
>Key:
<input id="aiGeneratorKey" placeholder="Enter API key" class="icon-key" />
<input
id="aiGeneratorKey"
placeholder="Enter API key"
class="icon-key"
data-tip="Enter API key. Note: the Generator doesn't store the key or any generated data"
/>
<button
id="aiGeneratorKeyHelp"
class="icon-help-circled"
data-tip="Open provider's website to get the API key there. Note: the Map Genenerator doesn't store the key or any generated data"
data-tip="Click to see the usage instructions"
/>
</label>
</div>
@ -8329,7 +8334,7 @@
<script src="utils/polyfills.js?v=1.99.00"></script>
<script src="utils/probabilityUtils.js?v=1.99.05"></script>
<script src="utils/stringUtils.js?v=1.106.0"></script>
<script src="utils/languageUtils.js?v=1.99.00"></script>
<script src="utils/languageUtils.js?v=1.108.11"></script>
<script src="utils/unitUtils.js?v=1.99.00"></script>
<script src="utils/pathUtils.js?v=1.106.0"></script>
<script defer src="utils/debugUtils.js?v=1.106.0"></script>
@ -8389,9 +8394,9 @@
<script defer src="modules/ui/relief-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/burg-group-editor.js?v=1.106.0"></script>
<script defer src="modules/ui/burg-editor.js?v=1.106.6"></script>
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
<script defer src="modules/ui/units-editor.js?v=1.108.12"></script>
<script defer src="modules/ui/notes-editor.js?v=1.107.3"></script>
<script defer src="modules/ui/ai-generator.js?v=1.105.22"></script>
<script defer src="modules/ui/ai-generator.js?v=1.108.8"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/zones-editor.js?v=1.105.20"></script>
<script defer src="modules/ui/burgs-overview.js?v=1.105.15"></script>
@ -8399,7 +8404,7 @@
<script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/military-overview.js?v=1.108.5"></script>
<script defer src="modules/ui/regiments-overview.js?v=1.108.5"></script>
<script defer src="modules/ui/markers-overview.js?v=1.108.5"></script>
<script defer src="modules/ui/markers-overview.js?v=1.108.10"></script>
<script defer src="modules/ui/regiment-editor.js?v=1.108.5"></script>
<script defer src="modules/ui/battle-screen.js?v=1.108.5"></script>
<script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script>
@ -8414,7 +8419,7 @@
<script defer src="modules/io/save.js?v=1.107.4"></script>
<script defer src="modules/io/load.js?v=1.108.0"></script>
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
<script defer src="modules/io/export.js?v=1.100.00"></script>
<script defer src="modules/io/export.js?v=1.108.11"></script>
<script defer src="modules/renderers/draw-features.js?v=1.108.2"></script>
<script defer src="modules/renderers/draw-borders.js?v=1.104.0"></script>

View file

@ -330,6 +330,40 @@ async function getMapURL(
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
}
{
// replace external marker icons
const externalMarkerImages = cloneEl.querySelectorAll('#markers image[href]:not([href=""])');
const imageHrefs = Array.from(externalMarkerImages).map(img => img.getAttribute("href"));
for (const url of imageHrefs) {
await new Promise(resolve => {
getBase64(url, base64 => {
externalMarkerImages.forEach(img => {
if (img.getAttribute("href") === url) img.setAttribute("href", base64);
});
resolve();
});
});
}
}
{
// replace external regiment icons
const externalRegimentImages = cloneEl.querySelectorAll('#armies image[href]:not([href=""])');
const imageHrefs = Array.from(externalRegimentImages).map(img => img.getAttribute("href"));
for (const url of imageHrefs) {
await new Promise(resolve => {
getBase64(url, base64 => {
externalRegimentImages.forEach(img => {
if (img.getAttribute("href") === url) img.setAttribute("href", base64);
});
resolve();
});
});
}
}
if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog")?.remove(); // remove unused fog
if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths")?.remove(); // removed unused statePaths
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths
@ -495,11 +529,10 @@ function saveGeoJsonCells() {
function saveGeoJsonRoutes() {
const features = pack.routes.map(({i, points, group, name = null}) => {
const coordinates = points.map(([x, y]) => getCoordinates(x, y, 4));
const id = `route${i}`;
return {
type: "Feature",
geometry: {type: "LineString", coordinates},
properties: {id, group, name}
properties: {id: i, group, name}
};
});
const json = {type: "FeatureCollection", features};
@ -514,11 +547,10 @@ function saveGeoJsonRivers() {
if (!cells || cells.length < 2) return;
const meanderedPoints = Rivers.addMeandering(cells, points);
const coordinates = meanderedPoints.map(([x, y]) => getCoordinates(x, y, 4));
const id = `river${i}`;
return {
type: "Feature",
geometry: {type: "LineString", coordinates},
properties: {id, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}
properties: {id: i, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}
};
}
);
@ -532,9 +564,8 @@ function saveGeoJsonMarkers() {
const features = pack.markers.map(marker => {
const {i, type, icon, x, y, size, fill, stroke} = marker;
const coordinates = getCoordinates(x, y, 4);
const id = `marker${i}`;
const note = notes.find(note => note.id === id);
const properties = {id, type, icon, x, y, ...note, size, fill, stroke};
const properties = {id: i, type, icon, x, y, ...note, size, fill, stroke};
return {type: "Feature", geometry: {type: "Point", coordinates}, properties};
});

View file

@ -8,6 +8,10 @@ const PROVIDERS = {
anthropic: {
keyLink: "https://console.anthropic.com/account/keys",
generate: generateWithAnthropic
},
ollama: {
keyLink: "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Ollama-text-generation",
generate: generateWithOllama
}
};
@ -18,11 +22,16 @@ const MODELS = {
"chatgpt-4o-latest": "openai",
"gpt-4o": "openai",
"gpt-4-turbo": "openai",
"o1-preview": "openai",
"o1-mini": "openai",
o3: "openai",
"o3-mini": "openai",
"o3-pro": "openai",
"o4-mini": "openai",
"claude-opus-4-20250514": "anthropic",
"claude-sonnet-4-20250514": "anthropic",
"claude-3-5-haiku-latest": "anthropic",
"claude-3-5-sonnet-latest": "anthropic",
"claude-3-opus-latest": "anthropic"
"claude-3-opus-latest": "anthropic",
"ollama (local models)": "ollama"
};
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
@ -76,10 +85,36 @@ async function generateWithAnthropic({key, model, prompt, temperature, onContent
await handleStream(response, getContent);
}
async function generateWithOllama({key, model, prompt, temperature, onContent}) {
const ollamaModelName = key; // for Ollama, 'key' is the actual model name entered by the user
const response = await fetch("http://localhost:11434/api/generate", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
model: ollamaModelName,
prompt,
system: SYSTEM_MESSAGE,
options: {temperature},
stream: true
})
});
const getContent = json => {
if (json.response) onContent(json.response);
};
await handleStream(response, getContent);
}
async function handleStream(response, getContent) {
if (!response.ok) {
const json = await response.json();
throw new Error(json?.error?.message || "Failed to generate");
let errorMessage = `Failed to generate (${response.status} ${response.statusText})`;
try {
const json = await response.json();
errorMessage = json.error?.message || json.error || errorMessage;
} catch {}
throw new Error(errorMessage);
}
const reader = response.body.getReader();
@ -95,13 +130,14 @@ async function handleStream(response, getContent) {
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (line.startsWith("data: ") && line !== "data: [DONE]") {
try {
const json = JSON.parse(line.slice(6));
getContent(json);
} catch (jsonError) {
ERROR && console.error(`Failed to parse JSON:`, jsonError, `Line: ${line}`);
}
if (!line) continue;
if (line === "data: [DONE]") break;
try {
const parsed = line.startsWith("data: ") ? JSON.parse(line.slice(6)) : JSON.parse(line);
getContent(parsed);
} catch (error) {
ERROR && console.error("Failed to parse line:", line, error);
}
}

View file

@ -214,15 +214,15 @@ function overviewMarkers() {
const body = pack.markers.map(marker => {
const {i, type, icon, x, y} = marker;
const id = `marker${i}`;
const note = notes.find(note => note.id === id);
const note = notes.find(note => note.id === "marker" + i);
const name = note ? quote(note.name) : "Unknown";
const legend = note ? quote(note.legend) : "";
const lat = getLatitude(y, 2);
const lon = getLongitude(x, 2);
return [id, type, icon, name, legend, x, y, lat, lon].join(",");
return [i, type, icon, name, legend, x, y, lat, lon].join(",");
});
const data = headers + body.join("\n");

View file

@ -121,11 +121,16 @@ function editUnits() {
function addRuler() {
if (!layerIsOn("toggleRulers")) toggleRulers();
const width = Math.min(graphWidth, svgWidth);
const height = Math.min(graphHeight, svgHeight);
const pt = byId("map").createSVGPoint();
(pt.x = graphWidth / 2), (pt.y = graphHeight / 4);
pt.x = width / 2;
pt.y = height / 4;
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
const dx = graphWidth / 4 / scale;
const dy = (rulers.data.length * 40) % (graphHeight / 2);
const dx = width / 4 / scale;
const dy = (rulers.data.length * 40) % (height / 2);
const from = [(p.x - dx) | 0, (p.y + dy) | 0];
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
rulers.create(Ruler, [from, to]).draw();

13
run_python_server.sh Normal file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env sh
if command -v python3 >/dev/null 2>&1; then
PYTHON=python3
elif command -v python >/dev/null 2>&1; then
PYTHON=python
else
echo "Neither 'python' nor 'python3' was found. Please install Python 3 package." >&2
exit 1
fi
chromium http://localhost:8000
$PYTHON -m http.server 8000

View file

@ -135,7 +135,7 @@ const adjectivizationRules = [
{
name: "an",
probability: 0.5,
condition: new RegExp("^[a-zA-Z]{0-7}$"),
condition: new RegExp("^[a-zA-Z]{0,7}$"),
action: noun => trimVowels(noun) + "an"
}
];