mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-23 07:37:24 +01:00
feat: add optional AI-based name generation for map entities
This commit is contained in:
parent
3f9a7702d4
commit
5b98f55bc7
20 changed files with 1393 additions and 7 deletions
|
|
@ -87,8 +87,9 @@ async function generateWithAnthropic({key, model, prompt, temperature, onContent
|
|||
|
||||
async function generateWithOllama({key, model, prompt, temperature, onContent}) {
|
||||
const ollamaModelName = key; // for Ollama, 'key' is the actual model name entered by the user
|
||||
const ollamaHost = localStorage.getItem("fmg-ai-ollama-host") || "http://localhost:11434";
|
||||
|
||||
const response = await fetch("http://localhost:11434/api/generate", {
|
||||
const response = await fetch(`${ollamaHost}/api/generate`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ function editBurg(id) {
|
|||
byId("burgType").on("change", changeType);
|
||||
byId("burgCulture").on("change", changeCulture);
|
||||
byId("burgNameReCulture").on("click", generateNameCulture);
|
||||
byId("burgNameAi").on("click", generateNameAi);
|
||||
byId("burgPopulation").on("change", changePopulation);
|
||||
burgBody.querySelectorAll(".burgFeature").forEach(el => el.on("click", toggleFeature));
|
||||
byId("burgLinkOpen").on("click", openBurgLink);
|
||||
|
|
@ -149,6 +150,17 @@ function editBurg(id) {
|
|||
changeName();
|
||||
}
|
||||
|
||||
async function generateNameAi() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const culture = pack.burgs[id].culture;
|
||||
try {
|
||||
burgName.value = await AiNames.generateName("burg", culture);
|
||||
changeName();
|
||||
} catch (error) {
|
||||
tip(error.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function changePopulation() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
|
|||
byId("burgsFilterCulture").addEventListener("change", burgsOverviewAddLines);
|
||||
byId("burgsSearch").addEventListener("input", burgsOverviewAddLines);
|
||||
byId("regenerateBurgNames").addEventListener("click", regenerateNames);
|
||||
byId("regenerateBurgNamesAi").addEventListener("click", regenerateNamesAi);
|
||||
byId("addNewBurg").addEventListener("click", enterAddBurgMode);
|
||||
byId("burgsExport").addEventListener("click", downloadBurgsData);
|
||||
byId("burgNamesImport").addEventListener("click", renameBurgsInBulk);
|
||||
|
|
@ -233,6 +234,39 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
|
|||
});
|
||||
}
|
||||
|
||||
async function regenerateNamesAi() {
|
||||
const elements = Array.from(body.querySelectorAll(":scope > div"));
|
||||
const unlocked = elements.filter(el => !pack.burgs[+el.dataset.id].lock);
|
||||
if (!unlocked.length) return;
|
||||
|
||||
// Group burgs by culture for batch generation
|
||||
const byCulture = new Map();
|
||||
for (const el of unlocked) {
|
||||
const burgId = +el.dataset.id;
|
||||
const culture = pack.burgs[burgId].culture;
|
||||
if (!byCulture.has(culture)) byCulture.set(culture, []);
|
||||
byCulture.get(culture).push({el, burgId});
|
||||
}
|
||||
|
||||
tip("Generating AI names...", false, "info");
|
||||
|
||||
try {
|
||||
for (const [culture, burgs] of byCulture) {
|
||||
const names = await AiNames.generateNames("burg", culture, burgs.length);
|
||||
for (let i = 0; i < burgs.length; i++) {
|
||||
const name = names[i] || Names.getCulture(culture);
|
||||
const {el, burgId} = burgs[i];
|
||||
el.querySelector(".burgName").value = name;
|
||||
pack.burgs[burgId].name = el.dataset.name = name;
|
||||
burgLabels.select("[data-id='" + burgId + "']").text(name);
|
||||
}
|
||||
}
|
||||
tip("AI names generated successfully", true, "success", 3000);
|
||||
} catch (error) {
|
||||
tip(error.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function enterAddBurgMode() {
|
||||
if (this.classList.contains("pressed")) return exitAddBurgMode();
|
||||
customization = 3;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ function editLake() {
|
|||
byId("lakeName").on("input", changeName);
|
||||
byId("lakeNameCulture").on("click", generateNameCulture);
|
||||
byId("lakeNameRandom").on("click", generateNameRandom);
|
||||
byId("lakeNameAi").on("click", generateNameAi);
|
||||
byId("lakeGroup").on("change", changeLakeGroup);
|
||||
byId("lakeGroupAdd").on("click", toggleNewGroupInput);
|
||||
byId("lakeGroupName").on("change", createNewGroup);
|
||||
|
|
@ -140,6 +141,17 @@ function editLake() {
|
|||
lake.name = lakeName.value = Names.getBase(rand(nameBases.length - 1));
|
||||
}
|
||||
|
||||
async function generateNameAi() {
|
||||
const lake = getLake();
|
||||
const cells = Array.from(pack.cells.i.filter(i => pack.cells.f[i] === lake.i));
|
||||
const culture = cells.length ? pack.cells.culture[cells[0]] : 0;
|
||||
try {
|
||||
lake.name = lakeName.value = await AiNames.generateName("lake", culture);
|
||||
} catch (error) {
|
||||
tip(error.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function selectLakeGroup() {
|
||||
const lake = getLake();
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,11 @@ optionsContent.addEventListener("input", event => {
|
|||
else if (id === "themeHueInput") changeThemeHue(value);
|
||||
else if (id === "themeColorInput") changeDialogsTheme(themeColorInput.value, transparencyInput.value);
|
||||
else if (id === "transparencyInput") changeDialogsTheme(themeColorInput.value, value);
|
||||
else if (id === "aiNamesCustomPrompt") localStorage.setItem("fmg-ai-custom-prompt", value);
|
||||
else if (id === "aiNamesLanguageOverride") localStorage.setItem("fmg-ai-language-override", value);
|
||||
else if (id === "aiNamesOllamaHost") localStorage.setItem("fmg-ai-ollama-host", value);
|
||||
else if (id === "aiNamesTemperature") localStorage.setItem("fmg-ai-temperature", value);
|
||||
else if (id === "aiNamesKey") saveAiKey();
|
||||
});
|
||||
|
||||
optionsContent.addEventListener("change", event => {
|
||||
|
|
@ -149,6 +154,7 @@ optionsContent.addEventListener("change", event => {
|
|||
else if (id === "eraInput") changeEra();
|
||||
else if (id === "stateLabelsModeInput") options.stateLabelsMode = value;
|
||||
else if (id === "azgaarAssistant") toggleAssistant();
|
||||
else if (id === "aiNamesModel") changeAiModel(value);
|
||||
});
|
||||
|
||||
optionsContent.addEventListener("click", event => {
|
||||
|
|
@ -164,6 +170,7 @@ optionsContent.addEventListener("click", event => {
|
|||
else if (id === "themeColorRestore") restoreDefaultThemeColor();
|
||||
else if (id === "loadGoogleTranslateButton") loadGoogleTranslate();
|
||||
else if (id === "resetLanguage") resetLanguage();
|
||||
else if (id === "aiNamesKeyHelp") openAiKeyHelp();
|
||||
});
|
||||
|
||||
function mapSizeInputChange() {
|
||||
|
|
@ -191,6 +198,86 @@ function restoreDefaultCanvasSize() {
|
|||
fitMapToScreen();
|
||||
}
|
||||
|
||||
// AI Name Generation settings
|
||||
const AI_MODELS = {
|
||||
"gpt-4o-mini": "openai",
|
||||
"chatgpt-4o-latest": "openai",
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4-turbo": "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",
|
||||
"ollama (local models)": "ollama"
|
||||
};
|
||||
|
||||
function initAiSettings() {
|
||||
const modelSelect = byId("aiNamesModel");
|
||||
if (!modelSelect) return;
|
||||
|
||||
modelSelect.options.length = 0;
|
||||
Object.keys(AI_MODELS).forEach(m => modelSelect.options.add(new Option(m, m)));
|
||||
modelSelect.value = localStorage.getItem("fmg-ai-model") || "gpt-4o-mini";
|
||||
if (!modelSelect.value || !AI_MODELS[modelSelect.value]) modelSelect.value = "gpt-4o-mini";
|
||||
|
||||
const provider = AI_MODELS[modelSelect.value];
|
||||
byId("aiNamesKey").value = localStorage.getItem(`fmg-ai-kl-${provider}`) || "";
|
||||
byId("aiNamesOllamaHost").value = localStorage.getItem("fmg-ai-ollama-host") || "http://localhost:11434";
|
||||
byId("aiNamesTemperature").value = localStorage.getItem("fmg-ai-temperature") || "1";
|
||||
byId("aiNamesLanguageOverride").value = localStorage.getItem("fmg-ai-language-override") || "";
|
||||
byId("aiNamesCustomPrompt").value = localStorage.getItem("fmg-ai-custom-prompt") || "";
|
||||
|
||||
// show/hide ollama host based on selected model
|
||||
const ollamaRow = byId("aiNamesOllamaHost").closest("tr");
|
||||
ollamaRow.style.display = provider === "ollama" ? "" : "none";
|
||||
}
|
||||
|
||||
function changeAiModel(value) {
|
||||
localStorage.setItem("fmg-ai-model", value);
|
||||
const provider = AI_MODELS[value];
|
||||
|
||||
// load key for the selected provider
|
||||
byId("aiNamesKey").value = localStorage.getItem(`fmg-ai-kl-${provider}`) || "";
|
||||
|
||||
// show/hide ollama host row
|
||||
const ollamaRow = byId("aiNamesOllamaHost").closest("tr");
|
||||
ollamaRow.style.display = provider === "ollama" ? "" : "none";
|
||||
}
|
||||
|
||||
function saveAiKey() {
|
||||
const model = byId("aiNamesModel").value;
|
||||
const provider = AI_MODELS[model];
|
||||
const key = byId("aiNamesKey").value;
|
||||
localStorage.setItem(`fmg-ai-kl-${provider}`, key);
|
||||
}
|
||||
|
||||
function openAiKeyHelp() {
|
||||
const model = byId("aiNamesModel").value;
|
||||
const provider = AI_MODELS[model];
|
||||
const links = {
|
||||
openai: "https://platform.openai.com/account/api-keys",
|
||||
anthropic: "https://console.anthropic.com/account/keys",
|
||||
ollama: "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Ollama-text-generation"
|
||||
};
|
||||
openURL(links[provider] || links.openai);
|
||||
}
|
||||
|
||||
// Initialize AI settings when Options tab is first shown
|
||||
{
|
||||
const optionsTab = byId("optionsTab");
|
||||
if (optionsTab) {
|
||||
const origClick = optionsTab.onclick;
|
||||
optionsTab.addEventListener("click", function () {
|
||||
initAiSettings();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// on map creation
|
||||
function applyGraphSize() {
|
||||
graphWidth = +mapWidthInput.value;
|
||||
|
|
|
|||
|
|
@ -542,6 +542,8 @@ function editProvinces() {
|
|||
byId("provinceNameEditorShortRandom").on("click", regenerateShortNameRandom);
|
||||
byId("provinceNameEditorAddForm").on("click", addCustomForm);
|
||||
byId("provinceNameEditorFullRegenerate").on("click", regenerateFullName);
|
||||
byId("provinceNameEditorShortAi").on("click", regenerateShortNameAi);
|
||||
byId("provinceNameEditorFullAi").on("click", regenerateFullNameAi);
|
||||
|
||||
function regenerateShortNameCulture() {
|
||||
const province = +provinceNameEditor.dataset.province;
|
||||
|
|
@ -576,6 +578,31 @@ function editProvinces() {
|
|||
}
|
||||
}
|
||||
|
||||
async function regenerateShortNameAi() {
|
||||
const province = +provinceNameEditor.dataset.province;
|
||||
const culture = pack.cells.culture[pack.provinces[province].center];
|
||||
try {
|
||||
const name = await AiNames.generateName("province", culture, {form: pack.provinces[province].formName});
|
||||
byId("provinceNameEditorShort").value = name;
|
||||
} catch (err) {
|
||||
tip("AI generation failed: " + err.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
async function regenerateFullNameAi() {
|
||||
const short = byId("provinceNameEditorShort").value;
|
||||
const form = byId("provinceNameEditorSelectForm").value;
|
||||
if (!form || !short) { regenerateFullName(); return; }
|
||||
try {
|
||||
const province = +provinceNameEditor.dataset.province;
|
||||
const culture = pack.cells.culture[pack.provinces[province].center];
|
||||
const fullName = await AiNames.generateName("provinceFullName", culture, {form, stateName: short});
|
||||
byId("provinceNameEditorFull").value = fullName;
|
||||
} catch (err) {
|
||||
tip("AI generation failed: " + err.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function applyNameChange(p) {
|
||||
p.name = byId("provinceNameEditorShort").value;
|
||||
p.formName = byId("provinceNameEditorSelectForm").value;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ function editRiver(id) {
|
|||
byId("riverType").on("input", changeType);
|
||||
byId("riverNameCulture").on("click", generateNameCulture);
|
||||
byId("riverNameRandom").on("click", generateNameRandom);
|
||||
byId("riverNameAi").on("click", generateNameAi);
|
||||
byId("riverMainstem").on("change", changeParent);
|
||||
byId("riverSourceWidth").on("input", changeSourceWidth);
|
||||
byId("riverWidthFactor").on("input", changeWidthFactor);
|
||||
|
|
@ -212,6 +213,17 @@ function editRiver(id) {
|
|||
if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length - 1));
|
||||
}
|
||||
|
||||
async function generateNameAi() {
|
||||
const r = getRiver();
|
||||
if (!r) return;
|
||||
const culture = pack.cells.culture[r.mouth];
|
||||
try {
|
||||
r.name = riverName.value = await AiNames.generateName("river", culture);
|
||||
} catch (error) {
|
||||
tip(error.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function changeParent() {
|
||||
const r = getRiver();
|
||||
r.parent = +this.value;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ function overviewRivers() {
|
|||
byId("riverCreateNew").on("click", createRiver);
|
||||
byId("riversBasinHighlight").on("click", toggleBasinsHightlight);
|
||||
byId("riversExport").on("click", downloadRiversData);
|
||||
byId("riversRegenerateNamesAi").on("click", regenerateRiverNamesAi);
|
||||
byId("riversRemoveAll").on("click", triggerAllRiversRemove);
|
||||
byId("riversSearch").on("input", riversOverviewAddLines);
|
||||
|
||||
|
|
@ -215,4 +216,37 @@ function overviewRivers() {
|
|||
rivers.selectAll("*").remove();
|
||||
riversOverviewAddLines();
|
||||
}
|
||||
|
||||
async function regenerateRiverNamesAi() {
|
||||
const elements = Array.from(body.querySelectorAll(":scope > div"));
|
||||
if (!elements.length) return;
|
||||
|
||||
const byCulture = new Map();
|
||||
for (const el of elements) {
|
||||
const riverId = +el.dataset.id;
|
||||
const river = pack.rivers.find(r => r.i === riverId);
|
||||
if (!river || river.lock) continue;
|
||||
const mouth = river.mouth;
|
||||
const culture = mouth != null ? pack.cells.culture[mouth] : 0;
|
||||
if (!byCulture.has(culture)) byCulture.set(culture, []);
|
||||
byCulture.get(culture).push({el, river});
|
||||
}
|
||||
|
||||
tip("Generating AI names...", false, "info");
|
||||
|
||||
try {
|
||||
for (const [culture, items] of byCulture) {
|
||||
const names = await AiNames.generateNames("river", culture, items.length);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const name = names[i] || Rivers.getName(items[i].river.mouth);
|
||||
const {el, river} = items[i];
|
||||
river.name = el.dataset.name = name;
|
||||
el.querySelector(".riverName").textContent = name;
|
||||
}
|
||||
}
|
||||
tip("AI names generated successfully", true, "success", 3000);
|
||||
} catch (error) {
|
||||
tip(error.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ function editRoute(id) {
|
|||
byId("routeGroupEdit").on("click", editRouteGroups);
|
||||
byId("routeEditStyle").on("click", editRouteGroupStyle);
|
||||
byId("routeGenerateName").on("click", generateName);
|
||||
byId("routeGenerateNameAi").on("click", generateNameAi);
|
||||
|
||||
function getRoute() {
|
||||
const routeId = +elSelected.attr("id").slice(5);
|
||||
|
|
@ -351,6 +352,25 @@ function editRoute(id) {
|
|||
route.name = routeName.value = Routes.generateName(route);
|
||||
}
|
||||
|
||||
async function generateNameAi() {
|
||||
const route = getRoute();
|
||||
const connectedBurgs = [];
|
||||
for (const [_x, _y, cellId] of route.points) {
|
||||
const burgId = pack.cells.burg[cellId];
|
||||
if (burgId && pack.burgs[burgId]) connectedBurgs.push(pack.burgs[burgId].name);
|
||||
}
|
||||
const culture = pack.cells.culture[route.points[0][2]] || 0;
|
||||
try {
|
||||
const name = await AiNames.generateName("route", culture, {
|
||||
routeGroup: route.group,
|
||||
connectedBurgs: connectedBurgs.slice(0, 3)
|
||||
});
|
||||
route.name = routeName.value = name;
|
||||
} catch (err) {
|
||||
if (err.message !== "No API key configured") tip("AI name generation failed: " + err.message, false, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function showRouteElevationProfile() {
|
||||
const route = getRoute();
|
||||
const length = rn(route.length * distanceScale);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ function overviewRoutes() {
|
|||
byId("routesOverviewRefresh").on("click", routesOverviewAddLines);
|
||||
byId("routesCreateNew").on("click", createRoute);
|
||||
byId("routesExport").on("click", downloadRoutesData);
|
||||
byId("routesRegenerateNamesAi").on("click", regenerateRouteNamesAi);
|
||||
byId("routesLockAll").on("click", toggleLockAll);
|
||||
byId("routesRemoveAll").on("click", triggerAllRoutesRemove);
|
||||
byId("routesSearch").on("input", routesOverviewAddLines);
|
||||
|
|
@ -190,4 +191,36 @@ function overviewRoutes() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function regenerateRouteNamesAi() {
|
||||
const elements = Array.from(body.querySelectorAll(":scope > div"));
|
||||
if (!elements.length) return;
|
||||
|
||||
const byCulture = new Map();
|
||||
for (const el of elements) {
|
||||
const routeId = +el.dataset.id;
|
||||
const route = pack.routes.find(r => r.i === routeId);
|
||||
if (!route || route.lock) continue;
|
||||
const culture = route.points?.[0] ? pack.cells.culture[route.points[0][2]] : 0;
|
||||
if (!byCulture.has(culture)) byCulture.set(culture, []);
|
||||
byCulture.get(culture).push({el, route});
|
||||
}
|
||||
|
||||
tip("Generating AI names...", false, "info");
|
||||
|
||||
try {
|
||||
for (const [culture, items] of byCulture) {
|
||||
const names = await AiNames.generateNames("route", culture, items.length);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const name = names[i] || Routes.generateName(items[i].route);
|
||||
const {el, route} = items[i];
|
||||
route.name = el.dataset.name = name;
|
||||
el.querySelector("div[data-tip='Route name']").textContent = name;
|
||||
}
|
||||
}
|
||||
tip("AI names generated successfully", true, "success", 3000);
|
||||
} catch (error) {
|
||||
tip(error.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ function processFeatureRegeneration(event, button) {
|
|||
else if (button === "regenerateIce") regenerateIce();
|
||||
else if (button === "regenerateMarkers") regenerateMarkers();
|
||||
else if (button === "regenerateZones") regenerateZones(event);
|
||||
else if (button === "regenerateAiNames") regenerateAllAiNames();
|
||||
}
|
||||
|
||||
async function openEmblemEditor() {
|
||||
|
|
@ -583,6 +584,189 @@ function regenerateZones(event) {
|
|||
}
|
||||
}
|
||||
|
||||
async function regenerateAllAiNames() {
|
||||
const {states, cultures, burgs, rivers, religions, routes, zones} = pack;
|
||||
|
||||
tip("Regenerating all names with AI...", false, "info");
|
||||
|
||||
try {
|
||||
// Cultures
|
||||
for (const c of cultures) {
|
||||
if (!c.i || c.removed || c.lock) continue;
|
||||
const names = await AiNames.generateNames("culture", c.i, 1);
|
||||
if (names[0]) c.name = names[0];
|
||||
}
|
||||
|
||||
// States
|
||||
const byCulture = new Map();
|
||||
for (const s of states) {
|
||||
if (!s.i || s.removed || s.lock) continue;
|
||||
if (!byCulture.has(s.culture)) byCulture.set(s.culture, []);
|
||||
byCulture.get(s.culture).push(s);
|
||||
}
|
||||
for (const [culture, items] of byCulture) {
|
||||
const names = await AiNames.generateNames("state", culture, items.length);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const s = items[i];
|
||||
s.name = names[i] || s.name;
|
||||
}
|
||||
}
|
||||
|
||||
// State full names (batched - 1 API call per culture)
|
||||
for (const [culture, items] of byCulture) {
|
||||
const withForm = items.filter(s => s.formName);
|
||||
const withoutForm = items.filter(s => !s.formName);
|
||||
for (const s of withoutForm) s.fullName = s.name;
|
||||
if (withForm.length) {
|
||||
const inputs = withForm.map(s => ({shortName: s.name, form: s.formName}));
|
||||
const fullNames = await AiNames.generateFullNamesBatch(inputs, culture);
|
||||
for (let i = 0; i < withForm.length; i++) {
|
||||
withForm[i].fullName = fullNames[i] || `${withForm[i].formName} of ${withForm[i].name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Burgs
|
||||
const burgsByCulture = new Map();
|
||||
for (const b of burgs) {
|
||||
if (!b.i || b.removed || b.lock) continue;
|
||||
if (!burgsByCulture.has(b.culture)) burgsByCulture.set(b.culture, []);
|
||||
burgsByCulture.get(b.culture).push(b);
|
||||
}
|
||||
for (const [culture, items] of burgsByCulture) {
|
||||
const names = await AiNames.generateNames("burg", culture, items.length);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
items[i].name = names[i] || items[i].name;
|
||||
}
|
||||
}
|
||||
|
||||
// Rivers
|
||||
const riversByCulture = new Map();
|
||||
for (const r of rivers) {
|
||||
if (!r.i || r.lock) continue;
|
||||
const culture = r.mouth != null ? pack.cells.culture[r.mouth] : 0;
|
||||
if (!riversByCulture.has(culture)) riversByCulture.set(culture, []);
|
||||
riversByCulture.get(culture).push(r);
|
||||
}
|
||||
for (const [culture, items] of riversByCulture) {
|
||||
const names = await AiNames.generateNames("river", culture, items.length);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
items[i].name = names[i] || items[i].name;
|
||||
}
|
||||
}
|
||||
|
||||
// Religions (names + deities in one batch per culture)
|
||||
const relByCulture = new Map();
|
||||
for (const r of religions) {
|
||||
if (!r.i || r.removed || r.lock) continue;
|
||||
if (!relByCulture.has(r.culture)) relByCulture.set(r.culture, []);
|
||||
relByCulture.get(r.culture).push(r);
|
||||
}
|
||||
for (const [culture, items] of relByCulture) {
|
||||
const inputs = items.map(r => ({type: r.type, form: r.form}));
|
||||
const results = await AiNames.generateReligionsBatch(inputs, culture);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (results[i]) {
|
||||
if (results[i].name) items[i].name = results[i].name;
|
||||
items[i].deity = results[i].deity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Routes (batched by culture, chunked for large maps)
|
||||
const CHUNK_SIZE = 50;
|
||||
const routesByCulture = new Map();
|
||||
for (const route of routes) {
|
||||
if (!route.points || route.lock) continue;
|
||||
const culture = route.points[0] ? pack.cells.culture[route.points[0][2]] : 0;
|
||||
if (!routesByCulture.has(culture)) routesByCulture.set(culture, []);
|
||||
routesByCulture.get(culture).push(route);
|
||||
}
|
||||
for (const [culture, items] of routesByCulture) {
|
||||
for (let start = 0; start < items.length; start += CHUNK_SIZE) {
|
||||
const chunk = items.slice(start, start + CHUNK_SIZE);
|
||||
const names = await AiNames.generateNames("route", culture, chunk.length);
|
||||
for (let i = 0; i < chunk.length; i++) {
|
||||
chunk[i].name = names[i] || chunk[i].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zones (translate types + generate context-aware descriptions)
|
||||
if (zones.length) {
|
||||
const uniqueTypes = [...new Set(zones.map(z => z.type))];
|
||||
const translatedTypes = await AiNames.translateTerms(uniqueTypes, 0);
|
||||
const typeMap = Object.fromEntries(uniqueTypes.map((t, i) => [t, translatedTypes[i]]));
|
||||
|
||||
for (const z of zones) {
|
||||
z.type = typeMap[z.type] || z.type;
|
||||
}
|
||||
|
||||
// Build context for each zone (biome + nearest burg)
|
||||
const zoneInputs = zones.map(z => {
|
||||
const cells = z.cells || [];
|
||||
const centerCell = cells[Math.floor(cells.length / 2)] || cells[0];
|
||||
|
||||
let biome = "unknown";
|
||||
if (centerCell != null && pack.cells.biome[centerCell] != null) {
|
||||
biome = biomesData.name[pack.cells.biome[centerCell]] || "unknown";
|
||||
}
|
||||
|
||||
let nearBurg = "";
|
||||
for (const cellId of cells) {
|
||||
const burgId = pack.cells.burg[cellId];
|
||||
if (burgId && pack.burgs[burgId]) { nearBurg = pack.burgs[burgId].name; break; }
|
||||
}
|
||||
|
||||
return {type: z.type, biome, nearBurg};
|
||||
});
|
||||
|
||||
const descriptions = await AiNames.generateZoneDescriptionsBatch(zoneInputs, 0);
|
||||
for (let i = 0; i < zones.length; i++) {
|
||||
if (descriptions[i]) zones[i].name = descriptions[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Provinces
|
||||
const provinces = pack.provinces;
|
||||
const provsByCulture = new Map();
|
||||
for (const p of provinces) {
|
||||
if (!p.i || p.removed || p.lock) continue;
|
||||
const culture = pack.cells.culture[p.center];
|
||||
if (!provsByCulture.has(culture)) provsByCulture.set(culture, []);
|
||||
provsByCulture.get(culture).push(p);
|
||||
}
|
||||
for (const [culture, items] of provsByCulture) {
|
||||
const names = await AiNames.generateNames("province", culture, items.length);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
items[i].name = names[i] || items[i].name;
|
||||
}
|
||||
}
|
||||
|
||||
// Province full names (batched - 1 API call per culture)
|
||||
for (const [culture, items] of provsByCulture) {
|
||||
const withForm = items.filter(p => p.formName);
|
||||
const withoutForm = items.filter(p => !p.formName);
|
||||
for (const p of withoutForm) p.fullName = p.name;
|
||||
if (withForm.length) {
|
||||
const inputs = withForm.map(p => ({shortName: p.name, form: p.formName}));
|
||||
const fullNames = await AiNames.generateFullNamesBatch(inputs, culture);
|
||||
for (let i = 0; i < withForm.length; i++) {
|
||||
withForm[i].fullName = fullNames[i] || `${withForm[i].formName} of ${withForm[i].name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw labels
|
||||
drawStateLabels();
|
||||
drawBurgLabels();
|
||||
|
||||
tip("All AI names regenerated successfully", true, "success", 4000);
|
||||
} catch (error) {
|
||||
tip("AI name regeneration failed: " + error.message, true, "error", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function unpressClickToAddButton() {
|
||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
restoreDefaultEvents();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ function editZones() {
|
|||
byId("zonesManuallyApply").on("click", applyZonesManualAssignent);
|
||||
byId("zonesManuallyCancel").on("click", cancelZonesManualAssignent);
|
||||
byId("zonesAdd").on("click", addZonesLayer);
|
||||
byId("zonesRegenerateNamesAi").on("click", regenerateZoneNamesAi);
|
||||
byId("zonesExport").on("click", downloadZonesData);
|
||||
byId("zonesRemove").on("click", e => e.target.classList.toggle("pressed"));
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ function editZones() {
|
|||
}
|
||||
|
||||
if (ev.target.closest("fill-box")) changeFill(ev.target.closest("fill-box").getAttribute("fill"), zone);
|
||||
else if (ev.target.classList.contains("icon-robot")) generateZoneNameAi(zone, line);
|
||||
else if (ev.target.classList.contains("zonePopulation")) changePopulation(zone);
|
||||
else if (ev.target.classList.contains("zoneRemove")) zoneRemove(zone);
|
||||
else if (ev.target.classList.contains("zoneHide")) toggleVisibility(zone);
|
||||
|
|
@ -94,6 +96,7 @@ function editZones() {
|
|||
}">
|
||||
<fill-box fill="${color}"></fill-box>
|
||||
<input data-tip="Zone description. Click and type to change" style="width: 11em" class="zoneName" value="${name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Generate zone name with AI" class="icon-robot hiddenIcon" style="visibility: hidden"></span>
|
||||
<input data-tip="Zone type. Click and type to change" class="zoneType" value="${type}">
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells hide">${cells.length}</div>
|
||||
|
|
@ -402,6 +405,17 @@ function editZones() {
|
|||
zones.select("#zone" + zone.i).attr("data-description", value);
|
||||
}
|
||||
|
||||
async function generateZoneNameAi(zone, line) {
|
||||
try {
|
||||
const name = await AiNames.generateName("zone", 0, {zoneType: zone.type});
|
||||
zone.name = name;
|
||||
zones.select("#zone" + zone.i).attr("data-description", name);
|
||||
line.querySelector("input.zoneName").value = name;
|
||||
} catch (err) {
|
||||
if (err.message !== "No API key configured") tip("AI name generation failed: " + err.message, false, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function changeType(zone, value) {
|
||||
zone.type = value;
|
||||
zones.select("#zone" + zone.i).attr("data-type", value);
|
||||
|
|
@ -492,4 +506,27 @@ function editZones() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function regenerateZoneNamesAi() {
|
||||
const elements = Array.from(body.querySelectorAll(":scope > div.states"));
|
||||
if (!elements.length) return;
|
||||
|
||||
tip("Generating AI names...", false, "info");
|
||||
|
||||
try {
|
||||
const names = await AiNames.generateNames("zone", 0, elements.length);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements[i];
|
||||
const zone = pack.zones.find(z => z.i === +el.dataset.id);
|
||||
if (!zone) continue;
|
||||
const name = names[i] || zone.name;
|
||||
zone.name = name;
|
||||
zones.select("#zone" + zone.i).attr("data-description", name);
|
||||
el.querySelector("input.zoneName").value = name;
|
||||
}
|
||||
tip("AI names generated successfully", true, "success", 3000);
|
||||
} catch (error) {
|
||||
tip(error.message, true, "error", 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue